Commit graph

276 commits

Author SHA1 Message Date
Anton A S d688d04bef Address PR #94 review: dedup, replace hand-rolled code, cleanup
- Replace calc.ts (150 lines) with expr-eval library
- Replace rgbLuminance with culori wcagLuminance
- Use @iconify/utils iconToSVG for viewBox normalization in iconify.ts
- Extract describe-shared.ts: findAncestorBackground, looksLikeButton, CONTAINER_TYPES
- Extract icon-render.ts: shared createIconFromPaths for create.ts and renderer.ts
- Inline codegen prompt as TS constant, remove runtime fs.readFile from MCP server
- Fix blur effect color alpha (a:1 → a:0)
- Encapsulate module-level mutable state in src/ai/tools.ts into RunState class
- Add network check to iconify tests (skip gracefully offline)
- Remove Caddyfile.dev and components.d.ts from tracking, add to .gitignore
2026-03-13 14:46:18 +03:00
Anton A S 68b5cfcea1 Fix .fig export roundtrip: variable GUIDs colliding with document
Variable/collection IDs (e.g. 'VariableCollectionID:42:1') parsed via
stringToGuid produced NaN sessionID, which Kiwi encodes as 0 — same
GUID as the DOCUMENT node. This overwrote DOCUMENT in changeMap during
re-import, causing all pages to be lost.

Fix: generate unique GUIDs for collections, variables, and modes via
localIdCounter instead of parsing string IDs. Pass varIdToGuid map
through to serializeVariableBindings for consistent references.
2026-03-13 13:27:15 +03:00
Anton A S 48abcf10e6 Fix all typecheck and lint errors (79 errors → 0)
Remove duplicate FigmaNodeProxy class from figma-api.ts — use the one
from figma-api-proxy.ts. The local class shadowed the imported proxy,
causing TS2440/TS2739 Symbol conflicts across all tool files.

Sync proxy's layoutSizingHorizontal/Vertical setters with the local
version (adds layoutGrow toggle on FILL, adds _parentLayout helper).

FigmaAPI now implements NodeProxyHost explicitly.

Other fixes:
- kiwi-serialize: safeColor returns Color (not {a?: number})
- chat-debug: eliminate nested ternary
- use-canvas-input: formatting

figma-api.ts: 1295 → 379 lines (under 600-line limit)
2026-03-13 12:42:07 +03:00
Anton A S 933511f43e Improve AI design quality: auto-infer flex, reduce false warnings, prompt patterns
Renderer: Frames with 2+ children auto-infer flex="col" when no explicit
flex/grid prop is set. Children with x/y keep ABSOLUTE positioning.
Extract shouldEnableAutoLayout() to stay under complexity limit.

Describe issues: suppress false positives that waste AI steps—
- checkNestedFlexWithoutFill: skip when parent uses justify=between/center
- checkGrowInHug/checkHugCollapse: skip when node will get concrete size
  from its own parent (grow>0, STRETCH, or effectively filling)
- checkSiblingHeightConsistency: skip when parent uses items=center

System prompt: add mandatory flex rule for multi-child frames,
tab-bar pattern, remove 'nested flex may collapse' from fix list.
2026-03-13 12:21:10 +03:00
Anton A S b5fd31b557 Fix Kiwi encodeColor crash on colors missing alpha field
Defensive safeColor() guard at serialization boundary — fills, strokes,
effects, and gradient stops all go through it before Kiwi encoding.
Also fix const→let for execResult in AI adapter (reassigned by step budget).
2026-03-13 11:48:43 +03:00
Anton A S d46b4b11a4 Fix layout axis mismatch, reduce describe noise, add bail-out strategy
Engine:
- layoutSizingVertical/Horizontal setters now use child's own layout
  direction (not parent's) to map primary/counter axis — matches Yoga
- FILL on cross-axis uses alignSelf:Stretch instead of flexGrow
  (flexGrow only works on main axis, caused even distribution bug)
- FILL on main axis auto-sets layoutGrow=1 for leaf nodes

This fixes the 20-step debugging spiral where AI set FIXED heights
on HORIZONTAL children inside VERTICAL parent but Yoga ignored them
(wrote primaryAxisSizing via parent axis, Yoga read counterAxisSizing
via child axis — never saw the FIXED value).

Describe noise reduction:
- Suppress 'absolutely positioned inside flex' — intentional for
  decorative overlays (selection handles, badges)
- Suppress 'justify=between with HUG' when node fills parent width
- Suppress 'fixed size + grow' when sizing is FILL (standard pattern)
- 'Inconsistent padding' requires 3+ siblings (not 2)

Prompt:
- Bail-out: delete and re-render after 2 failed set_* attempts
- eval restricted to operations, not layout debugging
2026-03-13 10:21:46 +03:00
Anton A S 417aaedcb3 Fix set_layout padding reset, enforce final describe after fixes
- set_layout: only change spacing/align/padding when explicitly passed.
  Previously spacing/align/padding defaulted to 0/MIN, wiping existing
  values when AI only wanted to change one property.
- search_icons: client-side slice to enforce limit (API ignores small
  limits, returned 31 results for limit=1)
- system-prompt: issues from describe are mandatory fixes, not advisory.
  Final describe after set_* calls is required workflow step.
2026-03-13 10:20:57 +03:00
Anton A S 1454afc036 Optimize AI token usage: ~61% reduction in per-step context
- System prompt: 19.5KB → 6.3KB (67% smaller), removed redundant
  rules, condensed spacing/typography/layout docs
- Tool tiers: CORE_TOOLS (34) vs EXTENDED_TOOLS (64), AI chat uses
  core only (~2.8K tokens vs ~6.5K). ALL_TOOLS for MCP/CLI unchanged.
- <Icon> element in render JSX: <Icon name="lucide:heart" size={20}
  color="#FFF" /> fetches and renders inline. Eliminates the
  search→fetch→insert×N→delete×N chain (~8 tool calls per design).
- search_icons default limit 20 → 5 (~300 fewer tokens per query)
- get_page_tree returns compact tree (id, type, name, w, h) instead
  of full recursive nodeToResult dump
- eval returns { ok: true } instead of null, saves a follow-up
  describe call
- renderTree/renderJSX/renderTreeNode now async to support Icon fetch
2026-03-13 10:20:56 +03:00
Anton A S fed6ea5734 Fix icon SVG primitives, harden describe issues, ban justify=evenly
- iconify: convert <circle>, <rect>, <ellipse>, <line>, <polygon>,
  <polyline> to path d-strings. Fixes lucide:search (missing circle),
  lucide:user (missing head), lucide:mail (missing envelope), etc.
- describe: show justify/items/sizing in layout description, detect
  icon role, recurse issues into children at every depth level
- describe-issues: split layout checks into describe-layout-issues.ts
- New checks: fill without flex parent, absolute in flex, nested flex
  without fill, duplicate sibling names, stroke color/weight mismatch,
  circular frame without clip, excessive nesting, same fill as parent,
  image placeholder without content, sibling height inconsistency,
  text wrapping detection, alignment issues (between+HUG, between<2,
  stretch with all fixed, equal children packed without gap)
- system-prompt: remove justify="evenly" — not supported
2026-03-13 10:20:55 +03:00
Anton A S 160d5474c9 Fix lint errors: eliminate all any casts, fix complexity and nested ternaries 2026-03-13 10:20:54 +03:00
Anton A S 9daf7ce6ea Fix icon vector positioning: zero x/y after appendChild 2026-03-13 10:18:47 +03:00
Anton A S 4d3a5341cd Fix lint errors: split describe.ts, fix TS errors, clean up type guards
- Extract issue detection from describe.ts into describe-issues.ts (609→167 lines)
- Fix graph not in scope in detectStructuralIssues (TS2304)
- Fix child.characters → child.text (TS2339, SceneNode uses text not characters)
- Remove unused DEFAULT_TEXT_LUMINANCE
- Split detectLayoutIssues (complexity 86→<20) into 8 focused helpers
- Fix non-null assertions in chat-debug.ts
- Fix nested ternary in chat-debug.ts
- Fix unnecessary optional chains in use-chat.ts, export-jsx.ts
- Use Map for MATH_FUNCTIONS in calc.ts (fixes no-unnecessary-condition)
- Use Partial<Record> for SKIP_ARGS and TokenEntry.values (honest nullable types)
- Remove redundant null checks in codegen.ts type guards

Errors: 32 → 1 (pre-existing applyTextOverrides complexity)
Warnings: 47 → 42
2026-03-13 10:18:45 +03:00
Anton A S 774cf6b90a Address PR #94 review feedback
- Remove docs/mcp-codegen-plan.md (internal planning doc, not for repo)
- Remove lol.fig test fixture left in repo root
- Fix inline type imports in codegen.ts — use top-level SceneGraph import
- Move get_codegen_prompt out of core/ALL_TOOLS into MCP server only
  (core must be browser-safe, node:fs is not available in browser)
- Remove skill from packages/mcp/skills/ — content saved to #97
- Slim down codegen.md overlap note (canonical reference stays there)

Closes review comments. Skill npm publishing tracked in #97.
2026-03-13 10:18:44 +03:00
Anton A S dd91b682d1 Fix icon positioning after insert, add emoji guidance to system prompt
- Zero icon frame x/y after appendChild to parent — fixes icons appearing
  at viewport origin instead of inside their parent container
- Add name→names fallback to insert_icon for backward compatibility with
  single-icon calls (name param still works, returns legacy format)
- Add emoji section to system prompt: emoji show as □ tofu on canvas,
  use insert_icon for UI iconography, emoji only in user content/reactions
- Strengthen flex requirement in layout docs: every parent with fill
  children MUST have flex='col'|'row'
2026-03-13 10:18:43 +03:00
Anton A S dec0a67e4f Batch insert_icon and search_icons, fetch_icons prefetch tool 2026-03-13 10:18:41 +03:00
Anton A S 733321fbb5 Add step budget warnings and Continue button for AI chat
- Track per-run step count, warn AI when 5 steps remain via _warning
  field injected into tool results
- Show 'Continue' button in chat when step limit (50) is exhausted
- Add step budget section to system prompt so AI plans ahead
- Reset run step counter on each new agent call via prepareCall
2026-03-13 10:18:20 +03:00
Anton A S 530b56acc5 Add calc tool for layout arithmetic, make set_layout direction optional
- Add calc tool (recursive descent parser) so AI uses exact math
  instead of error-prone mental arithmetic for layout sizing
- Make set_layout direction param optional — keeps current layoutMode
  when only changing spacing/padding (eliminates ~50% of NO-OP mutations)
- Add set_layout to SKIP_ARGS in NO-OP detection
- Update system prompt: calc in workflow, mandatory before rendering
  into fixed-size containers
2026-03-13 10:17:50 +03:00
Anton A S 94160168ec AI chat: debug log with tool loop detection, prompt caching, system prompt tuning
Debug log (chat-debug.ts, ai-adapter.ts):
- Before/after node snapshots detect no-op mutations
- Duplicate tool call detection (mutating tools only)
- Per-step token usage with cache hit/miss breakdown
- TOKEN USAGE & CACHING section with hit rate and cost savings

Prompt caching (use-chat.ts):
- Enable Anthropic automatic caching via providerOptions for
  anthropic, anthropic-compatible, and OpenRouter anthropic models
- Track step usage via onStepFinish with cache read/write tokens

Provider fixes:
- anthropic-compatible now treated as custom provider in ChatInput
  (shows customModelID instead of stale modelID from previous provider)

System prompt:
- Sizing rule: calculate available space before rendering skeleton
- Concise final summary (2-3 lines, no section-by-section listing)
2026-03-13 10:17:47 +03:00
Anton A S 6c8ba63aa7 Remove duplicate estimateTextSize and GLYPH_WIDTH_FACTOR declarations 2026-03-13 10:17:20 +03:00
Anton A S ba4333d798 Fix fill text MeasureFunc constraint, strengthen nested flex prompt
layout.ts: When text fills width (stretch in col parent), MeasureFunc
now uses Yoga's constraint width directly instead of clamping to
the 100px default fixedWidth.

prompt: Emphasize that ALL intermediate flex containers need w="fill",
not just the first level. grow inside HUG = zero width.
2026-03-13 10:17:18 +03:00
Anton A S 6ff1560621 Fix text w="fill" stuck at 100px in flex="col" parent
configureTextLeaf for HEIGHT mode was calling yogaChild.setWidth(child.width)
unconditionally, which locked text at the 100px default. This blocked
Yoga's alignSelf:stretch from expanding text to the parent's cross axis.

Now skips setWidth when text has layoutAlignSelf=STRETCH in a vertical
parent (w="fill" case). Same fix for the headless fallback path.

Test: text with w="fill" in flex="col" stretches to 260px (300 - 2×20 pad).
2026-03-13 10:17:16 +03:00
Anton A S 8300ebb807 Fix text w="fill" exported as w={computed}, add wrap rowGap check
export-jsx: Text with layoutAlignSelf=STRETCH (w="fill" in col parent) or
layoutGrow>0 (w="fill" in row parent) was emitting w={computedPx} instead
of letting the fill/grow detection below handle it. Now skips fixed w
emission when text stretches or grows.

describe: wrap layout without rowGap flagged — wrapped rows stick together.

prompt: wrap always needs rowGap.

Tests: 2 new round-trip tests for text w="fill" in col and row parents.
2026-03-13 10:17:15 +03:00
Anton A S ce5433da86 Add progress bar pattern, divider placement rules, w+grow conflict check
Prompt:
- Progress bar pattern with grow bar bg + overflow hidden
- Don't mix w={N} and grow={N} on same node
- Don't wrap Text in Frame just for sizing
- Vertical dividers must be inside flex=row with h=fill
- Horizontal dividers in flex=col

Describe:
- w + grow conflict detection
- Child Rectangle overflowing parent without clipping
- Vertical divider in column layout (wrong parent)
- Horizontal divider in row layout (wrong parent)
2026-03-13 10:17:14 +03:00
Anton A S 3904b987d8 Add text overflow and wrap layout rules to prompt and describe
Prompt:
- Text inside fixed-width containers must have explicit w
- Wrap layout column width formula: count chars × 0.6 × fontSize
- Don't guess widths in wrap grids

Describe:
- Text wider than parent available width flagged with fix suggestion
2026-03-13 10:17:12 +03:00
Anton A S 633469f61f Add text props (lineHeight, letterSpacing, textCase, textDecoration, maxLines), spacing checks, update prompt
Renderer + export-jsx:
- lineHeight={N}, letterSpacing={N}
- textDecoration="underline"|"strikethrough"
- textCase="upper"|"lower"|"title"
- maxLines={N} (auto-enables truncation), truncate (standalone)

Describe:
- Spacing consistency checks (inconsistent padding/gap among siblings)
- Off-grid spacing values (not on 4px grid)
- Gap larger than padding warning
- Corner radius: child radius vs parent − padding rule

Prompt:
- Complete props reference with explicit "nothing else exists" guard
- Typography: type scale, line height, letter spacing, hierarchy rules
- Spacing: 4px grid, Gestalt proximity, consistency rule
- All new text props documented

Tests:
- 7 round-trip tests for new text props (render → SceneNode → export JSX)
2026-03-13 10:17:11 +03:00
Anton A S 5f74908a35 Add typography and corner radius rules to prompt and describe
Prompt — full typography guide:
- Type scale (6-8 sizes, ratio ~1.25)
- Weight hierarchy (max 2-3 per design)
- Color hierarchy for light/dark backgrounds
- Line height rules (tight for headings, relaxed for body)
- Line length (45-75 chars), spacing between text elements
- Uppercase only for small labels/overlines
- Alignment guidelines

Corner radius — nested radius rule:
- inner = outer − padding, 0 if padding ≥ outer

Describe checks:
- Child radius exceeding parent or not matching inner rule
- Uppercase text at sizes > 13px
2026-03-13 10:17:10 +03:00
Anton A S 927bee4270 Add layout and visibility checks to describe tool, fix prompts
describe now catches:
- Text without color (invisible — fills is empty)
- Shapes without fill/stroke (invisible)
- Icon-sized frames with no visible content
- Dark text on dark background
- grow inside HUG parent (no effect)
- Children overflowing fixed parent
- HUG container where all children use grow/stretch (collapses)
- Cross-axis overflow

Prompt changes:
- Text always requires color="#hex" — no fill = invisible
- Shapes/icons always need bg or stroke
- Button icons highlighted as #1 invisible element source
2026-03-13 10:17:09 +03:00
Anton A S 036f486571 Fix layout blowup from 100×100 text fallback, skip compute for read-only AI tools
Two fixes:

1. layout.ts: Add estimateTextSize() fallback (~0.6 × fontSize per char)
   for when MeasureFunc returns null (font not loaded) or CanvasKit is
   unavailable. Previously fell back to child.width/child.height which
   defaults to 100×100 for new nodes, blowing up every HUG container.

2. ai/tools.ts: Only call computeAllLayouts + requestRender + push undo
   for mutating tools (def.mutates). Read-only tools (describe, get_jsx,
   node_bounds, etc.) were triggering full layout recompute on every call,
   wasting cycles and potentially destabilizing layouts during AI sessions.
2026-03-13 10:17:07 +03:00
Anton A S 31dbe9690c Add MCP codegen pipeline: tools, prompt, and plan 2026-03-13 10:17:06 +03:00
Danila Poyarkov 9f98833495 Remove worker timeout — large files need unbounded time 2026-03-13 10:03:41 +03:00
Danila Poyarkov 353c867d03 Add no-silent-catch lint rule, fix silent failures
- New oxlint rule: open-pencil/no-silent-catch — errors on empty catch blocks
- Replace all 8 empty catch blocks with console.warn() logging
- Add worker timeout (30s) and main-thread fallback for .fig parsing
- Fix null crash in renderer filter/picture cache cleanup
- Buffer copy before worker transfer for safe fallback
2026-03-13 10:03:12 +03:00
Anton A S 2a740a01ff Add subpath exports to @open-pencil/core
12 domain-specific subpath exports for targeted imports:
scene-graph, kiwi, tools, renderer, render, rpc, figma-api,
canvaskit, layout, color, render-image, profiler.

- Create kiwi/index.ts barrel (codec, fig-file, fig-import, protocol)
- Route main index.ts kiwi re-exports through the new barrel
- Add exports map + publishConfig mirror for npm consumers
- Mark package as sideEffects: false for tree-shaking
- Document subpath exports in AGENTS.md
2026-03-12 22:10:36 +03:00
Danila Poyarkov b00af4c98f Fix text rendering with wrong fonts on file open
- Load all font weights including default family (Inter 500/600/700 were skipped)
- Fix weightToStyle mapping: 400 → Regular, not Medium
- Block render loop during file loading to prevent stale fallback renders
- Prefer Figma textPicture when available over buildParagraph
- Clear textPicture when text properties change
- Await font loading before first render on file open and page switch
2026-03-12 22:05:59 +03:00
Danila Poyarkov b3cbf227e6
Fix per-character text fill colors and vector region loop direction (#95)
* Fix per-character text fill colors and vector region loop direction

* Replace rotation handle with corner rotation zones

Remove the rotation handle circle/stem above the selection bounding box.
Rotation is now triggered from corner zones outside resize handles,
matching Figma's behavior. Cursor uses native CSS directional arrows
(n-resize, ne-resize, etc.) that rotate with the node, avoiding
cross-platform SVG cursor issues.
2026-03-12 18:28:04 +03:00
Danila Poyarkov 1b1a04a596 Use only Figma clipboard format, fix textAutoResize and autoRename serialization
Drop the OpenPencil clipboard format from the editor — Figma's Kiwi
format is the single clipboard path now. Fix the remaining roundtrip
losses:
- textAutoResize: was hardcoded to WIDTH_AND_HEIGHT, now uses actual value
- autoRename: now serialized (was always defaulting to true on import)
2026-03-12 17:27:10 +03:00
Danila Poyarkov 9771917d99 Compress OpenPencil clipboard data with deflate, put lossless format first
Browser clipboard has size limits — the 4MB uncompressed OpenPencil
format was being silently truncated, leaving only the lossy Figma
format. Now:
- Deflate-compress the JSON payload (3.94MB → 597KB for gold-preview)
- Put OpenPencil format first in the HTML so it survives truncation
- Parse side tries inflate first, falls back to raw for compat
2026-03-12 16:57:22 +03:00
Danila Poyarkov 30462b2b1e Fix Figma clipboard roundtrip: clipsContent, constraints, arcData, strokes, layoutAlignSelf
Properties lost during copy→paste via Figma Kiwi format:
- clipsContent: frameMaskDisabled now set for all node types, not just FRAME/GROUP
- horizontalConstraint/verticalConstraint: now serialized (SCALE, CENTER, etc.)
- arcData: now serialized (startingAngle, endingAngle, innerRadius)
- strokeCap, strokeJoin, strokeMiterLimit, dashPattern: now serialized
- layoutAlignSelf: now serialized (STRETCH was lost, defaulting to AUTO)
2026-03-12 15:57:32 +03:00
Danila Poyarkov e97a4cfda4 Fix lint warnings: eliminate any types, split oversized files
- Replace all no-explicit-any with proper types or justified eslint-disable
- Extract scene-graph-hit-test.ts (hit testing), scene-graph-instances.ts
  (component/instance sync) from scene-graph.ts
- Extract renderer/text.ts (text rendering), renderer/pen-overlay.ts
  (vector pen tool overlay) from renderer.ts
- Extract figma-api-proxy.ts (FigmaNodeProxy class) from figma-api.ts
- Extract kiwi-convert-overrides.ts from kiwi-convert.ts
- Add eslint-disable with justification for remaining large class files
- Fix unused imports from splits
- Warnings: 42 → 2 (only unavoidable no-implied-eval in vendored code)
2026-03-12 15:13:30 +03:00
Danila Poyarkov 3b32b4d80c
Perf: 5.4× faster file open, worker-based parsing, instance index (#96)
* Perf: cache label collection, offload .fig compression to worker

Label cache: collect sections/components once per scene change,
filter by viewport on each frame. Eliminates full tree walk during
pan/zoom (~17ms/frame → <1ms on large files).

Export worker: move fflate compression off the main thread to prevent
frame drops during save (451ms+ → non-blocking).

* Perf: worker-based .fig parsing, instance index, non-blocking font loading

- Offload .fig parsing (unzip + Kiwi decode) to a Web Worker
- Add instance index (componentId → Set<nodeId>) for O(1) getInstances()
- Defer graph event subscription during file open to skip redundant syncs
- Make font loading non-blocking — render immediately, load fonts in background
- Copy image buffers before worker transfer to prevent detached ArrayBuffer crash
- Show toast on font load failure and file open errors
- Cache failed Google Fonts families to avoid repeated network requests
- Fix missing ref import in FillPicker
- Yield to UI between parse and layout for responsive loading spinner
2026-03-12 14:49:28 +03:00
Danila Poyarkov 6072ce57c1 Fix lint warnings: non-null assertions, useless spreads, unused code, type safety
- Replace non-null assertions with guards/optional chaining
- Replace useless spreads with Array.from() where mutation-safe
- Remove unused imports, variables, and functions
- Fix no-base-to-string with proper type narrowing
- Fix sort without comparator, new Array() pattern
- Prefix unused parameter with underscore

Warnings: 76 → 42 (remaining are max-lines, justified any casts, vendored code)
2026-03-11 13:33:54 +03:00
Danila Poyarkov 509520f273 Add MiniMax AI provider support 2026-03-11 13:26:32 +03:00
Dipankar Shaw c4db9ec165
Add Z.ai support to AI providers and chat functionality (#90) 2026-03-11 13:25:15 +03:00
Anton Soldatov 37da83aec3
Image drag-and-drop, clipboard paste, and paste-at-cursor (#92)
* Image drag-and-drop and clipboard paste onto canvas

* Paste images at cursor position, track canvas cursor in state

* Paste nodes at cursor position (Figma and internal clipboard)

* Fix review: center images at cursor, fix hasImageFiles, clean up hash/undo

* Paste at viewport center when cursor is outside canvas

* Fix Figma paste positioning: center nodes at cursor after import

* Image support: clipboard, export, drag-and-drop, paste, renderer, tools, UI

- Clipboard: embed image bytes (base64) in OpenPencil clipboard payload so
  copy/paste between documents preserves image fills
- Fig export: write images/ folder to .fig zip (both fflate and Tauri paths)
- Tauri: extend build_fig_file to accept image entries
- Renderer: implement CROP (with imageTransform) and TILE (TileMode.Repeat)
  scale modes, fix FIT to center the fitted image
- FigmaAPI: add createImage(bytes) with sync FNV-1a hash
- Tools: add set_image_fill tool for AI/MCP
- FillPicker: replace placeholder with file picker, preview, scale mode selector
- Drag-and-drop: new use-image-drop composable creates image nodes from dropped files
- Paste: keyboard paste handler detects image clipboard items
- Shared utils: extract hashImageBytes and getImageDimensions to src/utils/image.ts

* Add image tests and Yjs image sync for collaboration

Tests (18 new):
- FigmaAPI.createImage: deterministic hash, storage, format
- set_image_fill tool: all scale modes, error handling, storage
- Clipboard roundtrip: image bytes preserved, multiple images, children
- Fig export/import: zip contains images/, full round-trip

Collab:
- Add yimages Y.Map to sync graph.images via Yjs
- Observer applies remote image adds/deletes to local graph
- syncNodeToYjs pushes referenced image data alongside node props
- syncAllNodesToYjs bulk-syncs all images on room share

* Fix FillPicker: remove deleted utils/image import, use SHA-1 inline

* Extract storeImage() on editor store, use in FillPicker and placeImageNode

* Unify image hash: use sync FNV-1a everywhere, export computeImageHash

Editor store's hashBytes (async SHA-1) produced different hashes than
FigmaAPI.createImage (sync FNV-1a) for the same bytes. This meant
drag-and-drop images couldn't be deduplicated against AI tool images.

Replace hashBytes with computeImageHash from core. storeImage() is
now sync.

* Skip drawing IMAGE fills when image data is missing

When pasting from Figma, image fills reference a CDN hash but no pixel
data is included in the clipboard. Previously this rendered as a solid
black rectangle because applyImageFill bailed without setting a shader,
leaving stale paint state.

applyFill now returns false when the fill can't be applied, and callers
skip the draw call. The node still exists with the correct imageHash —
if the image data is later provided (e.g. via file re-open), it will
render correctly.

* Warn when Figma paste has missing image data

Show amber warning toast when pasted nodes reference image fills
without available bytes (Figma clipboard limitation).

Add 'warning' toast variant with tailwind-variants, extract toast
styles to src/components/ui/toast.ts.

* Use useFileDialog and useObjectUrl in FillPicker

Replace manual file input ref, click(), createObjectURL/revokeObjectURL
with vueuse composables. Remove hidden <input type=file> from template.

---------

Co-authored-by: Danila Poyarkov <dev@dannote.net>
2026-03-11 07:52:04 +03:00
Anton A S 9caa89e6f8 Fix text layout without CanvasKit: estimate size instead of 100×100 default
Text nodes with textAutoResize=WIDTH_AND_HEIGHT kept their 100×100
default SceneNode size when MeasureFunc was unavailable (no CanvasKit),
blowing up every HUG container.

Add fallback estimator (~0.6 × fontSize per char) in layout.ts so
headless layout produces sane sizes.

DO NOT change textAutoResize defaults in renderer.ts without testing
headless layout — see comments there and in layout.ts.
2026-03-10 22:59:15 +03:00
Danila Poyarkov 0d1f42f0b1
Fix CJK text garbled when font unavailable (#89)
* Fix CJK text garbled when font unavailable (#69)

Always use buildParagraph for text rendering when fonts are loaded,
even when the node's specific font isn't available. The paragraph
shaper falls back to the CJK font in fontFamilies, instead of the
previous drawText fallback which used Inter (no CJK glyphs).

* Add renderText unit tests for CJK font fallback behavior

* Add CJK visual regression test with Noto Sans SC fixture

- Download NotoSansSC-Regular.ttf as LFS-tracked test fixture
- Add setCJKFallbackFamily() export for headless/test use
- Visual test verifies CJK text renders through buildParagraph fallback
  when node font is unavailable (the exact PR #89 scenario)
- Assert dark pixel count > 500 to distinguish real glyphs from tofu
2026-03-10 16:58:57 +03:00
Danila Poyarkov 75cc4bf7f1 Fix CJK text garbled when font unavailable (#69)
Always use buildParagraph for text rendering when fonts are loaded,
even when the node's specific font isn't available. The paragraph
shaper falls back to the CJK font in fontFamilies, instead of the
previous drawText fallback which used Inter (no CJK glyphs).
2026-03-10 15:43:23 +03:00
Anton Soldatov e939025f7d
Fix auto-layout overflow, grid layout, AI chat UX (#86)
* Fix auto-layout overflow: MeasureFunc, min/max, absolute positioning, text wrap

Layout engine (layout.ts):
- Use Yoga MeasureFunc for text nodes instead of static pre-measurement,
  so text wraps correctly when width is determined by flex layout
- Add min/max width/height constraint support (minWidth, maxWidth, etc.)
- Translate counterAxisAlignContent to Yoga setAlignContent for wrap layouts
- Extract configureFlexContainer to share between root and nested frames

JSX renderer (render/renderer.ts):
- Auto-set layoutPositioning ABSOLUTE for elements with x/y inside auto-layout
- Fix text auto-resize: set HEIGHT when text fills parent in auto-layout

Text measurer (renderer/renderer.ts):
- Accept optional maxWidth parameter for constraint-based measurement

* AI prompt: prefer describe over export_image for post-render verification

* Strip TS casts from AI-generated JSX before sucrase parse

AI models sometimes emit `as any`, `as const` etc. in JSX props.
Sucrase with jsx-only transform can't handle these, causing parse errors.

- Strip TypeScript cast expressions before passing to sucrase
- Add forbidden patterns to AI prompt: as any, template literals for
  sizes, Math.random()

* Cache text MeasureFunc results to avoid repeated buildParagraph calls

Yoga calls MeasureFunc multiple times per node during calculateLayout.
Each call was creating a full CanvasKit Paragraph — expensive for layouts
with many text nodes. Cache by rounded constraint width.

* AI chat: fix provider settings popover, add max output tokens, fix paste in chat input

- Fix ProviderSettings popover not visible (remove tooltip/popover trigger
  conflict, add collision-padding, isolate z-[51])
- Add configurable max output tokens (default 16384) in provider settings
- Pass maxOutputTokens to ToolLoopAgent to prevent truncated tool calls
- Add system prompt size limits: split render calls at ~40 elements
- Fix paste/copy/cut in chat input (stop event propagation to canvas handler)

* Fix 6 layout engine bugs: hidden size, FILL basis, SPACE_EVENLY, alignSelf range, grid stretch, absolute children

Layout engine (layout.ts):
- Guard applyYogaLayout to preserve hidden children dimensions
- Add setFlexBasis(0) for FILL sizing so children share space from zero
- Add SPACE_EVENLY to mapJustify via Justify.SpaceEvenly
- Replace hardcoded STRETCH checks with mapAlignSelf for full range
- Use setWidthStretch/setHeightStretch for grid children instead of flexGrow
- Insert absolute children in Yoga tree via configureAbsoluteChild with
  PositionType.Absolute, keeping manual x/y (no write-back)

Types (scene-graph.ts):
- Add SPACE_EVENLY to LayoutAlign union
- New LayoutAlignSelf type: AUTO | MIN | CENTER | MAX | STRETCH | BASELINE

Import (kiwi-convert.ts):
- Map SPACE_EVENLY correctly instead of collapsing to SPACE_BETWEEN
- New mapAlignSelf function for full StackCounterAlign range

Tools (modify.ts):
- Expand set_layout align enum with SPACE_EVENLY
- Expand set_layout_child align_self enum with MIN/CENTER/MAX/BASELINE

Tests: 86 pass (+13 new covering all 6 fixes)

* Grid layout support in JSX renderer, auto-height grids, flex children stretch in cells

Renderer:
- Parse grid/columns/rows/columnGap/rowGap/gap props
- Parse grid child positioning: colStart/col/rowStart/row/colSpan/rowSpan
- Grid prop takes precedence over padding-triggered auto-layout
- SPACE_EVENLY added to ALIGN_MAP
- Numeric columns shorthand (columns={3} → 3×1fr)
- Auto-height: grid without rows sets height=0 for Yoga auto-sizing
- fill sizing in grid children → layoutAlignSelf: STRETCH
- Component builders accept variadic children: Frame(props, ...children)

Layout:
- Grid auto-height: skip setHeight when no gridTemplateRows
- Write back computed height for auto-height grids
- Grid children with layoutMode stretch width to fill cell
- recomputeGridChild: re-run flex layout after grid assigns cell size
  (temporarily sets sizing to FIXED so HUG doesn't override grid width)

Export:
- Skip height export for auto-height grids (no rows template)

Tests: 18 new grid render tests covering all paths

* Auto-enable flex for justify/items props, discourage export_image in AI prompt

* Address review: use sucrase typescript transform, don't reset chat on maxOutputTokens change

* Fix lint errors: extract applyFrameSize, wrap nested ternary, remove unnecessary cast

* Remove SPACE_EVENLY — not a valid Figma layout value

* CI: debug LFS pull from R2

* Apply maxOutputTokens dynamically via prepareCall instead of at agent creation

* Fix heavy .fig parse: restore GUID/defID guards, fix optional types

Commit f3eac5a removed null guards from fig-import and
instance-overrides as 'unnecessary conditions', but Kiwi-decoded data
can have NodeChanges without GUIDs and ComponentPropAssignments/Refs
without defIDs. Made the types optional to match reality.

Also fix CI LFS: add git lfs install --force before pull (R2 endpoint
needs the filter registered first).

* Fix Google AI model IDs: gemini-3.1-pro → gemini-3.1-pro-preview

Closes #91

---------

Co-authored-by: Danila Poyarkov <dev@dannote.net>
2026-03-10 15:43:04 +03:00
Danila Poyarkov fe755fbb85 Release v0.9.0 2026-03-09 17:43:11 +03:00
Danila Poyarkov abe108c473
Add event emitter to SceneGraph, refactor collab sync
- Add nanoevents to @open-pencil/core, emit typed events from
  SceneGraph mutation methods: node:created, node:updated,
  node:deleted, node:reparented, node:reordered
- Subscribe to node:updated in editor store for renderer
  invalidation (vector path + node picture cache) instead of
  inlining it in updateNode()
- Re-subscribe to graph events after file open/reload (graph
  instance is replaced)
- Replace monkey-patching of graph.updateNode in use-collab with
  event subscriptions for node:created, node:updated, node:deleted,
  node:reparented — fixes sync gaps for create/delete/reparent that
  the monkey-patch missed
- Clean up event subscriptions on collab disconnect
2026-03-09 16:02:54 +03:00
Danila Poyarkov e3ab8dab38 AI chat rendering, undo, visual verification, and provider compatibility
Rendering & layout:
- Fix COUNTER_ALIGN_MAP stretch mapping
- Direction-aware fill sizing based on parent flex direction
- Text without explicit width defaults to WIDTH_AND_HEIGHT auto-resize
- Padding auto-enables vertical auto-layout
- clipsContent propagated as Overflow.Hidden to Yoga
- Text height measurement for HEIGHT auto-resize mode
- Export x/y for absolute children, text w/h per textAutoResize
- Export STRETCH as fill on cross axis

Undo system:
- Undo for auto-layout and layer tree reorder
- Atomic undo for drag + reparent
- Page snapshot undo for AI tool operations
- mouseleave no longer terminates active drag

AI tools & vision:
- Add get_jsx, diff_jsx, and describe tools
- export_image returns image via toModelOutput
- Chunked uint8ArrayToBase64 (stack overflow fix)
- computeAllLayouts after AI tool execution
- onBeforeExecute/onAfterExecute receive ToolDef

Provider & UI:
- Add anthropic-compatible provider
- API type toggle for OpenAI-compatible
- Debug toolbar (dev only)
- Preserve chat on UI toggle
- Enhanced system prompt with JSX reference

Co-authored-by: Anton A S <eddclyde@yandex.ru>
2026-03-09 15:49:29 +03:00