Commit graph

138 commits

Author SHA1 Message Date
Danila Poyarkov d55eb7ce9e Replace inline import() type annotations with top-level imports 2026-03-04 20:03:55 +03:00
Danila Poyarkov 872c53dd9b Fix override resolution for deleted internal page nodes
Two fixes for component instances with multi-level nesting:

1. getComponentRoot now falls back to kiwi symbolData when graph
   nodes are deleted (internal page cleanup). Resolves the component
   chain via symbolData.symbolID so findNodeByComponentId can match
   clones whose componentId points to a deleted source node.

2. propagateOverridesTransitively skips nodes that were directly
   targeted by applySymbolOverrides. Previously the sync would
   overwrite override values (e.g. text 'Health' → 'Badge') with
   the source component's defaults.
2026-03-04 20:02:41 +03:00
Danila Poyarkov 8f740ce093 Fix vector rendering with pre-computed geometry and stroke outlines
Use fillGeometry/strokeGeometry blobs from .fig files for pixel-perfect
vector rendering. These pre-computed outlines account for stroke alignment,
caps, joins, and miter limits — eliminating white gaps between adjacent
stroked shapes.

For clipboard paste (where geometry blobs are unavailable), convert
vectorNetwork paths to filled stroke outlines via CanvasKit path.stroke().

Apply derivedSymbolData transforms, sizes, and geometry during import
so instance children render at the correct scale and position. When
derivedSymbolData provides size but no geometry, scale inherited blobs
proportionally.
2026-03-04 19:21:18 +03:00
Danila Poyarkov 41e5c08434 Fix component property assignments for nested instances
Three issues fixed:

1. componentPropAssignments inside symbolOverrides are scoped to the
   nested instance their guidPath resolves to, not the top-level
   instance. Resolve guidPath first, then apply to that subtree.

2. Instance swap values can use guidValue (GUID object) in addition
   to textValue (string). Handle both formats.

3. Transitive clone sync was running BEFORE component properties,
   causing repopulateInstance to wipe children that had just received
   visibility assignments. Reorder: symbolOverrides → transitive sync
   → componentProperties → derivedSymbolData.

Also extracted propagateOverridesTransitively into its own function
and guarded against propagating undefined values that would overwrite
explicitly-set properties.
2026-03-04 18:50:29 +03:00
Danila Poyarkov 850490a224 Clean up instance-overrides: extract helpers, remove dead type
- Extract swapInstanceComponent() — reused by symbolOverrides and
  componentPropAssignments
- Extract findPropRefs() — componentId chain walk as standalone fn
- Flatten applyPropAssignments() into a recursive fn instead of
  nested closure inside a loop
- Remove unused ComponentPropDef interface
- Replace per-node Set allocation with depth-limited loop
- Remove unnecessary Record<string, unknown> cast
2026-03-04 18:12:30 +03:00
Danila Poyarkov 311fc76616 Apply derivedSymbolData sizes on import
Figma pre-computes child dimensions for each combination of
component property values and stores them in derivedSymbolData.
Apply these size overrides using the same guidPath resolution
as symbolOverrides so containers shrink correctly when boolean
properties hide children.
2026-03-04 18:05:24 +03:00
Danila Poyarkov a0a93ff500 Apply component property assignments on import
Evaluate componentPropAssignments from instances against
componentPropRefs on cloned children. Handles VISIBLE (boolean
toggle) and OVERRIDDEN_SYMBOL_ID (instance swap) property fields.
Walks componentId chain to find propRefs on source nodes.
2026-03-04 18:00:01 +03:00
Danila Poyarkov efdb09852f Fix override resolution for nested instance targets
When an override targets an instance node directly (not a descendant),
the node's componentId matches the remapped target. Check the current
node itself before searching descendants. Also prefer exact componentId
match over root-chain match for more precise resolution.
2026-03-04 17:04:49 +03:00
Danila Poyarkov 26c14ac903 Unify instance override logic between .fig import and clipboard paste
Extract populateAndApplyOverrides() into instance-overrides.ts shared
by fig-import.ts and clipboard.ts. The clipboard path previously only
handled single-level guid paths and missed nested overrides, instance
swaps, and transitive propagation.
2026-03-04 15:15:19 +03:00
Danila Poyarkov aa0a1b5def Fix workspace resolution: add bun export condition to core package
Bun picks the "node" condition pointing to dist/index.js which doesn't
exist in CI or fresh checkouts. Explicit "bun" condition ensures Bun
always resolves to source files.
2026-03-04 14:51:44 +03:00
Danila Poyarkov 74b09d4397 Merge branch 'renderer-profiler' into compatibility-fixes 2026-03-04 14:19:35 +03:00
Danila Poyarkov 713a0dc817 Fix instance overrides and layout scoping on .fig import
- Resolve symbolOverride guidPaths by overrideKey instead of guid
- Handle overriddenSymbolID for instance component swaps
- Propagate overrides transitively through the componentId clone chain
- Preserve internalOnly flag on canvas nodes during export
- Scope computeAllLayouts to current page for paste/undo/font-load
2026-03-04 14:18:37 +03:00
Danila Poyarkov 2a86ea5ce5 Add renderer profiler with HUD overlay, GPU timing, and phase instrumentation
Profiler modules in packages/core/src/profiler/:
- FrameStats: rolling 120-frame buffer tracking FPS, CPU/GPU times, node counts
- GpuTimer: EXT_disjoint_timer_query_webgl2 wrapper for GPU-side timing
- DrawCallCounter: WebGL context proxy counting draw calls per frame
- PhaseTimer: Chrome DevTools Performance panel custom tracks via User Timing API
- HudRenderer: in-canvas overlay with stats text and frame time bar graph
- CaptureStack: per-node profiling with speedscope JSON export
- RenderProfiler: orchestrates all layers with zero cost when disabled

Integration:
- SkiaRenderer.render() instrumented with phase markers for scene, picture
  recording/replay, section titles, component labels, selection, rulers, flush
- Node counting and viewport culling tracking in renderNode()
- WebGL2 context passed through for GPU timer and draw call counter
- Shift+P keyboard shortcut to toggle HUD overlay
- toggleProfiler() added to editor store
2026-03-04 14:13:17 +03:00
Danila Poyarkov 72f140700d Guard null cached filters in renderer destroy 2026-03-04 12:30:35 +03:00
Danila Poyarkov 9d35b3a9b5 Apply symbol overrides to cloned instance children on .fig import
Figma .fig files store instance overrides (text, fills, visibility,
layout props, corner radii, etc.) in symbolData.symbolOverrides on
each INSTANCE node. Each override targets a descendant via guidPath
(chain of original Figma GUIDs).

Implementation:
- convertOverrideToProps() in kiwi-convert.ts maps raw kiwi override
  fields to SceneNode properties (reuses existing conversion helpers)
- resolveOverrideTarget() walks the cloned instance tree to find the
  target node by following componentId chains
- getComponentRoot() traverses componentId references to handle
  multi-level cloning (instance of instance of component)
- 49K+ overrides applied across 8.8K instances in Preline UI file
2026-03-04 11:11:52 +03:00
Danila Poyarkov 989378f444 Guard against null Yoga children in layout application
Instance population can create scene graph children that don't
have corresponding Yoga nodes (e.g. when the Yoga tree was built
before cloned children were fully resolved). Skip null yogaChild
instead of crashing.
2026-03-04 10:59:01 +03:00
Danila Poyarkov 362212b712 Populate instance children from components on .fig import
During .fig import, instance nodes have no children — Figma expects
them to be cloned from their component. The componentId references
used original Figma GUIDs that didn't match our generated node IDs.

Fix:
- Track original GUID → new node ID mapping during import
- Remap all instance componentId references after node creation
- Iteratively populate empty instances from their components
  (multiple passes needed since cloning creates new instances)
2026-03-04 10:55:29 +03:00
Danila Poyarkov 3c0c4e530c Guard against double destroy and post-unmount rendering
- Add destroyed flag to SkiaRenderer.destroy() to prevent double-free
- Check destroyed flag in renderNow() to skip rendering after unmount
  (loadFonts promise could resolve after component unmounts)
2026-03-04 10:35:44 +03:00
Danila Poyarkov 4e966c4976 Use byte comparison for fractional index sorting
localeCompare is locale-dependent and produces different orderings
across runtimes (Bun vs browser, Safari vs Chrome). Figma's
fractional indexing uses base-95 ASCII which sorts correctly by
code point.
2026-03-04 10:27:22 +03:00
Danila Poyarkov 9e3a6c0e7f Extract collectFontKeys into core and add font tests
- Extract font key collection from editor store into
  collectFontKeys() in packages/core/src/fonts.ts
- 18 tests: styleToWeight, weightToStyle, markFontLoaded/isFontLoaded,
  collectFontKeys (nested nodes, style runs, deduplication, default
  font filtering, invalid IDs)
2026-03-04 10:16:26 +03:00
Danila Poyarkov 45cab3e5fb Merge branch 'individual-strokes' into compatibility-fixes 2026-03-04 09:49:23 +03:00
Danila Poyarkov c412b78ed8 Add stroke align, individual side strokes, and side selector UI
- Renderer: stroke align (INSIDE/CENTER/OUTSIDE) using clip-based
  approach for all shape types including sections
- Renderer: individual side strokes when independentStrokeWeights is
  enabled, drawing per-side lines with correct align offsets
- StrokeSection: align dropdown, side selector dropdown (All/Top/Bottom/
  Left/Right/Custom), individual weight inputs with side-specific icons
- Kiwi serialize: export correct strokeAlign and border weight fields
- Scene graph: sync border weight props through instance sync
- 19 tests covering align, individual weights, instance sync, kiwi
  export, and FigmaAPI properties
2026-03-04 09:47:14 +03:00
Danila Poyarkov 969158a6c2 Move Google Fonts API key to constants 2026-03-04 09:33:42 +03:00
Danila Poyarkov 630277b896 Add Google Fonts fallback via Developer API
Fetch TTF files from Google Fonts when local and bundled fonts
are unavailable. Uses the Webfonts API for direct TTF URLs per
variant. Font file listings are cached per family; failed lookups
are recorded to avoid retries.
2026-03-04 09:32:45 +03:00
Danila Poyarkov 68cfba3b6d Fix text truncation: use textTruncation field instead of textAutoResize 2026-03-04 09:04:04 +03:00
Danila Poyarkov b38d088383 Fix core tsconfig: add ESNext and DOM libs for npm publish 2026-03-04 01:58:37 +03:00
Danila Poyarkov 7ad4116860 Release v0.6.0 2026-03-04 01:47:37 +03:00
Danila Poyarkov 698ffe389a Remove Gatekeeper workaround — app is now code-signed 2026-03-04 01:45:29 +03:00
Danila Poyarkov 81ef1fbc45 Enable halfLeading for text rendering only, not measurement
Figma distributes leading equally above and below text. CanvasKit
defaults to ascent-biased leading. halfLeading: true fixes this but
must only apply to rendering (drawParagraph), not measureTextNode —
otherwise measured sizes change and break grid layouts.
2026-03-04 01:15:44 +03:00
Danila Poyarkov 26ed064b07 Fix InvalidCharacterError when copying nodes with non-ASCII text
btoa() only handles Latin-1. Use TextEncoder + binaryToBase64 for
encoding and TextDecoder + base64ToBinary for decoding the internal
OpenPencil clipboard format.
2026-03-04 01:15:44 +03:00
Danila Poyarkov af9a0ebb5a Fix PERCENT line height conversion — was stored as raw value instead of pixels
Figma's lineHeight with units=PERCENT stores e.g. 100 meaning 100%
of fontSize. We stored the raw value (100) which the renderer
interpreted as 100px, producing ~6x line height. Now properly
converts: PERCENT → value/100 * fontSize, PIXELS → value as-is.
2026-03-04 00:20:43 +03:00
Danila Poyarkov 9c82cba672 Fix font loading not registering in core cache, add layout text measurement tests
Tauri's loadFont registered fonts with the CanvasKit provider but
not in core's loadedFamilies cache, so isFontLoaded returned false
and measureTextNode returned null during layout recomputation.

Add markFontLoaded() to core and call it from Tauri font loader.
Add tests verifying text measurement integrates with auto-layout
centering.
2026-03-04 00:16:15 +03:00
Danila Poyarkov d7e2015967 Estimate text width for WIDTH_AND_HEIGHT auto-resize in layout
Text nodes with textAutoResize=WIDTH_AND_HEIGHT now use an estimated
content width instead of the node's current width when computing
auto-layout. Fixes month headers (and similar centered text in
auto-layout) appearing left-aligned after paste.
2026-03-04 00:11:48 +03:00
Danila Poyarkov 9eaab86733 Apply layout overrides (layoutGrow, textAutoResize) to instance children 2026-03-04 00:04:07 +03:00
Danila Poyarkov 534a7b298e Add DEFAULT_FONT_FAMILY constant, load fonts on paste and .fig import
Replace all 'Inter' string literals with DEFAULT_FONT_FAMILY from
constants. Add loadFontsForNodes() that collects font families from
text nodes and loads them into CanvasKit, called after clipboard
paste and .fig file import.
2026-03-03 23:56:21 +03:00
Danila Poyarkov da2df1296b Extract shared sortChildren, reduce kiwi-convert export surface 2026-03-03 23:34:02 +03:00
Danila Poyarkov 9044b1b183 Sort auto-layout children by geometric position on import
Figma's parentIndex.position strings represent z-order, not visual
layout order. In auto-layout frames, children must be sorted by their
x (horizontal) or y (vertical) coordinate to match the visual order.
Fixes scrambled calendar dates and other auto-layout child ordering.
2026-03-03 23:31:23 +03:00
Danila Poyarkov 157c776c0c Apply instance symbolOverrides on clipboard paste
Each instance in Figma's clipboard carries symbolData.symbolOverrides
with per-child property changes (text, fills, visibility). The override
targets a child via guidPath which matches the overrideKey field on the
component's child node.

After cloning component children into instances, overrides are resolved
through the overrideKey→figmaId→internalId→clonedChild chain and applied
to the instance's children.
2026-03-03 23:26:29 +03:00
Danila Poyarkov 3eb3a921a0 Detect component sets and skip internal canvas on paste
FRAME nodes with VARIANT componentPropDefs are now promoted to
COMPONENT_SET on import (both .fig files and clipboard paste).

Clipboard paste now respects the Internal Only Canvas: components
on it are used to populate instance children but are not pasted
as visible nodes, matching Figma's behavior.
2026-03-03 23:20:55 +03:00
Danila Poyarkov 3d4f083d5e Populate instance children from component on clipboard paste
When pasting from Figma, INSTANCE nodes reference their component via
symbolData.symbolID but have no children in the clipboard. If the
component was also pasted, clone its children into the instance using
the componentId mapping (cloneChildrenWithMapping).
2026-03-03 21:20:31 +03:00
Danila Poyarkov e737ee5c13 Scale vector paths from normalizedSize to node size on import
Figma vectors use a normalized coordinate space (vectorData.normalizedSize)
that can differ from the node's actual size. The decoded path vertices and
tangents must be scaled to fit the node bounds, otherwise vectors render
at the wrong size (e.g. 16×16 path in a 12×12 node).
2026-03-03 21:19:35 +03:00
Danila Poyarkov 22e9553b30 Fix Figma clipboard paste: broken layout and missing properties
Extract shared kiwi→SceneNode conversion into kiwi-convert.ts, used by
both fig-import.ts (.fig file opening) and clipboard.ts (Figma paste).

The clipboard import was a simplified copy that diverged over time:
- Missing SYMBOL→COMPONENT type mapping (Figma's kiwi name for components)
- textAutoResize hardcoded to NONE instead of using clipboard value
- No gradient/image fill support, no effects, no style runs
- Missing flipX/flipY, constraints, text decorations, arc data, etc.
- Percent letter spacing not converted to pixels

Now both paths use nodeChangeToProps() with identical conversion logic.
2026-03-03 21:19:35 +03:00
Danila Poyarkov 3f4169e32d Add macOS Gatekeeper workaround to README and docs 2026-03-03 21:19:07 +03:00
Danila Poyarkov 7e06969ab5 Multi-selection properties panel with tests
Show position, appearance, fill, stroke, and effects sections
when multiple nodes are selected. Shared values display normally,
differing values show 'Mixed'. Editing applies to all selected.

- Shared composable (use-multi-props) for isMulti, active, activeNode
- ScrubInput supports MIXED symbol as model-value
- Per-node previous values for correct undo on multi-edit
- Fix flip transform encoding (scale first column only)
- Fix npm publish auth for MCP package
- 22 new tests: flip, alignment, multi-node merging, kiwi roundtrip
2026-03-03 20:35:51 +03:00
Danila Poyarkov 5a77f2c1cc Fix flip and single-node alignment
Flip horizontal/vertical now toggles flipX/flipY with scale
transform instead of manipulating rotation. Import detects flip
from negative transform determinant. Single-node alignment uses
parent frame bounds instead of bailing out.
2026-03-03 18:35:30 +03:00
Danila Poyarkov 9c9bfc38b2 Release v0.5.1 2026-03-03 17:30:09 +03:00
Danila Poyarkov 06fb0a3e7a Fix save: map LAYER_BLUR to FOREGROUND_BLUR for kiwi export 2026-03-03 17:29:43 +03:00
Peter 31d614a272
fix(mcp): harden HTTP transport defaults (#26) 2026-03-03 17:19:08 +03:00
Danila Poyarkov 5eac653f4c Release v0.5.0 2026-03-03 14:51:56 +03:00
Danila Poyarkov c36f3c4667 Only show component labels for COMPONENT and COMPONENT_SET, not instances 2026-03-03 14:40:29 +03:00