Fix export panel target handling

This commit is contained in:
Danila Poyarkov 2026-03-29 19:16:45 +03:00
parent d25c48a33d
commit 581abf3b93
3 changed files with 135 additions and 45 deletions

View file

@ -1,14 +1,12 @@
import { ref } from 'vue'
import { computed, ref } from 'vue'
import { BUILTIN_IO_FORMATS, IORegistry } from '@open-pencil/core'
import { useEditor } from '@open-pencil/vue/context/editorContext'
import { useSceneComputed } from '@open-pencil/vue/internal/useSceneComputed'
export type ExportFormatId = 'png' | 'jpg' | 'webp' | 'svg' | 'fig'
export type ExportPanelTarget = 'selection' | 'page'
/**
* Single export preset row managed by {@link useExport}.
*/
interface ExportSetting {
scale: number
format: ExportFormatId
@ -18,60 +16,104 @@ const SCALES = [0.5, 0.75, 1, 1.5, 2, 3, 4] as const
const FORMATS: ExportFormatId[] = ['png', 'jpg', 'webp', 'svg', 'fig']
const io = new IORegistry(BUILTIN_IO_FORMATS)
/**
* Returns selection-aware export settings for export panel UIs.
*
* This composable manages export presets such as scale and format while also
* exposing the current export target label derived from the selection.
*/
function createDefaultSetting(): ExportSetting {
return { scale: 1, format: 'png' }
}
export function useExport() {
const editor = useEditor()
const settings = ref<ExportSetting[]>([{ scale: 1, format: 'png' }])
const selectionSettings = ref<ExportSetting[]>([createDefaultSetting()])
const pageSettings = ref<ExportSetting[]>([createDefaultSetting()])
const selectedIds = useSceneComputed(() => [...editor.state.selectedIds])
const formatSupportsScale = (format: ExportFormatId) =>
io.getFormat(format)?.exportOptions?.scale ?? false
const nodeName = useSceneComputed(() => {
const selectedNodeName = useSceneComputed(() => {
const ids = editor.state.selectedIds
if (ids.size === 1) {
const id = [...ids][0]
return editor.graph.getNode(id)?.name ?? 'Export'
}
return `${ids.size} layers`
if (ids.size > 1) return `${ids.size} layers`
return null
})
function addSetting() {
const last = settings.value[settings.value.length - 1]
const currentPageName = useSceneComputed(() => {
const page = editor.graph.getNode(editor.state.currentPageId)
return page?.name ?? 'Page'
})
const hasSelection = computed(() => selectedIds.value.length > 0)
const activeTarget = computed<ExportPanelTarget>(() =>
hasSelection.value ? 'selection' : 'page'
)
const activeName = computed(() =>
activeTarget.value === 'selection' ? (selectedNodeName.value ?? 'Export') : currentPageName.value
)
const activeSettings = computed(() =>
activeTarget.value === 'selection' ? selectionSettings.value : pageSettings.value
)
function addSelectionSetting() {
const last = selectionSettings.value[selectionSettings.value.length - 1]
const nextScale = SCALES.find((s) => s > (last?.scale ?? 1)) ?? 2
settings.value.push({ scale: nextScale, format: last?.format ?? 'png' })
selectionSettings.value.push({ scale: nextScale, format: last?.format ?? 'png' })
}
function removeSetting(index: number) {
settings.value.splice(index, 1)
function addPageSetting() {
const last = pageSettings.value[pageSettings.value.length - 1]
const nextScale = SCALES.find((s) => s > (last?.scale ?? 1)) ?? 2
pageSettings.value.push({ scale: nextScale, format: last?.format ?? 'png' })
}
function updateScale(index: number, scale: number) {
settings.value[index] = { ...settings.value[index], scale }
function removeSelectionSetting(index: number) {
selectionSettings.value.splice(index, 1)
}
function updateFormat(index: number, format: ExportFormatId) {
settings.value[index] = { ...settings.value[index], format }
function removePageSetting(index: number) {
pageSettings.value.splice(index, 1)
}
function updateSelectionScale(index: number, scale: number) {
selectionSettings.value[index] = { ...selectionSettings.value[index], scale }
}
function updatePageScale(index: number, scale: number) {
pageSettings.value[index] = { ...pageSettings.value[index], scale }
}
function updateSelectionFormat(index: number, format: ExportFormatId) {
selectionSettings.value[index] = { ...selectionSettings.value[index], format }
}
function updatePageFormat(index: number, format: ExportFormatId) {
pageSettings.value[index] = { ...pageSettings.value[index], format }
}
return {
editor,
settings,
selectedIds,
nodeName,
scales: SCALES,
formats: FORMATS,
addSetting,
removeSetting,
updateScale,
updateFormat,
formatSupportsScale
formatSupportsScale,
hasSelection,
activeTarget,
activeName,
activeSettings,
selectedNodeName,
currentPageName,
selectionSettings,
pageSettings,
addSelectionSetting,
addPageSetting,
removeSelectionSetting,
removePageSetting,
updateSelectionScale,
updatePageScale,
updateSelectionFormat,
updatePageFormat
}
}

View file

@ -105,6 +105,7 @@ const { panels } = useI18n()
>
<PageSection />
<VariablesSection @open-dialog="variablesOpen = true" />
<ExportSection />
</div>
<VariablesDialog v-model:open="variablesOpen" />

View file

@ -12,12 +12,17 @@ import type { ExportFormatId } from '@open-pencil/vue/controls/useExport'
const editorStore = useEditorStore()
const { panels } = useI18n()
const {
settings,
nodeName,
addSetting,
removeSetting,
updateScale,
updateFormat,
activeTarget,
activeName,
activeSettings,
addSelectionSetting,
addPageSetting,
removeSelectionSetting,
removePageSetting,
updateSelectionScale,
updatePageScale,
updateSelectionFormat,
updatePageFormat,
formatSupportsScale
} = useExport()
@ -36,10 +41,41 @@ const exporting = ref(false)
const PREVIEW_WIDTH = 480
async function doExport(exportSettings: Array<{ scale: number; format: ExportFormatId }>) {
function addSetting() {
if (activeTarget.value === 'selection') addSelectionSetting()
else addPageSetting()
}
function removeSetting(index: number) {
if (activeTarget.value === 'selection') removeSelectionSetting(index)
else removePageSetting(index)
}
function updateScale(index: number, scale: number) {
if (activeTarget.value === 'selection') updateSelectionScale(index, scale)
else updatePageScale(index, scale)
}
function updateFormat(index: number, format: ExportFormatId) {
if (activeTarget.value === 'selection') updateSelectionFormat(index, format)
else updatePageFormat(index, format)
}
async function doExport() {
exporting.value = true
try {
for (const s of exportSettings) await editorStore.exportSelection(s.scale, s.format)
if (activeTarget.value === 'selection') {
for (const s of activeSettings.value) await editorStore.exportSelection(s.scale, s.format)
return
}
for (const s of activeSettings.value) {
await editorStore.exportTarget(
{ scope: 'page', pageId: editorStore.state.currentPageId },
s.format,
{ scale: s.scale }
)
}
} finally {
exporting.value = false
}
@ -47,12 +83,18 @@ async function doExport(exportSettings: Array<{ scale: number; format: ExportFor
async function updatePreview() {
if (!showPreview.value) return
const ids = [...editorStore.state.selectedIds]
const ids =
activeTarget.value === 'selection'
? [...editorStore.state.selectedIds]
: editorStore.graph.getChildren(editorStore.state.currentPageId).map((n) => n.id)
if (ids.length === 0) {
if (previewUrl.value) URL.revokeObjectURL(previewUrl.value)
previewUrl.value = null
return
}
let maxW = 0
for (const id of ids) {
const node = editorStore.getNode(id)
@ -68,7 +110,12 @@ async function updatePreview() {
}
const previewKey = computed(
() => `${editorStore.state.sceneVersion}:${[...editorStore.state.selectedIds].sort().join(',')}`
() =>
`${activeTarget.value}:${editorStore.state.sceneVersion}:${editorStore.state.currentPageId}:${[
...editorStore.state.selectedIds
]
.sort()
.join(',')}`
)
watch(() => showPreview.value, updatePreview, { flush: 'post' })
@ -87,8 +134,8 @@ onScopeDispose(() => {
</div>
<div
v-for="(setting, i) in settings"
:key="i"
v-for="(setting, i) in activeSettings"
:key="`${activeTarget}:${i}`"
data-test-id="export-item"
:data-test-index="i"
class="flex items-center gap-1.5 py-0.5"
@ -108,17 +155,17 @@ onScopeDispose(() => {
</div>
<button
v-if="settings.length > 0"
v-if="activeSettings.length > 0"
data-test-id="export-button"
class="mt-1.5 w-full cursor-pointer truncate rounded bg-blue-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-blue-700 disabled:cursor-default disabled:opacity-50"
:disabled="exporting"
@click="doExport(settings)"
@click="doExport"
>
Export {{ nodeName }}
Export {{ activeName }}
</button>
<button
v-if="settings.length > 0"
v-if="activeSettings.length > 0"
data-test-id="export-preview-toggle"
class="mt-1 flex w-full cursor-pointer items-center gap-1 rounded border-none bg-transparent px-0 py-1 text-[11px] text-muted hover:text-surface"
@click="showPreview = !showPreview"