Commit graph

150 commits

Author SHA1 Message Date
Danila Poyarkov 4b3d97ce0d feat(text): add demand-driven font resolver
- Resolve exact faces through registered, local, cache, and remote sources with deduplicated state transitions\n- Use CanvasKit notdef glyphs to request script fallbacks and repaint after resolution\n- Make CJK coverage analysis codepoint-safe across supplementary characters and style runs
2026-07-17 13:00:09 +03:00
Danila Poyarkov 5a89b520b2 fix(text): load CJK fallback only when needed 2026-07-01 22:06:19 +03:00
Danila Poyarkov 3a5b6a5c37 test: share scene variable helper 2026-06-30 10:55:05 +03:00
Danila Poyarkov 4c26536e69 test: align render shard with package split 2026-06-30 10:55:05 +03:00
Danila Poyarkov 898431e646 feat: split SceneGraph and Pen packages 2026-06-30 10:54:32 +03:00
Danila Poyarkov 7d27cac5ab refactor(kiwi): remove redundant core shims 2026-06-30 10:50:30 +03:00
Danila Poyarkov 6877516f06 refactor(kiwi): consume standalone Kiwi package 2026-06-30 10:49:14 +03:00
Danila Poyarkov 8983c11f45 feat(design-jsx): support structured effects 2026-06-30 10:45:15 +03:00
Danila Poyarkov cc1167eed5 feat(design-jsx): support structured paints 2026-06-30 10:44:51 +03:00
Danila Poyarkov 4b65e64c18 feat(design-jsx): support variable refs 2026-06-30 10:44:28 +03:00
Danila Poyarkov 568cdf91ef Merge remote-tracking branch 'joey/feature-overlapping-element-analysis-tool' into pr352-local-merge
# Conflicts:
#	tests/engine/clipboard/fixtures/gold-preview-roundtrip.test.ts
#	tests/engine/io/fig/import/group-reclassify.test.ts
#	tests/engine/io/fig/roundtrip/glyph-blob.test.ts
#	tests/engine/layout/auto-layout/text/measurement.test.ts
2026-06-30 09:47:07 +03:00
Joseph Cumines dc9638ac3e
fix(export): prevent GUID collisions and file corruption on .fig round-trip (#333)
* fix(export): prevent GUID collisions and file corruption on .fig round-trip

Nodes sharing the same imported source.id (component instance children,
cloned subtrees) silently overwrote each other on export because the
GUID assignment reused imported GUID values without checking for
duplicates.  This caused data loss on reimport — only the last node
with a given GUID survived.

- Track all assigned GUID values in a Set for O(1) collision detection.
- Scan imported source.ids for both sessionID 0 and 1 before assigning
  any new GUIDs, so the counter starts past every imported value.
- Fall back to counter-based GUIDs when source.id collides with an
  already-assigned value.

Additional fixes in the same change set:

- cloneTree now deep-copies source.fig via structuredClone, preventing
  mutations on a clone from corrupting the original node's kiwi payload.
- Removed decompressFigKiwiData sync wrapper (zero callers) and the
  silent try/catch fallback in parseFigKiwiContainer that masked
  corrupt data as raw bytes.
- buildFigKiwi uses Bun.zstdCompressSync when available, matching the
  zstd decompression path already used on import.
- Fixed setSavedVersion ordering in read.ts — must run after
  requestRender to capture the post-bump version, preventing spurious
  dirty-state immediately after file reload.

Tests: GUID collision (2 and 3 node), clone isolation, parse failure,
text export zstd compatibility, gold-preview round-trip.

* fix(export): handle EXCLUDE boolean operation and BOOLEAN_OPERATION node type

The internal representation uses EXCLUDE for exclude boolean operations,
but Figma's kiwi schema uses XOR. Map EXCLUDE back to XOR on export so
round-trips through .fig files don't fail. Also add BOOLEAN_OPERATION to
VALID_NODE_TYPES and increase timeout for heavy material3 fixture test.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* style: format export-node.ts

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* test: add type guard after null assertion in guid-collision test

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(clipboard): detect zstd-compressed data before zlib inflate

fflate's inflateSync silently accepts zstd-compressed data and returns
garbage instead of throwing. Check for the zstd magic bytes (28 b5 2f
fd) before attempting zlib decompression. Also revert the EXCLUDE enum
addition to the kiwi schema since the export-node.ts mapping is
sufficient.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(clipboard): add length guard before zstd magic byte check

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* test(scene-graph): group clone regression coverage

* ci: retrigger CI checks

* test: increase timeouts for heavy .fig fixture tests on slow CI runners

Gold-preview.fig and material3.fig parsing/export tests consistently
exceed the 5s bun:test default timeout on GitHub Actions runners.
Increase to 30s for: beforeAll codec init, clipboard roundtrip,
glyph blob roundtrip, group reclassification, and text measurement.

* test: add individual test timeouts for heavy .fig fixture tests

The beforeAll timeout helped but individual test() calls also need
explicit 30s timeouts since bun:test applies the 5s default per-test.
Fixes remaining CI flakes in glyph-blob roundtrip and clipboard
roundtrip tests.

* test: increase beforeAll timeout for render cache test

The canvas/render cache test loads gold-preview.fig AND initializes
CanvasKit (Skia WASM), which is much slower than the other fixture
tests. Use 60s timeout to account for slow CI runners.

* fix(export): reserve document GUID to prevent 0:0 namespace collision

- Add docGuid (0:0) to assignedGuidValues before processing imported
  node source.ids, preventing an imported node with source.id "0:0"
  from reusing the document's GUID slot
- Add regression test using session-0 source.ids to verify nodes
  survive roundtrip without document GUID collision
- Remove unnecessary async keyword from synchronous component
  metadata test

* fix(export): guard canvas GUID reuse with assignedGuidValues check

- Mirror getOrCreateNodeGuid() collision logic in buildCanvasEntries():
  if an imported page's source.id maps to a GUID already in
  assignedGuidValues, generate a fresh counter-based GUID instead
- Prevents canvas-level last-write-wins when multiple pages share
  the same source.id or a page uses 0:0

* fix(test): use explicit little-endian writes and fix misleading test title

- Replace host-endian Uint32Array writes with DataView.setUint32(offset, value, true) in parse-failures.test.ts to ensure platform-independent fig-kiwi container assembly
- Rename test title from "clone clears source.id from the original" to "clone clears source.id from the clone" to accurately reflect what the assertions verify

* test(io): use file-level timeout for heavy fixture

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Danila Poyarkov <dev@dannote.net>
2026-06-23 18:05:23 +03:00
Joseph Cumines fa5c072cad test: extend timeout for slow gold-preview fixture tests
Port the timeout fixes from PR #351 that resolved the unit-test CI
timeouts. Add HEAVY_TEST_TIMEOUT_MS (30s) in tests/helpers/test-utils.ts
and apply it to the slow gold-preview.fig fixture tests/hooks:
- clipboard roundtrip
- group reclassification
- glyph blob preservation
- auto-layout text measurement
- render/canvas/cache
2026-06-19 11:05:25 +10:00
Danila Poyarkov 05e4a29a08 feat(design-jsx): support components and instances 2026-05-31 18:33:56 +03:00
Danila Poyarkov 22ff3acdb5 Merge remote-tracking branch 'origin/master' into figma-compat-render-gaps
# Conflicts:
#	CHANGELOG.md
2026-05-31 13:59:19 +03:00
Danila Poyarkov 6ee0e5f9e8 fix(kiwi): preserve imported boolean geometry 2026-05-31 13:28:41 +03:00
Danila Poyarkov f9bd0c7be9 fix(canvas): render complex text fills as outlines 2026-05-30 12:32:55 +03:00
Danila Poyarkov 6ae425ee3b fix(core): address compatibility review findings 2026-05-28 09:57:46 +03:00
Danila Poyarkov 5fcd8e0e9c fix(canvas): support stretched layout grids 2026-05-28 05:39:40 +03:00
Danila Poyarkov 64e8b21949 fix(canvas): render imported page guides 2026-05-27 19:00:23 +03:00
Danila Poyarkov 42fcb37f4d fix(canvas): render imported layout grids 2026-05-27 17:57:41 +03:00
Danila Poyarkov 033326cffb fix(canvas): render raw noise effect fallback 2026-05-27 10:53:12 +03:00
Danila Poyarkov 4224179501 fix(canvas): render derived text decoration styles 2026-05-27 10:11:06 +03:00
Danila Poyarkov 94dd6cf3aa fix(canvas): tune Figma pattern origin 2026-05-26 21:21:49 +03:00
Danila Poyarkov e02782d1b5 test(canvas): record Figma visual oracle diffs 2026-05-26 20:44:18 +03:00
Danila Poyarkov c1532610db fix(canvas): tighten Figma text and pattern fidelity 2026-05-26 18:30:08 +03:00
Danila Poyarkov d208cc326e fix(canvas): render Figma pattern fills 2026-05-26 17:33:45 +03:00
Danila Poyarkov 1e1d78c7f6 fix(canvas): render Figma leading trim 2026-05-26 16:23:59 +03:00
Danila Poyarkov c2a0de5f0c fix(kiwi): support Figma OpenType variant fields 2026-05-26 15:14:23 +03:00
Danila Poyarkov 80daa0d1e9 fix(canvas): render Figma text decoration styles 2026-05-26 13:22:56 +03:00
Danila Poyarkov 84cf18ba80 fix(kiwi): preserve schema paint fields 2026-05-25 09:19:25 +03:00
Danila Poyarkov 5c09ca0299 test(core): stabilize engine unit tests
- Update EffectType schema assertion for modern Figma enum values
- Treat unsupported OpenType font buffers as unavailable
- Skip CJK outline fallback coverage when the LFS font fixture is not materialized
2026-05-25 01:41:59 +03:00
Danila Poyarkov afd571341b fix(ai): render common generated JSX aliases 2026-05-24 12:45:42 +03:00
Danila Poyarkov abd795bd9a chore: format codebase 2026-05-24 12:15:29 +03:00
Danila Poyarkov c337905a02 fix(canvas): preserve text OpenType toggles 2026-05-23 01:43:00 +03:00
Danila Poyarkov f36d6b94b7 fix(canvas): apply affine image fill transforms 2026-05-23 01:31:04 +03:00
Danila Poyarkov 762c86e094 Revert "perf(canvas): defer backing rebuilds while zooming"
This reverts commit 21a79d4cd5.
2026-05-23 01:17:48 +03:00
Danila Poyarkov 21a79d4cd5 perf(canvas): defer backing rebuilds while zooming 2026-05-23 01:17:07 +03:00
Danila Poyarkov 0d5db35147 perf(canvas): bound blend and text mask layers 2026-05-23 01:10:24 +03:00
Danila Poyarkov f07d26c50b perf(canvas): bound effect render layers 2026-05-23 01:01:38 +03:00
Danila Poyarkov 7184bc1b06 Revert "perf(canvas): cap retained backing size"
This reverts commit 7b62c8acca.
2026-05-23 00:45:13 +03:00
Danila Poyarkov 7b62c8acca perf(canvas): cap retained backing size 2026-05-23 00:30:30 +03:00
Danila Poyarkov 8c85d2b953 fix(canvas): improve Figma mask and text fidelity 2026-05-23 00:18:33 +03:00
Danila Poyarkov 3dc0904466 fix(canvas): support luminance masks and tile transforms 2026-05-22 23:44:10 +03:00
Danila Poyarkov 98616c8610 fix(canvas): render Figma-style corner smoothing 2026-05-22 23:05:24 +03:00
Danila Poyarkov 76ee122dee fix(canvas): render smoothed corners and effect blend modes 2026-05-22 22:50:58 +03:00
Danila Poyarkov 0ce57ea0fa fix(canvas): render Figma mask stacks 2026-05-22 21:05:35 +03:00
Danila Poyarkov 8e1d5d40e7 fix(canvas): render Figma blend modes 2026-05-22 21:00:34 +03:00
Danila Poyarkov 11c3cfe73e test(canvas): drop brittle shadow source check
- Rely on behavioral effect tests for stroked container shadow coverage
2026-05-22 14:44:16 +03:00
Danila Poyarkov 8f4f7a47e9 fix(canvas): filter retained backing previews
- Keep stale scene backings available during zoom for smooth interaction

- Draw scaled backings with linear filtering to avoid blocky text artifacts
2026-05-22 14:44:16 +03:00
Danila Poyarkov a49f626e3e fix(canvas): render stroked container shadows from bounds
- Use container bounds for stroked frames with drop shadows

- Keep stroke-geometry shadows for childless stroke-only shapes

- Cover the shadcn dropdown menu case
2026-05-22 13:39:29 +03:00
Danila Poyarkov 1c22b3f7a9 test(canvas): align shadow cleanup checks
- Keep explicit color filter deletion checks compatible with cleanup guards

- Verify text inner-shadow restore balancing through the restore-count loop

Co-authored-by: Joseph Cumines <joeycumines@gmail.com>
2026-05-22 13:17:04 +03:00
Danila Poyarkov f6e31be707 fix(canvas): delete renderer label resources
- Dispose parent outline paint during renderer teardown

- Dispose section and component label fonts with other renderer-owned fonts

- Cover renderer teardown for label resources

Co-authored-by: Joseph Cumines <joeycumines@gmail.com>
2026-05-22 13:15:35 +03:00
Danila Poyarkov 420c61e004 fix(canvas): guard retained backing surfaces
- Fall back to live rendering when CanvasKit cannot allocate the backing surface

- Match retained backing metadata against the current graph preview version

- Cover null-surface and stale-preview backing regressions

Co-authored-by: Joseph Cumines <joeycumines@gmail.com>
2026-05-22 12:51:00 +03:00
Danila Poyarkov 9f67a1086d fix(canvas): restore shadow render state on errors
- Restore canvas save layers and shared paints from shadow effect paths when rendering fails

- Delete temporary paths and color filters through error paths without suppressing failures

- Cover drop-shadow and text inner-shadow cleanup regressions

Co-authored-by: Joseph Cumines <joeycumines@gmail.com>
2026-05-22 12:47:33 +03:00
Danila Poyarkov 0b4fcc9f77 fix(canvas): render shadows from path shapes
- Use polygon, star, and vector paths for shadows when geometry blobs are unavailable
- Apply shadow spread to copied geometry paths without mutating caches
- Clip and cut out inner shadows with the same path geometry

Co-authored-by: Joseph Cumines <joeycumines@gmail.com>
2026-05-22 12:37:00 +03:00
Danila Poyarkov a48b23129f fix(fig): stabilize engine test suite 2026-05-21 14:54:17 +03:00
Danila Poyarkov 213db6373a refactor(kiwi): group fig format internals
- Move codec, NodeChange conversion, and instance overrides under kiwi/fig
- Flatten the codec entrypoint to kiwi/fig/codec
- Keep schema-runtime as the generic Kiwi parser/compiler boundary
2026-05-21 14:54:13 +03:00
Danila Poyarkov 54310308dc refactor(kiwi): split schema text from runtime
- Move the Kiwi runtime under schema-runtime to match folder naming
- Load the Figma schema from a .kiwi text asset with static validation
- Assert Figma clipboard-derived field numbers for fragile layout/text fields
2026-05-21 14:54:12 +03:00
Danila Poyarkov 957db338ba fix(fig): sync lazy instance override clones
- Re-propagate resolved overrides after lazy instance population
- Preserve Figma showShadowBehindNode on effects
- Mask unfilled stroke shadows behind their fill geometry
2026-05-20 13:12:16 +03:00
Danila Poyarkov 4603bbf81b fix(canvas): shadow stroked shapes without fills
- Use stroke geometry for drop shadows on unfilled stroked nodes
- Avoid filling the interior of outline-only image icons
2026-05-20 12:52:21 +03:00
Danila Poyarkov 779e0e493f fix(mcp): improve agent-rendered JSX 2026-05-19 17:57:43 +03:00
Danila Poyarkov 26fb9b6a0b fix(canvas): render background blur as backdrop 2026-05-19 13:07:46 +03:00
Danila Poyarkov f466946c1c fix(canvas): render derived text underlines 2026-05-18 20:28:15 +03:00
Danila Poyarkov eb7a663c1c fix(fig): preserve scene node defaults during overrides 2026-05-18 19:50:34 +03:00
Danila Poyarkov f3649550f2 chore: enable stricter oxlint rules 2026-05-18 18:58:30 +03:00
Danila Poyarkov 6d8920a496 fix(canvas): tune derived glyph coverage 2026-05-18 18:21:26 +03:00
Danila Poyarkov 70114ef6de fix(io): preserve one-pixel export fringes 2026-05-18 14:00:23 +03:00
Danila Poyarkov ff85febddb fix(canvas): render derived text without fonts 2026-05-18 13:36:03 +03:00
Danila Poyarkov 4195f13fad fix(fig): preserve imported hug container bounds 2026-05-18 04:20:22 +03:00
Danila Poyarkov dd05c54d68 fix(io): trim transparent page exports 2026-05-18 04:02:32 +03:00
Danila Poyarkov a0ba8bda3a fix(canvas): orient linear gradients like Figma 2026-05-18 03:21:16 +03:00
Danila Poyarkov f365ccf23b fix(canvas): ellipsize frame labels 2026-05-18 03:11:48 +03:00
Danila Poyarkov 7b428b58aa fix(canvas): ellipsize component labels 2026-05-18 03:11:47 +03:00
Danila Poyarkov 383e03defa fix(canvas): hide nested component labels 2026-05-18 03:11:47 +03:00
Danila Poyarkov 2348966879 fix(fig): snap derived text baselines 2026-05-18 01:47:33 +03:00
Danila Poyarkov 5d71e810b6 fix(text): preserve requested font weights 2026-05-17 20:40:39 +03:00
Danila Poyarkov 0772354bff test(canvas): assert boolean matrix cells 2026-05-17 19:16:56 +03:00
Danila Poyarkov d48e3e5335 test(canvas): verify boolean visual matrix 2026-05-17 18:41:32 +03:00
Danila Poyarkov 92328a21d8 build(lint): enforce shared black color constants 2026-05-17 18:05:07 +03:00
Danila Poyarkov 2eab8f9a50 test(canvas): add boolean shape visual matrix 2026-05-17 18:00:35 +03:00
Danila Poyarkov 40d8a54046 feat(canvas): render boolean operation paths 2026-05-17 17:54:19 +03:00
Danila Poyarkov f5bcf92ce4 test(canvas): cover scene cache recovery after drag preview 2026-05-17 02:41:13 +03:00
Danila Poyarkov 6954ff3ce7 refactor(jsx): replace hand-rolled Tailwind mapping with twirlwind
- Use twirlwind for CSS-to-Tailwind class conversion instead of
  450 lines of manual lookup tables and formatting functions
- Build a CSS style object from scene node properties, pass to twirl()
- Remove design-jsx/tailwind.ts (pxToSpacing, colorToTwClass, etc.)
- Remove unused Tailwind-specific helpers from jsx/helpers.ts
- Update tests for twirlwind's v4-native output format
2026-05-17 00:54:53 +03:00
Danila Poyarkov bf636155cf test(engine): stabilize render and heavy fig tests 2026-05-16 13:39:45 +03:00
Danila Poyarkov 6eb50affc4 test(engine): move root tests into domains 2026-05-16 12:30:27 +03:00
Danila Poyarkov 834f369882 test(canvas): expect pending fonts to skip text 2026-05-14 23:46:36 +03:00
Danila Poyarkov 419c77d8de fix(canvas): wait for exact font styles 2026-05-13 04:47:32 +03:00
Danila Poyarkov 2f39ecaa4d fix(canvas): cull nested nodes in world space 2026-05-13 03:56:04 +03:00
Danila Poyarkov 24629610a4 test(render): split canvas effect coverage
- Move canvas effect renderer tests into focused files by behavior
- Share renderer and canvas mocks from effects/helpers.ts
- Remove canvas effects from max-lines exceptions
2026-05-06 13:05:51 +03:00
Danila Poyarkov 21a3337a96 chore(tests): enforce no-empty-function
- Replace remaining empty mock callbacks with explicit undefined returns
- Remove the last no-empty-function test exceptions
- Validate check and affected renderer/text tests
2026-05-06 12:40:42 +03:00
Danila Poyarkov 657188b097 chore(tests): enforce non-null assertion rule
- Remove remaining test non-null assertions and delete the broad test override
- Add explicit guards for canvas, design panel, auto-layout, SVG, Kiwi, hit-test, scene graph, text editor, and clipboard tests
- Validate check plus affected engine and E2E coverage
2026-05-06 11:44:18 +03:00
Danila Poyarkov 17857f912f chore(tests): guard render text and mutation lookups
- Replace non-null assertions in canvas text rendering and mutation undo tests
- Use expectDefined for CanvasKit handles and getNodeOrThrow for graph nodes
- Validate check and affected engine tests
2026-05-06 11:27:24 +03:00
Danila Poyarkov 9e4d7bee5d chore(lint): fix unused test code
- Remove stale imports and unused locals from split engine and e2e tests
- Drop the targeted no-unused-vars test allowances
- Keep check and affected test suites warning-free
2026-05-06 02:53:19 +03:00
Danila Poyarkov a63ac93a3d chore(lint): enforce type imports in tests
- Remove the broad test exception for consistent type imports
- Mark test-only core and helper imports as type imports where appropriate
- Keep targeted validation for affected engine tests
2026-05-06 02:35:57 +03:00
Danila Poyarkov 654aa421e9 chore(lint): reduce test rule exceptions
- Re-enable duplicate imports, radix, prefer-const, nested ternary, and silent catch checks in tests
- Drop the redundant spec-file override covered by the broader test override
- Fix the remaining test offenders surfaced by the stricter rules
2026-05-06 02:31:07 +03:00
Danila Poyarkov c3f5189f8f style: tighten import grouping
- Configure oxfmt custom import groups for workspace, app, package, and test aliases
- Keep type imports grouped with their matching source category instead of one global tail group
- Expand the format script to cover formatter config, Vite files, and scripts
2026-05-06 02:22:08 +03:00
Danila Poyarkov 930cf39216 test: organize prefixed test files into folders
- Move top-level engine and e2e prefixed test files under domain folders
- Update fixture path helpers after moving render and pen tests
- Add lint coverage to prevent new top-level prefixed test files
- Refresh testing docs for the new fig and layout paths
2026-05-06 02:13:18 +03:00
Danila Poyarkov e42dffa003 chore(lint): consolidate oxlint config
- Merge structural lint rules into oxlint.json and remove the second config file
- Add #tests/* path aliases and rewrite deep test helper imports
- Ban new deep parent-relative imports through the shared lint config
2026-05-06 02:05:02 +03:00
Danila Poyarkov 8a5c4847fd test(render): split JSX render coverage
- Move render.test.ts into focused JSX builder, tree render, grid layout, and text prop files
- Preserve all 56 render tests and keep scoped non-null assertion enforcement on the new folder
- Validate the split with the render/jsx test folder and the full check command
2026-05-06 01:22:27 +03:00