Commit graph

756 commits

Author SHA1 Message Date
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 54a418f555 Block chat input during AI streaming to prevent message spam
Input is now disabled during both 'submitted' and 'streaming' states.
handleSubmit guards against concurrent sends.
2026-03-13 10:17:49 +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 b2d3e877a0 Simplify top-down workflow: 3 renders + 3 describes, not 10+
Was too granular — one render per element. Now: skeleton, poster
content, main content. Describe always targets root frame.
2026-03-13 10:17:23 +03:00
Anton A S 224c876806 Require describe after every render call, plus final verification 2026-03-13 10:17:23 +03:00
Anton A S 106d6de365 Ban export_image in AI chat prompt 2026-03-13 10:17:22 +03:00
Anton A S ee0b9eeed8 Make top-down rendering mandatory with hard 40-element limit
AI was ignoring the soft suggestion and generating 130+ element JSX
in a single render call. Now uses explicit skeleton example, numbered
steps, and 🚫 markers to enforce incremental building.
2026-03-13 10:17:21 +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 b4b7ffa81d Add top-down building workflow to AI prompt
Complex designs must be built layer by layer: skeleton first,
then fill sections one at a time with describe checks between.
Prevents nested containers missing w="fill" and collapsing.
2026-03-13 10:17:19 +03:00
Anton A S 82f586ac73 Fix AI chat not reacting to provider/model changes
- Mark transport dirty instead of resetting chat on settings change
- Rebuild Chat with new transport while preserving message history
- Watch apiKey and customBaseURL changes in addition to model/provider
- Fix provider select z-index inside settings popover (z-52 + isolate)
- Prevent popover dismiss when interacting with select dropdown
2026-03-13 10:17:19 +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 766cf11699 Add decorative vs content layer rule to prompt
Decorative elements (bokeh, glows, gradients, star dots) must use
absolute positioning (x/y), not flex layout. Putting 10+ tiny shapes
in a flex container wastes gap space and squashes real content.
2026-03-13 10:17:17 +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 86a2191b9c Add maxLines guidance for fixed-height text in grids 2026-03-13 10:17:13 +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 682559f682 Extract AI system prompt to markdown file 2026-03-13 10:17:08 +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
Danila Poyarkov e8f8a0b8e2 Update Vite 7 → 8 2026-03-12 22:13:30 +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 bc7a687aef Update changelog with font rendering fix 2026-03-12 22:07:10 +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 198f2d138a Update changelog with clipboard roundtrip fixes 2026-03-12 17:29:43 +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 f54fcc32f9 Add gold-preview.fig clipboard roundtrip tests
OpenPencil format: zero property differences, compressed under 1MB.
Figma format: node count, clipsContent, constraints, arcData,
layoutAlignSelf all verified against the fixture file.
2026-03-12 17:05:00 +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 91c6f55143 Update changelog with Z.ai and MiniMax providers 2026-03-11 13:26:55 +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
Danila Poyarkov be845f5eeb Merge remote-tracking branch 'origin/fix/auto-layout-overflow' 2026-03-10 23:00:50 +03:00
Danila Poyarkov 0c4581b04f Add headless text layout fix to changelog 2026-03-10 23:00:45 +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 57eaf870af Add CJK text fix to changelog 2026-03-10 16:59:18 +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 f25e238e53 Update changelog with PR #86 fixes 2026-03-10 16:31:51 +03:00