fix(io): parse fig files off the main thread

This commit is contained in:
Danila Poyarkov 2026-04-25 12:26:39 +03:00
parent 317b3469df
commit cf416e6ceb
4 changed files with 80 additions and 30 deletions

View file

@ -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<SceneGraph> {
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<FigParseResult & { error?: string }>) => {
worker.onmessage = (e: MessageEvent<WorkerParseResult>) => {
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<string, Uint8Array>(imageEntries)
const graph = importNodeChanges(nodeChanges, blobs, images)
graph.figKiwiVersion = figKiwiVersion
resolve(graph)
resolve(deserializeSceneGraph(e.data.graph))
}
worker.onerror = (err) => {

View file

@ -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<ArrayBuffer>) => {
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) })
}

View file

@ -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
}

View file

@ -82,6 +82,10 @@ export function closeTab(tabId: string) {
closingTab.store.dispose()
}
function yieldToUI(): Promise<void> {
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
}
}