Localize remaining menu state labels like Hide/Show and Lock/Unlock,
wire toolbar tool/action labels through the i18n stores, and localize
the app menu UI toggle tooltip.
Replace template-string JSON imports with an explicit locale loader map
so published package builds have stable locale asset references.
Commands and menu models now read labels from nanostores i18n
messages instead of hardcoded English strings. App menu rebuilt
to use i18n directly instead of matching on localized labels.
Added locale files: de, es, fr, it, pl (ru was added earlier).
All 6 translations plus English base cover menus, commands, tools,
panels, pages, and dialogs.
nanostores + @nanostores/i18n + @nanostores/vue (~6 KB total).
English base translations defined inline in messages.ts, other
locales loaded on demand from JSON. Grouped by domain: menu,
commands, tools, panels, pages, dialogs.
useI18n() composable returns reactive translation objects plus
locale controls. Russian locale included as first translation.
Browser language auto-detected, user preference persisted in
localStorage.
Figma expects text inside auto-layout frames to have no textAutoResize
field (NONE). Internally OpenPencil uses WIDTH_AND_HEIGHT so the layout
engine can measure text, but this caused overflow when exported .fig
files were opened in Figma.
Normalize at export time, matching how we already normalize font
families for the binary format.
Fixes#136
Parse the version field from the fig-kiwi header during .fig import
and store it on SceneGraph.figKiwiVersion. Use the original version
when re-exporting. New files default to 101 (current Figma standard).
Previously hardcoded to 106, which Figma rejected on import.
Fixes#153
The initial renderNow() fired before loadFonts() completed, causing
text measurement to return null on slower connections (especially
Safari). Layout computed without text dimensions got corrupted.
Move loadFonts() into the async init() flow and only render after
fonts are ready.
Hold Spacebar to temporarily switch to the Hand tool for panning,
release to return to the previously active tool. This matches the
standard Figma UX pattern.
- Uses separate keydown/keyup event listeners for reliable handling
- Guards against text editing mode and key repeat
- References state.activeTool for correct tool state
Fixes#147
- Zero transforms for auto-layout children (Figma computes positions)
- Write bordersTakeSpace from strokesIncludedInLayout
- Always write explicit lineHeight, defaulting to ceil(fontSize * 1.2)
- Normalize font family in derivedTextData fontMetaData and fontName
- Extend normalizeFontFamily to strip optical size suffixes (9pt, 18px)
- Extract computeExportTransform() helper for lint compliance
- Add 20 unit tests covering all fixes
Fixes#134
TEXT nodes created via SceneGraph.createNode() defaulted to empty
fills, causing text to be invisible when .fig files are opened in
Figma. Now defaults to a solid black fill matching Figma's behavior.
Closes#133
Strip optical size suffixes (e.g. "DM Sans 9pt" → "DM Sans") and
"Variable" suffixes when writing fontName.family to .fig files.
This ensures Figma recognizes the font instead of showing a
"Missing font" dialog.
Closes#131
Previously, the CJK fallback loaded only a single Google Font
(Noto Sans SC) when local system fonts were unavailable. This meant
Korean and Japanese text would not render correctly in browsers that
block the Local Font Access API.
Changes:
- Replace CJK_GOOGLE_FONT (single string) with CJK_GOOGLE_FONTS array
containing Noto Sans SC, Noto Sans JP, and Noto Sans KR
- Load all three fonts in parallel via Promise.allSettled so each
script has proper glyph coverage
- Update text renderer to pass all loaded CJK families as fallbacks
- Maintain backward compatibility: getCJKFallbackFamily() still works,
new getCJKFallbackFamilies() returns the full list
Drop the emitter-to-shallowRef bridge (rAF batched, 60 lines).
state.sceneVersion on shallowReactive IS already Vue-reactive.
useSceneComputed is now 15 lines. Zero latency during scrub.
- CanvasContext hit test return types: unknown → SceneNode | null
- Move toast.ts from SDK to app (src/utils/toast.ts) — singleton
with unguarded window access doesn't belong in headless SDK
- ExportSection: store.graph.getNode → store.getNode
- ImageFillPicker: store.graph.images.get → store.getImage (new method)
- Add getImage(hash) to core editor create.ts
Zero store.graph access from components. Zero toast in SDK.
- duplicateSelected: recursive subtree cloning via graph.cloneTree()
Filters to top-level selection to avoid double-cloning. Children
get new IDs wired to new parent. Single undo entry.
- PropertyListRoot isMixed: compare array lengths first (fast path),
only JSON.stringify when lengths match (rare).
- glContext in use-canvas.ts: ownership verified correct, no change.
Critical:
- restorePageFromSnapshot: emit node:reordered after bulk restore
so component sync and useSceneComputed bridge detect changes
- subscribeToGraph: capture and cleanup unbind functions on
replaceGraph() — prevents listener leak on old graph emitter
Important:
- useNodeProps: node/nodes use useSceneComputed instead of plain
computed — re-evaluate on any scene mutation, not just selection
- Undo forward closures: explicit id: containerId/frameId/groupId
in createNode spreads — no more implicit ID preservation contract
Moderate:
- Extract usePropScrub composable — deduplicate previousValues +
updateProp + commitProp from AppearanceControls + PositionControls
Replace the sceneVersion counter hack with proper Vue pattern:
- shallowRef + triggerRef bridged from SceneGraph's nanoevents emitter
- Ref-counted listeners — subscribe on first useSceneComputed, unsubscribe
when last scope is disposed. WeakMap per editor instance.
- Emitter fires on node:updated/created/deleted/reparented/reordered
sceneVersion counter stays in EditorState for:
- Renderer cache invalidation (label-cache, scene picture recording)
- watch() dependencies in LayerTreeRoot, ExportSection, autosave
These are legitimate uses of the counter as a cache key.
Add useSceneComputed() to SDK — wraps a function in a computed()
that depends on sceneVersion internally. Eliminates the anti-pattern
of 'void editor.state.sceneVersion' at the top of computed blocks.
Updated 8 files: EditorMenuRoot, ExportControlsRoot, PageListRoot,
useSelectionState, PagesPanel, CodePanel, VariablesDialog,
VariablesSection.
Zero void-sceneVersion hacks remaining in components.
Extracted shared helpers to eliminate copy-pasted code:
Core:
- computeAbsoluteBounds() in geometry.ts — replaces 4 bounding box loops
- makeDocumentNodeChange/makeCanvasNodeChange in kiwi-serialize.ts
- parseFigBuffer/parseFigKiwiContainer in kiwi/fig-parse-core.ts
(fig-parse-worker.ts: 103 → 11 lines)
- createDefaultEditorState() — shared between core + app store
- calcClusterConfidence() in tools/analyze.ts
- retryWithNormalizedFamily() in fonts.ts
- getVectorNode() in tools/vector.ts
- renderFills() helper in renderer/scene.ts
- withNodeBounds() helper in renderer/overlays.ts
- drawNodeHighlightRect() in renderer/highlight-rect.ts
- isInViewport() in renderer/label-cache.ts
App:
- decodeTauriStderr() in utils/tauri.ts
- printNodeResults() in cli format.ts
Remaining 28 clones are: text-editor cursor movement (inherent),
xpath DOM traversal (inherent), small 5-line patterns too
context-specific to extract.