From cf416e6ceb52eb02fcd40e6ba79e2e2d76a07ed1 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Sat, 25 Apr 2026 12:26:39 +0300 Subject: [PATCH] fix(io): parse fig files off the main thread --- packages/core/src/io/formats/fig/read.ts | 20 +++++----- packages/core/src/kiwi/fig-parse-worker.ts | 9 +++-- packages/core/src/kiwi/graph-transfer.ts | 43 ++++++++++++++++++++++ src/stores/tabs.ts | 38 ++++++++++--------- 4 files changed, 80 insertions(+), 30 deletions(-) create mode 100644 packages/core/src/kiwi/graph-transfer.ts diff --git a/packages/core/src/io/formats/fig/read.ts b/packages/core/src/io/formats/fig/read.ts index 2ede3a32a..2a0ed82f5 100644 --- a/packages/core/src/io/formats/fig/read.ts +++ b/packages/core/src/io/formats/fig/read.ts @@ -1,8 +1,9 @@ import { IS_BROWSER } from '../../../constants' import { importNodeChanges } from '../../../kiwi/fig-import' import { parseFigBuffer } from '../../../kiwi/fig-parse-core' +import { deserializeSceneGraph } from '../../../kiwi/graph-transfer' -import type { FigParseResult } from '../../../kiwi/fig-parse-core' +import type { SerializedSceneGraph } from '../../../kiwi/graph-transfer' import type { SceneGraph } from '../../../scene-graph' function parseFigFileSync(buffer: ArrayBuffer): SceneGraph { @@ -12,23 +13,24 @@ function parseFigFileSync(buffer: ArrayBuffer): SceneGraph { return graph } +interface WorkerParseResult { + graph?: SerializedSceneGraph + error?: string +} + function parseViaWorker(buffer: ArrayBuffer): Promise { return new Promise((resolve, reject) => { const worker = new Worker(new URL('../../../kiwi/fig-parse-worker.ts', import.meta.url), { type: 'module' }) - worker.onmessage = (e: MessageEvent) => { + worker.onmessage = (e: MessageEvent) => { worker.terminate() - if (e.data.error) { - reject(new Error(e.data.error)) + if (e.data.error || !e.data.graph) { + reject(new Error(e.data.error ?? 'Worker failed to parse .fig file')) return } - const { nodeChanges, blobs, images: imageEntries, figKiwiVersion } = e.data - const images = new Map(imageEntries) - const graph = importNodeChanges(nodeChanges, blobs, images) - graph.figKiwiVersion = figKiwiVersion - resolve(graph) + resolve(deserializeSceneGraph(e.data.graph)) } worker.onerror = (err) => { diff --git a/packages/core/src/kiwi/fig-parse-worker.ts b/packages/core/src/kiwi/fig-parse-worker.ts index 5cff4cfd4..c2662b5ae 100644 --- a/packages/core/src/kiwi/fig-parse-worker.ts +++ b/packages/core/src/kiwi/fig-parse-worker.ts @@ -1,10 +1,13 @@ +import { importNodeChanges } from './fig-import' import { parseFigBuffer } from './fig-parse-core' - -export type { FigParseResult } from './fig-parse-core' +import { serializeSceneGraph } from './graph-transfer' self.onmessage = (e: MessageEvent) => { try { - self.postMessage(parseFigBuffer(e.data)) + const { nodeChanges, blobs, images, figKiwiVersion } = parseFigBuffer(e.data) + const graph = importNodeChanges(nodeChanges, blobs, new Map(images)) + graph.figKiwiVersion = figKiwiVersion + self.postMessage({ graph: serializeSceneGraph(graph) }) } catch (err) { self.postMessage({ error: err instanceof Error ? err.message : String(err) }) } diff --git a/packages/core/src/kiwi/graph-transfer.ts b/packages/core/src/kiwi/graph-transfer.ts new file mode 100644 index 000000000..457bd4cba --- /dev/null +++ b/packages/core/src/kiwi/graph-transfer.ts @@ -0,0 +1,43 @@ +import { SceneGraph } from '../scene-graph' + +import type { SceneNode, Variable, VariableCollection, DocumentColorSpace } from '../scene-graph' + +export interface SerializedSceneGraph { + rootId: string + nodes: Array<[string, SceneNode]> + images: Array<[string, Uint8Array]> + variables: Array<[string, Variable]> + variableCollections: Array<[string, VariableCollection]> + activeMode: Array<[string, string]> + instanceIndex: Array<[string, string[]]> + figKiwiVersion: number | null + documentColorSpace: DocumentColorSpace +} + +export function serializeSceneGraph(graph: SceneGraph): SerializedSceneGraph { + return { + rootId: graph.rootId, + nodes: [...graph.nodes], + images: [...graph.images], + variables: [...graph.variables], + variableCollections: [...graph.variableCollections], + activeMode: [...graph.activeMode], + instanceIndex: [...graph.instanceIndex].map(([id, nodeIds]) => [id, [...nodeIds]]), + figKiwiVersion: graph.figKiwiVersion, + documentColorSpace: graph.documentColorSpace + } +} + +export function deserializeSceneGraph(data: SerializedSceneGraph): SceneGraph { + const graph = new SceneGraph() + graph.rootId = data.rootId + graph.nodes = new Map(data.nodes) + graph.images = new Map(data.images) + graph.variables = new Map(data.variables) + graph.variableCollections = new Map(data.variableCollections) + graph.activeMode = new Map(data.activeMode) + graph.instanceIndex = new Map(data.instanceIndex.map(([id, nodeIds]) => [id, new Set(nodeIds)])) + graph.figKiwiVersion = data.figKiwiVersion + graph.documentColorSpace = data.documentColorSpace + return graph +} diff --git a/src/stores/tabs.ts b/src/stores/tabs.ts index fc5c83311..83590496a 100644 --- a/src/stores/tabs.ts +++ b/src/stores/tabs.ts @@ -82,6 +82,10 @@ export function closeTab(tabId: string) { closingTab.store.dispose() } +function yieldToUI(): Promise { + return new Promise((resolve) => requestAnimationFrame(() => resolve())) +} + export async function openFileInNewTab( file: File, handle?: FileSystemFileHandle, @@ -90,31 +94,29 @@ export async function openFileInNewTab( const current = activeTab.value const isUntouched = current?.store.state.documentName === 'Untitled' && !current.store.undo.canUndo - const bytes = new Uint8Array(await file.arrayBuffer()) - const { graph: imported, sourceFormat } = await io.readDocument({ - name: file.name, - mimeType: file.type || undefined, - data: bytes - }) + const store = isUntouched ? current.store : createTab().store const documentName = file.name.replace(/\.[^.]+$/i, '') - if (isUntouched) { - current.store.replaceGraph(imported) - current.store.undo.clear() - current.store.state.documentName = documentName - current.store.setDocumentSource(file.name, sourceFormat, handle, path) - current.store.state.selectedIds = new Set() - const pageId = current.store.graph.getPages()[0]?.id ?? current.store.graph.rootId - await current.store.switchPage(pageId) - } else { - const store = createEditorStore(imported) - createTab(store) + store.state.documentName = documentName + store.state.loading = true + await yieldToUI() + + try { + const bytes = new Uint8Array(await file.arrayBuffer()) + const { graph: imported, sourceFormat } = await io.readDocument({ + name: file.name, + mimeType: file.type || undefined, + data: bytes + }) + + store.replaceGraph(imported) store.undo.clear() - store.state.documentName = documentName store.setDocumentSource(file.name, sourceFormat, handle, path) store.state.selectedIds = new Set() const pageId = store.graph.getPages()[0]?.id ?? store.graph.rootId await store.switchPage(pageId) + } finally { + store.state.loading = false } }