Fix export panel target handling
This commit is contained in:
parent
d25c48a33d
commit
581abf3b93
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -105,6 +105,7 @@ const { panels } = useI18n()
|
|||
>
|
||||
<PageSection />
|
||||
<VariablesSection @open-dialog="variablesOpen = true" />
|
||||
<ExportSection />
|
||||
</div>
|
||||
|
||||
<VariablesDialog v-model:open="variablesOpen" />
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue