diff --git a/AGENTS.md b/AGENTS.md index f1d6f32f6..46aaab7ce 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,9 +9,9 @@ Vue 3 + CanvasKit (Skia WASM) + Yoga WASM design editor. Tauri v2 desktop, also Bun workspace packages: - `packages/scene-graph` — `@open-pencil/scene-graph`: SceneGraph, node types, copy/snap/undo helpers, variables, instances, hit testing. Framework-agnostic. -- `packages/pen` — `@open-pencil/pen`: Pen/vector editing helpers shared by core/editor surfaces. +- `packages/pen` — `@open-pencil/pen`: Pencil.dev `.pen` document model, parser, and SceneGraph import adapter. - `packages/kiwi` — `@open-pencil/kiwi`: pure Kiwi schema/runtime/protocol package. Owns low-level Figma Kiwi codec/container/parse helpers and stays SceneGraph-agnostic. -- `packages/fig` — `@open-pencil/fig`: publishable `.fig` package shell and low-level smoke/test boundary. Production SceneGraph `.fig` policy still lives mostly in core while this package grows. +- `packages/fig` — `@open-pencil/fig`: `.fig` archive/parser package and staged home for Figma-specific SceneGraph conversion policy. Core keeps format-neutral IO registration and runtime rendering integration. - `packages/core` — `@open-pencil/core`: renderer, layout, editor core, Figma API, tools, clipboard, vector conversion, and app/CLI-facing document I/O. Depends on scene-graph/pen/kiwi but keeps browser DOM out of core. - `packages/dom-css` — `@open-pencil/dom-css`: DOM/CSS projection layer for HTML/CSS/JSX/Tailwind compatibility. Owns DesignDOM types and browser/headless CSS runtime adapters; keeps DOM/CSS parser dependencies out of core. - `packages/vue` — `@open-pencil/vue`: headless Vue 3 SDK (Reka UI-style) for building custom OpenPencil-powered editor shells and embedded editing surfaces. Renderless components and composables. The app is one consumer of the SDK. @@ -295,7 +295,7 @@ Self-review checklist: ## File format -- `.fig` files use Figma's Kiwi schema and `NodeChange[]` records. Low-level schema/runtime/codec/container/parse helpers live in `packages/kiwi/src/fig/**` and `packages/kiwi/src/schema-runtime/**`. +- `.fig` files use Figma's Kiwi schema and `NodeChange[]` records. Low-level schema/runtime/codec/container helpers live in `packages/kiwi/src/fig/**` and `packages/kiwi/src/schema-runtime/**`; complete `.fig` archive parsing lives in `packages/fig`. - Core still owns SceneGraph `.fig` policy: import/export orchestration in `packages/core/src/io/formats/fig/**`, SceneGraph ⇄ NodeChange conversion in `packages/core/src/kiwi/fig/node-change/**`, and component/instance override interpretation in `packages/core/src/kiwi/fig/instance-overrides/**`. - `packages/fig` is the publishable boundary for future `.fig` policy extraction; do not move behavior there without package-local tests and dist smoke. - Vector data uses reverse-engineered `vectorNetworkBlob` binary format — encoder/decoder in `packages/core/src/vector/` and scene-graph vector-network types in `@open-pencil/scene-graph`. diff --git a/bun.lock b/bun.lock index e84aa2a06..3dec567cc 100644 --- a/bun.lock +++ b/bun.lock @@ -136,6 +136,7 @@ "dependencies": { "@chenglou/pretext": "^0.0.7", "@iconify/utils": "^3.1.0", + "@open-pencil/fig": "workspace:*", "@open-pencil/kiwi": "workspace:*", "@open-pencil/pen": "workspace:*", "@open-pencil/scene-graph": "workspace:*", @@ -206,6 +207,9 @@ "packages/fig": { "name": "@open-pencil/fig", "version": "0.13.2", + "dependencies": { + "fflate": "^0.8.2", + }, "devDependencies": { "@types/bun": "^1.3.14", "tsdown": "^0.22.3", @@ -213,6 +217,7 @@ }, "peerDependencies": { "@open-pencil/kiwi": "workspace:*", + "@open-pencil/scene-graph": "workspace:*", }, }, "packages/kiwi": { @@ -3261,6 +3266,8 @@ "@open-pencil/docs/@tailwindcss/vite": ["@tailwindcss/vite@4.3.2", "", { "dependencies": { "@tailwindcss/node": "4.3.2", "@tailwindcss/oxide": "4.3.2", "tailwindcss": "4.3.2" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-eHpMeX4JXfVNJDEcsouTeCBubJBTcTLigeaw/NTUW6PB5ATKKXdyonnXgTBX2VuRbjz1hjfz6C5XAhr52ImQXA=="], + "@open-pencil/fig/fflate": ["fflate@0.8.3", "", {}, "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA=="], + "@open-pencil/kiwi/fflate": ["fflate@0.8.3", "", {}, "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA=="], "@oxc-resolver/binding-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q=="], diff --git a/packages/core/package.json b/packages/core/package.json index b1709056f..a08b540d2 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -167,6 +167,7 @@ "dependencies": { "@chenglou/pretext": "^0.0.7", "@iconify/utils": "^3.1.0", + "@open-pencil/fig": "workspace:*", "@open-pencil/kiwi": "workspace:*", "@open-pencil/pen": "workspace:*", "@open-pencil/scene-graph": "workspace:*", diff --git a/packages/core/src/io/formats/fig/compress.ts b/packages/core/src/io/formats/fig/compress.ts index 99d365fb0..b0824a8b2 100644 --- a/packages/core/src/io/formats/fig/compress.ts +++ b/packages/core/src/io/formats/fig/compress.ts @@ -1,23 +1 @@ -import { zipSync, type Zippable } from 'fflate' - -import { buildFigKiwi } from '#core/kiwi/fig/node-change/serialize' - -export function compressFigDataSync( - schemaDeflated: Uint8Array, - kiwiData: Uint8Array, - thumbnailPng: Uint8Array, - metaJson: string, - imageEntries: Array<{ name: string; data: Uint8Array }>, - figKiwiVersion?: number -): Uint8Array { - const canvasData = buildFigKiwi(schemaDeflated, kiwiData, figKiwiVersion) - const zipEntries: Zippable = { - 'canvas.fig': [canvasData, { level: 0 }], - 'thumbnail.png': [thumbnailPng, { level: 0 }], - 'meta.json': new TextEncoder().encode(metaJson) - } - for (const entry of imageEntries) { - zipEntries[entry.name] = [entry.data, { level: 0 }] - } - return zipSync(zipEntries) -} +export { compressFigDataSync } from '@open-pencil/fig' diff --git a/packages/core/src/io/formats/fig/read.ts b/packages/core/src/io/formats/fig/read.ts index 1eba5fb3d..dceef8e57 100644 --- a/packages/core/src/io/formats/fig/read.ts +++ b/packages/core/src/io/formats/fig/read.ts @@ -1,4 +1,4 @@ -import { parseFigBuffer } from '@open-pencil/kiwi/fig/parse' +import { parseFigBuffer } from '@open-pencil/fig' import type { SceneGraph } from '@open-pencil/scene-graph' import { IS_BROWSER } from '#core/constants' diff --git a/packages/core/src/kiwi/fig/import.ts b/packages/core/src/kiwi/fig/import.ts index aa8a6c8c1..6ce5f2af6 100644 --- a/packages/core/src/kiwi/fig/import.ts +++ b/packages/core/src/kiwi/fig/import.ts @@ -1,5 +1,6 @@ import { isNotNil } from 'es-toolkit/predicate' +import { applyStyleRefsToFields } from '@open-pencil/fig/node-change' import type { NodeChange, VariableDataValuesEntry, Color, GUID } from '@open-pencil/kiwi/fig/codec' import { SceneGraph } from '@open-pencil/scene-graph' import type { VariableType, VariableValue } from '@open-pencil/scene-graph' @@ -16,7 +17,6 @@ import { setVariableColorResolver, VARIABLE_BINDING_FIELDS_INVERSE } from '#core/kiwi/fig/node-change/convert' -import { applyStyleRefsToFields } from '#core/kiwi/fig/node-change/style-refs' type AssetRef = { key: string; version?: string } type AliasRef = { guid?: GUID; assetRef?: AssetRef } diff --git a/packages/core/src/kiwi/fig/instance-overrides/derived-symbol-data/layout.ts b/packages/core/src/kiwi/fig/instance-overrides/derived-symbol-data/layout.ts index f991eabb6..09f26fbde 100644 --- a/packages/core/src/kiwi/fig/instance-overrides/derived-symbol-data/layout.ts +++ b/packages/core/src/kiwi/fig/instance-overrides/derived-symbol-data/layout.ts @@ -1,3 +1,4 @@ +import { convertFigmaDerivedTextGlyphs } from '@open-pencil/fig/node-change' import type { SceneNode } from '@open-pencil/scene-graph' import type { @@ -5,7 +6,6 @@ import type { OverrideContext } from '#core/kiwi/fig/instance-overrides/types' import { convertLetterSpacing, convertLineHeight } from '#core/kiwi/fig/node-change/convert' -import { convertFigmaDerivedTextGlyphs } from '#core/kiwi/fig/node-change/derived-text-glyphs' import { resolveDsdGeometry } from './geometry' diff --git a/packages/core/src/kiwi/fig/instance-overrides/symbol/patches.ts b/packages/core/src/kiwi/fig/instance-overrides/symbol/patches.ts index 922a7e902..aa4cd4cce 100644 --- a/packages/core/src/kiwi/fig/instance-overrides/symbol/patches.ts +++ b/packages/core/src/kiwi/fig/instance-overrides/symbol/patches.ts @@ -1,3 +1,4 @@ +import { applyStyleRefsToFields } from '@open-pencil/fig/node-change' import type { GUID } from '@open-pencil/kiwi/fig/codec' import type { OverridePatch } from '#core/kiwi/fig/instance-overrides/patches' @@ -7,7 +8,6 @@ import type { SymbolOverrideFields } from '#core/kiwi/fig/instance-overrides/types' import { guidToString, VARIABLE_BINDING_FIELDS_INVERSE } from '#core/kiwi/fig/node-change/convert' -import { applyStyleRefsToFields } from '#core/kiwi/fig/node-change/style-refs' import { convertOverrideToProps } from './props' diff --git a/packages/core/src/kiwi/fig/node-change/convert.ts b/packages/core/src/kiwi/fig/node-change/convert.ts index a3c893432..848722d6b 100644 --- a/packages/core/src/kiwi/fig/node-change/convert.ts +++ b/packages/core/src/kiwi/fig/node-change/convert.ts @@ -5,16 +5,17 @@ import { parseVariantName } from '@open-pencil/scene-graph/variant-name' import { DEFAULT_FONT_FAMILY, DEFAULT_STROKE_MITER_LIMIT } from '#core/constants' import { styleToWeight } from '#core/text/fonts' -import { convertEffects, convertFills, convertStrokes } from './paint' import { importStyleRuns } from './style-runs' export { importStyleRuns } from './style-runs' -import { convertFigmaDerivedTextGlyphs } from './derived-text-glyphs' -import { convertFontFeatures } from './font/features' -import { convertFontVariations } from './font/variations' -import { convertLetterSpacing, convertLineHeight, mapTextDecoration } from './text-values' -export { convertEffects, convertFills, convertStrokes, setVariableColorResolver } from './paint' -export { convertLetterSpacing, convertLineHeight, mapTextDecoration } from './text-values' import { + convertEffects, + convertFigmaDerivedTextGlyphs, + convertFills, + convertFontFeatures, + convertFontVariations, + convertLetterSpacing, + convertLineHeight, + convertStrokes, extractBoundVariables, extractExportSettings, extractPluginData, @@ -22,8 +23,19 @@ import { getOpenPencilPluginValue, LAYOUT_DIRECTION_PLUGIN_KEY, NODE_TYPE_PLUGIN_KEY, - TEXT_DIRECTION_PLUGIN_KEY -} from './plugin-data' + TEXT_DIRECTION_PLUGIN_KEY, + mapTextDecoration +} from '@open-pencil/fig/node-change' + +export { + convertEffects, + convertFills, + convertLetterSpacing, + convertLineHeight, + convertStrokes, + mapTextDecoration, + setVariableColorResolver +} from '@open-pencil/fig/node-change' import { resolveGeometryPaths, resolveVectorNetwork } from './vector-geometry' export { resolveGeometryPaths } from './vector-geometry' diff --git a/packages/core/src/kiwi/fig/node-change/export-node.ts b/packages/core/src/kiwi/fig/node-change/export-node.ts index 6d4d145b6..ab568f4b0 100644 --- a/packages/core/src/kiwi/fig/node-change/export-node.ts +++ b/packages/core/src/kiwi/fig/node-change/export-node.ts @@ -1,3 +1,10 @@ +import { + applyExportSettingsPluginData, + mergePluginData, + NODE_TYPE_PLUGIN_KEY, + serializePluginRelaunchData, + upsertPluginData +} from '@open-pencil/fig/node-change' import type { NodeChange, Paint } from '@open-pencil/kiwi/fig/codec' import { stringToGuid } from '@open-pencil/kiwi/fig/guid' import type { @@ -12,14 +19,6 @@ import type { Color, GUID, Matrix, Vector } from '@open-pencil/scene-graph/primi import { bytesToHex } from '#core/bytes/hex' import { DEFAULT_STROKE_MITER_LIMIT } from '#core/constants' -import { - applyExportSettingsPluginData, - mergePluginData, - NODE_TYPE_PLUGIN_KEY, - serializePluginRelaunchData, - upsertPluginData -} from './plugin-data' - export type KiwiNodeChange = NodeChange & Record type KiwiBooleanOperation = NonNullable diff --git a/packages/core/src/kiwi/fig/node-change/font/index.ts b/packages/core/src/kiwi/fig/node-change/font/index.ts index d51fc7a70..b83ab250f 100644 --- a/packages/core/src/kiwi/fig/node-change/font/index.ts +++ b/packages/core/src/kiwi/fig/node-change/font/index.ts @@ -1,3 +1,8 @@ -export { convertFontFeatures, applyFontFeaturesToKiwi } from './features' -export { figmaAxisTagToString, stringToFigmaAxisTag, convertFontVariations } from './variations' +export { + applyFontFeaturesToKiwi, + convertFontFeatures, + convertFontVariations, + figmaAxisTagToString, + stringToFigmaAxisTag +} from '@open-pencil/fig/node-change' export { buildFontDigestMap } from './digests' diff --git a/packages/core/src/kiwi/fig/node-change/serialize.ts b/packages/core/src/kiwi/fig/node-change/serialize.ts index 2e5d03ee4..cd64fbffe 100644 --- a/packages/core/src/kiwi/fig/node-change/serialize.ts +++ b/packages/core/src/kiwi/fig/node-change/serialize.ts @@ -13,6 +13,13 @@ export { } from '@open-pencil/kiwi/fig/container' export { buildFontDigestMap } from './font/digests' +import { + applyFontFeaturesToKiwi, + BOUND_VARIABLES_PLUGIN_KEY, + LAYOUT_DIRECTION_PLUGIN_KEY, + TEXT_DIRECTION_PLUGIN_KEY, + upsertPluginData +} from '@open-pencil/fig/node-change' import type { NodeChange, Paint, VariableConsumptionEntry } from '@open-pencil/kiwi/fig/codec' import type { SceneGraph, SceneNode } from '@open-pencil/scene-graph' import type { Color, GUID, JsonObject, Matrix } from '@open-pencil/scene-graph/primitives' @@ -23,13 +30,6 @@ import { sceneNodeToKiwiWithContext, type KiwiNodeChange } from './export-node' -import { applyFontFeaturesToKiwi } from './font/features' -import { - BOUND_VARIABLES_PLUGIN_KEY, - LAYOUT_DIRECTION_PLUGIN_KEY, - TEXT_DIRECTION_PLUGIN_KEY, - upsertPluginData -} from './plugin-data' import { exportTextData, fontVariationToKiwi } from './text-data-export' export function mapToFigmaType(type: SceneNode['type']): string { diff --git a/packages/core/src/kiwi/fig/node-change/style-runs.ts b/packages/core/src/kiwi/fig/node-change/style-runs.ts index 8aa27c649..fea68afcb 100644 --- a/packages/core/src/kiwi/fig/node-change/style-runs.ts +++ b/packages/core/src/kiwi/fig/node-change/style-runs.ts @@ -1,13 +1,16 @@ +import { + convertFontFeatures, + convertFontVariations, + convertFills, + convertLetterSpacing, + convertLineHeight, + mapTextDecoration +} from '@open-pencil/fig/node-change' import type { NodeChange } from '@open-pencil/kiwi/fig/codec' import type { CharacterStyleOverride, StyleRun } from '@open-pencil/scene-graph' import { styleToWeight } from '#core/text/fonts' -import { convertFontFeatures } from './font/features' -import { convertFontVariations } from './font/variations' -import { convertFills } from './paint' -import { convertLetterSpacing, convertLineHeight, mapTextDecoration } from './text-values' - function applyTextDecorationOverride(style: CharacterStyleOverride, override: NodeChange): void { const deco = override.textDecoration if (deco) style.textDecoration = mapTextDecoration(deco) diff --git a/packages/core/src/kiwi/fig/node-change/text-data-export.ts b/packages/core/src/kiwi/fig/node-change/text-data-export.ts index 820921cca..1393ad3ea 100644 --- a/packages/core/src/kiwi/fig/node-change/text-data-export.ts +++ b/packages/core/src/kiwi/fig/node-change/text-data-export.ts @@ -1,11 +1,9 @@ +import { applyFontFeaturesToKiwi, stringToFigmaAxisTag } from '@open-pencil/fig/node-change' import type { NodeChange, Paint } from '@open-pencil/kiwi/fig/codec' import type { CharacterStyleOverride, SceneNode } from '@open-pencil/scene-graph' import { normalizeFontFamily, weightToFigmaStyle } from '#core/text/fonts' -import { applyFontFeaturesToKiwi } from './font/features' -import { stringToFigmaAxisTag } from './font/variations' - export function fontVariationToKiwi(variation: SceneNode['fontVariations'][number]) { const axisTag = stringToFigmaAxisTag(variation.axis) return axisTag === undefined diff --git a/packages/core/src/kiwi/fig/parse/worker.ts b/packages/core/src/kiwi/fig/parse/worker.ts index abe95a16e..96691d427 100644 --- a/packages/core/src/kiwi/fig/parse/worker.ts +++ b/packages/core/src/kiwi/fig/parse/worker.ts @@ -1,4 +1,4 @@ -import { parseFigBuffer } from '@open-pencil/kiwi/fig/parse' +import { parseFigBuffer } from '@open-pencil/fig' import { importNodeChanges } from '#core/kiwi/fig/import' import { diff --git a/packages/fig/README.md b/packages/fig/README.md index 8abd34667..a9165c82f 100644 --- a/packages/fig/README.md +++ b/packages/fig/README.md @@ -1,28 +1,36 @@ # @open-pencil/fig -`.fig` document policy package for OpenPencil. +`.fig` file-format package for OpenPencil. -This package currently exposes low-level `fig-kiwi` container read/write helpers and remains the staging area for the next package-split stage. Use `@open-pencil/core` for production SceneGraph `.fig` read/write APIs until higher-level behavior moves here. +The package owns the outer `.fig` archive boundary and is the staged home for Figma-specific +SceneGraph conversion policy. Production SceneGraph read/write remains available through +`@open-pencil/core/io` while conversion modules move behind this package's public API. Current ownership: -- `readFigContainer()` / `writeFigContainer()` wrappers over `@open-pencil/kiwi` container helpers -- `.fig` document source typing +- Complete `.fig` archive parsing through `parseFigBuffer()` +- `.fig` archive assembly through `writeFigArchive()` +- Canvas payload and image resource handling +- `readFigContainer()` / `writeFigContainer()` helpers for raw `fig-kiwi` payloads +- `.fig` source and archive result types +- Dependency-free NodeChange policy helpers for styles, plugin metadata, text values, and font axes/features through `@open-pencil/fig/node-change` Planned ownership: -- `.fig` read/write orchestration -- SceneGraph ⇄ Figma NodeChange conversion policy -- raw Figma metadata preservation and invalidation -- component/instance interpretation -- oracle-backed `.fig` fixtures and package-local tests +- SceneGraph ⇄ Figma `NodeChange` conversion +- Raw Figma metadata precedence and invalidation policy +- Component and instance interpretation +- Oracle-backed `.fig` fixtures and package-local tests Non-goals: -- low-level Kiwi schema/runtime/codec internals — use `@open-pencil/kiwi` -- editor actions, renderer behavior, Vue/app UI, CLI formatting, or MCP transport +- Generic Kiwi schema/runtime internals — use `@open-pencil/kiwi` +- Format-neutral IO registration, export targeting, CanvasKit thumbnails, or browser workers — use + `@open-pencil/core/io` +- Editor actions, renderer behavior, Vue/app UI, CLI formatting, or MCP transport -See `packages/docs/development/fig-package-plan.md` for the staged migration plan. +This follows the existing `@open-pencil/pen` pattern: a format package owns its source model/parser +and SceneGraph policy, while core registers it in the shared IO system. ## Checks diff --git a/packages/fig/package.json b/packages/fig/package.json index 4a25c7921..864b8ca4e 100644 --- a/packages/fig/package.json +++ b/packages/fig/package.json @@ -9,6 +9,12 @@ "bun": "./src/index.ts", "import": "./dist/index.js", "default": "./dist/index.js" + }, + "./node-change": { + "types": "./dist/node-change.d.ts", + "bun": "./src/node-change/index.ts", + "import": "./dist/node-change.js", + "default": "./dist/node-change.js" } }, "main": "./dist/index.js", @@ -36,7 +42,11 @@ "provenance": true }, "peerDependencies": { - "@open-pencil/kiwi": "workspace:*" + "@open-pencil/kiwi": "workspace:*", + "@open-pencil/scene-graph": "workspace:*" + }, + "dependencies": { + "fflate": "^0.8.2" }, "devDependencies": { "@types/bun": "^1.3.14", diff --git a/packages/fig/scripts/smoke-dist.ts b/packages/fig/scripts/smoke-dist.ts index 92e432415..eb42004f6 100644 --- a/packages/fig/scripts/smoke-dist.ts +++ b/packages/fig/scripts/smoke-dist.ts @@ -1,9 +1,15 @@ export {} const mod = await import('../dist/index.js') +const nodeChange = await import('../dist/node-change.js') -if (mod.FIG_PACKAGE_STATUS !== 'container-api') { - throw new Error('Expected @open-pencil/fig container API export') +if ( + mod.FIG_PACKAGE_STATUS !== 'archive-api' || + typeof mod.parseFigBuffer !== 'function' || + typeof mod.writeFigArchive !== 'function' || + typeof nodeChange.convertLineHeight !== 'function' +) { + throw new Error('Expected @open-pencil/fig archive API exports') } const bytes = mod.writeFigContainer({ diff --git a/packages/fig/src/archive.ts b/packages/fig/src/archive.ts new file mode 100644 index 000000000..7b850c6e9 --- /dev/null +++ b/packages/fig/src/archive.ts @@ -0,0 +1,99 @@ +import { unzipSync, zipSync, type Zippable } from 'fflate' + +import type { NodeChange } from '@open-pencil/kiwi/fig/codec' +import { buildFigKiwi } from '@open-pencil/kiwi/fig/container' +import { decodeFigKiwiCanvas } from '@open-pencil/kiwi/fig/parse' + +export interface FigImageEntry { + name: string + data: Uint8Array +} + +export interface WriteFigArchiveInput { + schemaDeflated: Uint8Array + kiwiData: Uint8Array + thumbnailPng: Uint8Array + metaJson: string + images?: FigImageEntry[] + figKiwiVersion?: number +} + +export interface FigParseResult { + nodeChanges: NodeChange[] + blobs: Uint8Array[] + images: Array<[string, Uint8Array]> + figKiwiVersion: number + /** Deflated Kiwi schema bytes from the original file, retained for round-trip fidelity. */ + figSchemaDeflated: Uint8Array +} + +function isLikelyAsset(name: string): boolean { + const lower = name.toLowerCase() + return lower.endsWith('.png') || lower.endsWith('.jpg') || lower.endsWith('.json') +} + +function findCanvasData(entries: Partial>): Uint8Array | null { + const canonical = entries['canvas.fig'] ?? entries.canvas + if (canonical) return canonical + + let largest: Uint8Array | null = null + for (const [name, data] of Object.entries(entries)) { + if (!data || isLikelyAsset(name)) continue + if (!largest || data.byteLength > largest.byteLength) largest = data + } + return largest +} + +/** Parse a complete zipped `.fig` file into its Figma protocol payload and binary resources. */ +export function parseFigBuffer(buffer: ArrayBuffer): FigParseResult { + const archive = unzipSync(new Uint8Array(buffer), { + filter: (file) => + file.name === 'canvas.fig' || + file.name === 'canvas' || + (file.name.startsWith('images/') && file.name !== 'images/') + }) + const canvasData = findCanvasData(archive) + if (!canvasData) { + throw new Error( + `No canvas data found in .fig file. Entries: ${Object.keys(archive).join(', ')}` + ) + } + + const decoded = decodeFigKiwiCanvas(canvasData) + const images = Object.entries(archive) + .filter(([name]) => name.startsWith('images/') && name !== 'images/') + .map(([name, data]) => [name.slice('images/'.length), data] as [string, Uint8Array]) + + return { ...decoded, images } +} + +/** Assemble a complete zipped `.fig` archive from an encoded Kiwi message and resources. */ +export function writeFigArchive(input: WriteFigArchiveInput): Uint8Array { + const canvasData = buildFigKiwi(input.schemaDeflated, input.kiwiData, input.figKiwiVersion) + const entries: Zippable = { + 'canvas.fig': [canvasData, { level: 0 }], + 'thumbnail.png': [input.thumbnailPng, { level: 0 }], + 'meta.json': new TextEncoder().encode(input.metaJson) + } + for (const image of input.images ?? []) entries[image.name] = [image.data, { level: 0 }] + return zipSync(entries) +} + +/** Compatibility signature used by core while archive assembly migrates to this package. */ +export function compressFigDataSync( + schemaDeflated: Uint8Array, + kiwiData: Uint8Array, + thumbnailPng: Uint8Array, + metaJson: string, + imageEntries: FigImageEntry[], + figKiwiVersion?: number +): Uint8Array { + return writeFigArchive({ + schemaDeflated, + kiwiData, + thumbnailPng, + metaJson, + images: imageEntries, + figKiwiVersion + }) +} diff --git a/packages/fig/src/index.ts b/packages/fig/src/index.ts index ce79fbd94..d12f8ba48 100644 --- a/packages/fig/src/index.ts +++ b/packages/fig/src/index.ts @@ -1,3 +1,12 @@ +export { + compressFigDataSync, + parseFigBuffer, + writeFigArchive, + type FigImageEntry, + type FigParseResult, + type WriteFigArchiveInput +} from './archive' + import { FIG_KIWI_DEFAULT_VERSION, buildFigKiwi, @@ -37,7 +46,7 @@ export interface WriteFigContainerOptions { readonly version?: number } -export const FIG_PACKAGE_STATUS = 'container-api' as const +export const FIG_PACKAGE_STATUS = 'archive-api' as const export function readFigContainer( bytes: Uint8Array, @@ -66,6 +75,6 @@ export function writeFigContainer( export function assertFigPackageReady(): void { throw new Error( - '@open-pencil/fig currently exposes low-level container APIs; use @open-pencil/core for SceneGraph .fig read/write APIs for now.' + '@open-pencil/fig currently exposes archive/container APIs; use @open-pencil/core for SceneGraph .fig read/write APIs for now.' ) } diff --git a/packages/core/src/kiwi/fig/node-change/derived-text-glyphs.ts b/packages/fig/src/node-change/derived-text-glyphs.ts similarity index 100% rename from packages/core/src/kiwi/fig/node-change/derived-text-glyphs.ts rename to packages/fig/src/node-change/derived-text-glyphs.ts diff --git a/packages/core/src/kiwi/fig/node-change/font/features.ts b/packages/fig/src/node-change/font/features.ts similarity index 100% rename from packages/core/src/kiwi/fig/node-change/font/features.ts rename to packages/fig/src/node-change/font/features.ts diff --git a/packages/core/src/kiwi/fig/node-change/font/variations.ts b/packages/fig/src/node-change/font/variations.ts similarity index 100% rename from packages/core/src/kiwi/fig/node-change/font/variations.ts rename to packages/fig/src/node-change/font/variations.ts diff --git a/packages/fig/src/node-change/index.ts b/packages/fig/src/node-change/index.ts new file mode 100644 index 000000000..a4ce75f5e --- /dev/null +++ b/packages/fig/src/node-change/index.ts @@ -0,0 +1,7 @@ +export * from './derived-text-glyphs' +export * from './font/features' +export * from './font/variations' +export * from './paint' +export * from './plugin-data' +export * from './style-refs' +export * from './text-values' diff --git a/packages/core/src/kiwi/fig/node-change/paint.ts b/packages/fig/src/node-change/paint.ts similarity index 95% rename from packages/core/src/kiwi/fig/node-change/paint.ts rename to packages/fig/src/node-change/paint.ts index e0576b9c0..a23eb4933 100644 --- a/packages/core/src/kiwi/fig/node-change/paint.ts +++ b/packages/fig/src/node-change/paint.ts @@ -11,11 +11,13 @@ import type { StrokeCap, StrokeJoin } from '@open-pencil/scene-graph' +import { BLACK } from '@open-pencil/scene-graph/constants' import type { Color, Matrix } from '@open-pencil/scene-graph/primitives' -import { normalizeColor } from '#core/color' - -const convertColor = normalizeColor +function convertColor(color?: Partial): Color { + if (!color) return { ...BLACK } + return { r: color.r ?? 0, g: color.g ?? 0, b: color.b ?? 0, a: color.a ?? 1 } +} function imageHashToString(hash: Record): string { const bytes = Object.keys(hash) diff --git a/packages/core/src/kiwi/fig/node-change/plugin-data.ts b/packages/fig/src/node-change/plugin-data.ts similarity index 100% rename from packages/core/src/kiwi/fig/node-change/plugin-data.ts rename to packages/fig/src/node-change/plugin-data.ts diff --git a/packages/core/src/kiwi/fig/node-change/style-refs.ts b/packages/fig/src/node-change/style-refs.ts similarity index 100% rename from packages/core/src/kiwi/fig/node-change/style-refs.ts rename to packages/fig/src/node-change/style-refs.ts diff --git a/packages/core/src/kiwi/fig/node-change/text-values.ts b/packages/fig/src/node-change/text-values.ts similarity index 100% rename from packages/core/src/kiwi/fig/node-change/text-values.ts rename to packages/fig/src/node-change/text-values.ts diff --git a/packages/fig/tests/index.test.ts b/packages/fig/tests/index.test.ts index 442f2be62..ad5b8e201 100644 --- a/packages/fig/tests/index.test.ts +++ b/packages/fig/tests/index.test.ts @@ -1,15 +1,30 @@ -import { describe, expect, it } from 'bun:test' +import { beforeAll, describe, expect, it } from 'bun:test' + +import { deflateSync } from 'fflate' + +import { + createNodeChangesMessage, + encodeMessage, + getSchemaBytes, + initCodec +} from '@open-pencil/kiwi/fig/codec' import { FIG_PACKAGE_STATUS, assertFigPackageReady, + parseFigBuffer, readFigContainer, + writeFigArchive, writeFigContainer } from '../src/index' describe('@open-pencil/fig package API', () => { - it('exports container API status', () => { - expect(FIG_PACKAGE_STATUS).toBe('container-api') + beforeAll(async () => { + await initCodec() + }) + + it('exports archive API status', () => { + expect(FIG_PACKAGE_STATUS).toBe('archive-api') }) it('round-trips fig-kiwi container bytes', () => { @@ -25,11 +40,35 @@ describe('@open-pencil/fig package API', () => { expect(document.source?.fileName).toBe('fixture.fig') }) + it('parses complete .fig archives and image resources', () => { + const bytes = writeFigArchive({ + schemaDeflated: deflateSync(getSchemaBytes()), + kiwiData: encodeMessage( + createNodeChangesMessage(0, 0, [ + { + guid: { sessionID: 0, localID: 0 }, + type: 'DOCUMENT', + phase: 'CREATED', + name: 'Document' + } + ]) + ), + thumbnailPng: new Uint8Array([1]), + metaJson: '{}', + images: [{ name: 'images/hash', data: new Uint8Array([9, 8, 7]) }] + }) + const parsed = parseFigBuffer(bytes.buffer as ArrayBuffer) + + expect(parsed.nodeChanges).toHaveLength(1) + expect(parsed.nodeChanges[0]?.type).toBe('DOCUMENT') + expect(parsed.images).toEqual([['hash', new Uint8Array([9, 8, 7])]]) + }) + it('rejects invalid fig-kiwi containers', () => { expect(() => readFigContainer(new Uint8Array([1, 2, 3]))).toThrow('Invalid fig-kiwi') }) it('directs consumers to core for SceneGraph read/write', () => { - expect(() => assertFigPackageReady()).toThrow('low-level container APIs') + expect(() => assertFigPackageReady()).toThrow('archive/container APIs') }) }) diff --git a/packages/fig/tests/node-change.test.ts b/packages/fig/tests/node-change.test.ts new file mode 100644 index 000000000..a754ed673 --- /dev/null +++ b/packages/fig/tests/node-change.test.ts @@ -0,0 +1,64 @@ +import { describe, expect, test } from 'bun:test' + +import { + applyStyleRefsToFields, + convertEffects, + convertFills, + convertFontFeatures, + convertLetterSpacing, + convertLineHeight, + mapTextDecoration +} from '../src/node-change' + +describe('@open-pencil/fig NodeChange policy', () => { + test('converts normalized text values', () => { + expect(convertLineHeight({ value: 120, units: 'PERCENT' }, 20)).toBe(24) + expect(convertLetterSpacing({ value: 10, units: 'PERCENT' }, 20)).toBe(2) + expect(mapTextDecoration('UNDERLINE')).toBe('UNDERLINE') + }) + + test('converts Figma OpenType feature toggles', () => { + expect( + convertFontFeatures({ + toggledOnOTFeatures: ['DLIG'], + toggledOffOTFeatures: ['LIGA'] + }) + ).toEqual([ + { tag: 'DLIG', enabled: true }, + { tag: 'LIGA', enabled: false } + ]) + }) + + test('normalizes imported paints and effects', () => { + expect(convertFills([{ type: 'SOLID' }])[0]).toMatchObject({ + color: { r: 0, g: 0, b: 0, a: 1 }, + opacity: 1, + visible: true + }) + expect(convertEffects([{ type: 'DROP_SHADOW' }])[0]).toMatchObject({ + type: 'DROP_SHADOW', + radius: 0, + visible: true + }) + }) + + test('resolves imported style references before SceneGraph conversion', () => { + const fields: Record = { + styleIdForFill: { guid: { sessionID: 2, localID: 3 } } + } + applyStyleRefsToFields( + new Map([ + [ + '2:3', + { + type: 'RECTANGLE', + styleType: 'FILL', + fillPaints: [{ type: 'SOLID', visible: true }] + } + ] + ]), + fields + ) + expect(fields.fillPaints).toEqual([{ type: 'SOLID', visible: true }]) + }) +}) diff --git a/packages/fig/tsconfig.json b/packages/fig/tsconfig.json index f78bbb1ae..7143ab4be 100644 --- a/packages/fig/tsconfig.json +++ b/packages/fig/tsconfig.json @@ -1,14 +1,23 @@ { - "extends": "../../tsconfig.json", "compilerOptions": { - "rootDir": ".", - "outDir": "dist", + "target": "ES2022", + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, "declaration": true, "declarationMap": true, - "emitDeclarationOnly": true, - "noEmit": false, - "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo", - "types": ["bun"] + "outDir": "./dist", + "rootDir": ".", + "skipLibCheck": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "types": ["bun"], + "paths": { + "@open-pencil/fig": ["./src/index.ts"], + "@open-pencil/fig/node-change": ["./src/node-change/index.ts"] + } }, - "include": ["src"] + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist"] } diff --git a/packages/fig/tsdown.config.ts b/packages/fig/tsdown.config.ts index b7cae2edb..5e935e1a5 100644 --- a/packages/fig/tsdown.config.ts +++ b/packages/fig/tsdown.config.ts @@ -2,7 +2,8 @@ import { defineConfig } from 'tsdown' export default defineConfig({ entry: { - index: './src/index.ts' + index: './src/index.ts', + 'node-change': './src/node-change/index.ts' }, platform: 'neutral', format: ['esm'], diff --git a/packages/kiwi/README.md b/packages/kiwi/README.md index be5914b4c..52c71a036 100644 --- a/packages/kiwi/README.md +++ b/packages/kiwi/README.md @@ -2,7 +2,7 @@ Scene-graph-agnostic Kiwi runtime utilities for OpenPencil. -This package owns pure Kiwi schema parsing, Figma Kiwi schema data, low-level Figma message encode/decode, FIG Kiwi container helpers, GUID formatting, and raw `.fig` parse helpers. `.fig` import/export policy stays in `@open-pencil/core`: SceneGraph conversion, raw metadata invalidation, component/instance interpretation, and app/CLI document I/O are not part of this package. +This package owns pure Kiwi schema parsing, Figma Kiwi schema data, low-level Figma message encode/decode, raw `fig-kiwi` container helpers, and GUID formatting. Complete `.fig` archive parsing lives in `@open-pencil/fig`; SceneGraph integration remains outside this package. ## Installation @@ -74,19 +74,16 @@ const container = buildFigKiwi(new Uint8Array([1, 2, 3])) const chunks = parseFigKiwiChunks(container) ``` -## Raw `.fig` parsing +## Raw `fig-kiwi` payload decoding ```ts -import { parseFigBytes } from '@open-pencil/kiwi/fig/parse' +import { decodeFigKiwiCanvas } from '@open-pencil/kiwi/fig/parse' -const parsed = await parseFigBytes(await Bun.file('design.fig').arrayBuffer()) - -for (const canvas of parsed.canvases) { - console.log(canvas.name, canvas.nodeChanges.length) -} +const decoded = decodeFigKiwiCanvas(canvasBytes) +console.log(decoded.nodeChanges.length, decoded.blobs.length) ``` -`parseFigBytes()` returns structural `NodeChange` data, blobs, images, schema bytes, and container metadata. Use `@open-pencil/core` for converting that data into an editable `SceneGraph`. +Use `parseFigBuffer()` from `@open-pencil/fig` for complete zipped `.fig` files, including image resources. Use `@open-pencil/core/io` for conversion into an editable `SceneGraph`. ## GUID helpers diff --git a/packages/kiwi/src/fig/parse.ts b/packages/kiwi/src/fig/parse.ts index 3f4baeafd..4b5a301d4 100644 --- a/packages/kiwi/src/fig/parse.ts +++ b/packages/kiwi/src/fig/parse.ts @@ -1,4 +1,4 @@ -import { unzipSync, inflateSync } from 'fflate' +import { inflateSync } from 'fflate' import { decompress as zstdDecompress } from 'fzstd' import { decodeBinarySchema, compileSchema, ByteBuffer } from '../schema-runtime' @@ -84,48 +84,17 @@ export function parseFigKiwiContainer(data: Uint8Array): FigKiwiPayload | null { return { schemaDeflated: chunks[0], dataRaw, version } } -export interface FigParseResult { +export interface FigKiwiDecodeResult { nodeChanges: NodeChange[] blobs: Uint8Array[] - images: Array<[string, Uint8Array]> figKiwiVersion: number /** Deflated kiwi schema bytes from the original file (for roundtrip fidelity). */ figSchemaDeflated: Uint8Array } -export function parseFigBuffer(buffer: ArrayBuffer): FigParseResult { - const zip = unzipSync(new Uint8Array(buffer), { - filter: (file) => - file.name === 'canvas.fig' || - file.name === 'canvas' || - (file.name.startsWith('images/') && file.name !== 'images/') - }) - const entries = Object.keys(zip) - - let canvasData: Uint8Array | null = null - for (const name of entries) { - if (name === 'canvas.fig' || name === 'canvas') { - canvasData = zip[name] - break - } - } - if (!canvasData) { - let maxSize = 0 - for (const name of entries) { - const lower = name.toLowerCase() - if (lower.endsWith('.png') || lower.endsWith('.jpg') || lower.endsWith('.json')) continue - if (zip[name].byteLength > maxSize) { - maxSize = zip[name].byteLength - canvasData = zip[name] - } - } - } - - if (!canvasData) { - throw new Error(`No canvas data found in .fig file. Entries: ${entries.join(', ')}`) - } - - const payload = parseFigKiwiContainer(canvasData) +/** Decode one raw `fig-kiwi` canvas payload. Outer `.fig` archive handling lives in `@open-pencil/fig`. */ +export function decodeFigKiwiCanvas(data: Uint8Array): FigKiwiDecodeResult { + const payload = parseFigKiwiContainer(data) if (!payload) throw new Error('Invalid fig-kiwi container') const schemaBytes = inflateSync(payload.schemaDeflated) @@ -138,25 +107,15 @@ export function parseFigBuffer(buffer: ArrayBuffer): FigParseResult { throw new Error('No nodes found in .fig file') } - // Deduplicate before returning — critical for worker path where raw - // nodeChanges are serialized via postMessage before extractPluginData runs deduplicateNodeChangePluginData(nodeChanges) - const blobs: Uint8Array[] = (message.blobs ?? []).map((b) => - b.bytes instanceof Uint8Array ? b.bytes : new Uint8Array(Object.values(b.bytes)) + const blobs: Uint8Array[] = (message.blobs ?? []).map((blob) => + blob.bytes instanceof Uint8Array ? blob.bytes : new Uint8Array(Object.values(blob.bytes)) ) - const images: Array<[string, Uint8Array]> = [] - for (const name of entries) { - if (name.startsWith('images/') && name !== 'images/') { - images.push([name.replace('images/', ''), zip[name]]) - } - } - return { nodeChanges, blobs, - images, figKiwiVersion: payload.version, figSchemaDeflated: payload.schemaDeflated } diff --git a/tests/engine/io/fig/export/component-properties.test.ts b/tests/engine/io/fig/export/component-properties.test.ts index 68d933029..b7cc8d3c4 100644 --- a/tests/engine/io/fig/export/component-properties.test.ts +++ b/tests/engine/io/fig/export/component-properties.test.ts @@ -1,7 +1,7 @@ import { beforeAll, describe, expect, test } from 'bun:test' import { exportFigFile, initCodec } from '@open-pencil/core' -import { parseFigBuffer } from '@open-pencil/kiwi/fig/parse' +import { parseFigBuffer } from '@open-pencil/fig' import { SceneGraph } from '@open-pencil/scene-graph' import { importNodeChanges } from '#core/kiwi/fig/import' diff --git a/tests/engine/io/fig/export/shared-styles.test.ts b/tests/engine/io/fig/export/shared-styles.test.ts index cdabca697..d4498245e 100644 --- a/tests/engine/io/fig/export/shared-styles.test.ts +++ b/tests/engine/io/fig/export/shared-styles.test.ts @@ -1,7 +1,7 @@ import { beforeAll, describe, expect, test } from 'bun:test' import { exportFigFile, initCodec } from '@open-pencil/core' -import { parseFigBuffer } from '@open-pencil/kiwi/fig/parse' +import { parseFigBuffer } from '@open-pencil/fig' import { SceneGraph } from '@open-pencil/scene-graph' import { sceneNodeToKiwi } from '#core/kiwi/fig/node-change/serialize' diff --git a/tests/engine/io/fig/heavy/component-metadata.test.ts b/tests/engine/io/fig/heavy/component-metadata.test.ts index 6332df808..9fc4dc880 100644 --- a/tests/engine/io/fig/heavy/component-metadata.test.ts +++ b/tests/engine/io/fig/heavy/component-metadata.test.ts @@ -1,7 +1,7 @@ import { expect, setDefaultTimeout, test } from 'bun:test' import { readFileSync } from 'node:fs' -import { parseFigBuffer } from '@open-pencil/kiwi/fig/parse' +import { parseFigBuffer } from '@open-pencil/fig' import { importNodeChanges } from '#core/kiwi' diff --git a/tests/engine/io/fig/import/style-refs.test.ts b/tests/engine/io/fig/import/style-refs.test.ts index e05da69cf..0155746f6 100644 --- a/tests/engine/io/fig/import/style-refs.test.ts +++ b/tests/engine/io/fig/import/style-refs.test.ts @@ -1,9 +1,8 @@ import { describe, expect, test } from 'bun:test' +import { applyStyleRefsToFields } from '@open-pencil/fig/node-change' import type { NodeChange } from '@open-pencil/kiwi/fig/codec' -import { applyStyleRefsToFields } from '#core/kiwi/fig/node-change/style-refs' - describe('fig import style refs', () => { test('effect and grid styles replace stale direct payloads', () => { const effectGuid = { sessionID: 4, localID: 5000 } diff --git a/tests/engine/io/fig/roundtrip/export-settings.test.ts b/tests/engine/io/fig/roundtrip/export-settings.test.ts index a37197880..8802b5f0e 100644 --- a/tests/engine/io/fig/roundtrip/export-settings.test.ts +++ b/tests/engine/io/fig/roundtrip/export-settings.test.ts @@ -8,7 +8,7 @@ import { SceneGraph, type NodeChange } from '@open-pencil/core' -import { parseFigBuffer } from '@open-pencil/kiwi/fig/parse' +import { parseFigBuffer } from '@open-pencil/fig' import { MAX_EXPORT_SCALE } from '@open-pencil/scene-graph' function decodeExport(bytes: Uint8Array) { diff --git a/tests/engine/io/fig/roundtrip/glyph-blob.test.ts b/tests/engine/io/fig/roundtrip/glyph-blob.test.ts index f58b5428c..045254eb4 100644 --- a/tests/engine/io/fig/roundtrip/glyph-blob.test.ts +++ b/tests/engine/io/fig/roundtrip/glyph-blob.test.ts @@ -4,7 +4,7 @@ import { resolve } from 'node:path' import { exportFigFile, initCodec, parseFigFile, SceneGraph } from '@open-pencil/core' import { fontManager } from '@open-pencil/core/text' -import { parseFigBuffer } from '@open-pencil/kiwi/fig/parse' +import { parseFigBuffer } from '@open-pencil/fig' import { HEAVY_TEST_TIMEOUT_MS } from '#tests/helpers/test-utils' diff --git a/tests/engine/io/fig/roundtrip/source-metadata.test.ts b/tests/engine/io/fig/roundtrip/source-metadata.test.ts index 80eafed33..0456fffd5 100644 --- a/tests/engine/io/fig/roundtrip/source-metadata.test.ts +++ b/tests/engine/io/fig/roundtrip/source-metadata.test.ts @@ -2,7 +2,7 @@ import { beforeAll, describe, expect, test } from 'bun:test' import { exportFigFile, initCodec, parseFigFile, SceneGraph } from '@open-pencil/core' import { guidToString } from '@open-pencil/core/kiwi/fig/node-change/convert' -import { parseFigBuffer } from '@open-pencil/kiwi/fig/parse' +import { parseFigBuffer } from '@open-pencil/fig' function decodeExport(bytes: Uint8Array) { return parseFigBuffer(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength)) diff --git a/tests/engine/io/subgraph.test.ts b/tests/engine/io/subgraph.test.ts index 1531fe705..ffc1decca 100644 --- a/tests/engine/io/subgraph.test.ts +++ b/tests/engine/io/subgraph.test.ts @@ -3,8 +3,8 @@ import { readFileSync } from 'node:fs' import { exportFigFile, extractExportGraph, parseFigFile } from '@open-pencil/core/io' import { initCodec } from '@open-pencil/core/kiwi' +import { parseFigBuffer } from '@open-pencil/fig' import { guidToString } from '@open-pencil/kiwi/fig/guid' -import { parseFigBuffer } from '@open-pencil/kiwi/fig/parse' import { SceneGraph } from '@open-pencil/scene-graph' describe('export subgraph extraction', () => { diff --git a/tests/engine/render/canvas/silhouette-autopsy.test.ts b/tests/engine/render/canvas/silhouette-autopsy.test.ts index a33039897..5cadf5592 100644 --- a/tests/engine/render/canvas/silhouette-autopsy.test.ts +++ b/tests/engine/render/canvas/silhouette-autopsy.test.ts @@ -39,7 +39,7 @@ const scenePath = coreSourcePath('canvas/scene.ts') const rendererPath = coreSourcePath('canvas/renderer.ts') const sgTypesPath = repoPath('packages/scene-graph/src/types.ts') const nodeExportPath = coreSourcePath('kiwi/fig/node-change/export-node.ts') -const convertPath = coreSourcePath('kiwi/fig/node-change/paint.ts') +const convertPath = repoPath('packages/fig/src/node-change/paint.ts') const schemaPath = repoPath('packages/kiwi/src/fig/schema/fig.kiwi') const codecPath = repoPath('packages/kiwi/src/fig/codec.ts') const lifecyclePath = coreSourcePath('canvas/renderer/lifecycle.ts') diff --git a/tools/package-quality/src/smoke.ts b/tools/package-quality/src/smoke.ts index 82ede4dc6..c453e73f8 100644 --- a/tools/package-quality/src/smoke.ts +++ b/tools/package-quality/src/smoke.ts @@ -90,7 +90,11 @@ try { tempDir ) nodeEval( - "const { FIG_PACKAGE_STATUS, readFigContainer, writeFigContainer } = await import('@open-pencil/fig'); if (FIG_PACKAGE_STATUS !== 'container-api') throw new Error('Fig package status smoke failed'); const document = readFigContainer(writeFigContainer({ schemaDeflated: new Uint8Array([1]), dataRaw: new Uint8Array([2]) })); if (document.dataRaw[0] !== 2) throw new Error('Fig container smoke failed')", + "const { FIG_PACKAGE_STATUS, parseFigBuffer, writeFigArchive, readFigContainer, writeFigContainer } = await import('@open-pencil/fig'); if (FIG_PACKAGE_STATUS !== 'archive-api' || typeof parseFigBuffer !== 'function' || typeof writeFigArchive !== 'function') throw new Error('Fig package status smoke failed'); const document = readFigContainer(writeFigContainer({ schemaDeflated: new Uint8Array([1]), dataRaw: new Uint8Array([2]) })); if (document.dataRaw[0] !== 2) throw new Error('Fig container smoke failed')", + tempDir + ) + nodeEval( + "const { convertLineHeight } = await import('@open-pencil/fig/node-change'); if (convertLineHeight({ value: 120, units: 'PERCENT' }, 20) !== 24) throw new Error('Fig NodeChange subpath failed')", tempDir ) nodeEval(