Commit graph

45 commits

Author SHA1 Message Date
Danila Poyarkov ce55af6171 Fix pasteID int overflow and make error toasts copyable
- Use Int32Array instead of Uint32Array for Kiwi pasteID (signed int field)
- Error toasts: don't auto-dismiss, show copy button, text is selectable
- Add clipboard roundtrip tests (encode → decode → verify)
2026-03-02 18:55:49 +03:00
Danila Poyarkov db7a019385 Fix Figma paste redo + add clipboard import tests
Redo was creating duplicate childIds because snapshots captured after
tree construction had populated childIds, and createNode appends to
parent.childIds again. Fix: clear childIds in redo snapshots.

New tests (7):
- layoutAlignSelf from stackChildAlignSelf
- clipsContent from frameMaskDisabled
- fontWeight from fontName.style via styleToWeight
- letterSpacing object → pixels conversion
- undo removes full subtree
- redo recreates correct parent-child tree
- redo without childIds:[] demonstrates the duplicate bug
2026-03-02 18:05:22 +03:00
Danila Poyarkov 8b1547ece7 Center pasted Figma nodes in viewport instead of using original coordinates 2026-03-02 17:32:06 +03:00
Danila Poyarkov d7ba0bfef9 Skip non-visual node types in clipboard import (variables, widgets, etc.)
Figma clipboard data includes VARIABLE_SET, VARIABLE, and other non-visual
types that were falling through to the default RECTANGLE case in mapNodeType.
These now join DOCUMENT and CANVAS in the skipTypes set.
2026-03-02 17:22:09 +03:00
Danila Poyarkov ebb85e8f20 Improve scene cache regression test for font load race 2026-03-02 15:51:02 +03:00
Danila Poyarkov af8b90a939 Fix text disappearing after hover: invalidate SkPicture on font load
The first render records an SkPicture before fonts are loaded (fallback
drawText path). When fonts finish loading asynchronously, the cached
picture was never invalidated — subsequent renders replayed stale picture
without paragraph text. Hovering a node forced a full re-render (volatile
overlay path), making text reappear. Un-hovering replayed the stale cache.

Fix: call invalidateScenePicture() at the end of loadFonts() so the next
render records a fresh picture with proper paragraph text.
2026-03-02 15:45:58 +03:00
Danila Poyarkov 4ad90d4819 Add MCP server package with stdio and HTTP transports
29 tools: open_file, save_file, new_document + all 26 from @open-pencil/core.
Stdio for MCP clients (Claude Code, Cursor), HTTP (Hono + Streamable HTTP
with sessions) for scripts, browser extensions, CI.
Runs on both Bun and Node.js (via tsx).
2026-03-02 14:20:49 +03:00
Danila Poyarkov 527be98f2a Re-apply SkPicture scene caching with visual regression tests 2026-03-02 11:33:45 +03:00
Danila Poyarkov 1ee8e8e5af Add render performance benchmark test 2026-03-01 22:35:16 +03:00
Danila Poyarkov ca20bfb2ac Fix layers-panel tests: match current demo shapes (Components, App Preview) 2026-03-01 15:28:13 +03:00
Danila Poyarkov 20290f61b5 Add integration tests for app menu and autosave
9 app-menu tests: menu visibility, all 6 submenus content, Undo/Duplicate/
Zoom to fit via menu actions.
2 autosave tests: write triggers after scene change with mock file handle,
no write without file handle.
2026-03-01 15:26:10 +03:00
Danila Poyarkov 3849872b32 Unify tool definitions: define once, adapt for AI/CLI/MCP
Move tool logic to @open-pencil/core/tools/schema.ts as framework-agnostic
ToolDef objects. AI adapter generates valibot schemas + Vercel AI tool()
wrappers automatically.

26 tools (was 10): create_shape, render (JSX), set_fill, set_stroke,
set_effects, set_layout, set_constraints, update_node, delete, clone,
rename, reparent, group/ungroup, find_nodes, get_node, get_page_tree,
get_selection, select, list_pages, switch_page, list_variables,
list_collections, create_component, create_instance, eval.

src/ai/tools.ts: 269→28 lines (adapter only).

Tests for all 3 interfaces:
- 26 core tool tests (FigmaAPI directly)
- 11 AI adapter tests (valibot + Vercel AI SDK tool())
- 12 CLI integration tests (eval command on .fig fixture)
323 total, all passing.
2026-03-01 15:02:05 +03:00
Danila Poyarkov f22bd2e133 Merge eval-command: Figma Plugin API + eval CLI command
FigmaAPI wraps SceneGraph with Figma-compatible interface (Symbol-hidden internals).
CLI eval command runs Plugin API scripts headless on .fig files.
106 new tests (60 figma-api + 17 cli integration + 29 gap-filling).
2026-03-01 13:55:39 +03:00
Danila Poyarkov 48219af2fb Fill Figma Plugin API gaps: 29 new tests, 30+ new properties and methods
SceneNode: add minWidth/maxWidth/minHeight/maxHeight, isMask, maskType,
counterAxisAlignContent, itemReverseZIndex, strokesIncludedInLayout,
expanded, textTruncation, autoRename, strokeMiterLimit. All with defaults
and .fig import mapping.

FigmaNodeProxy: add clone(), strokeCap, strokeJoin, strokeMiterLimit,
strokeTopWeight/BottomWeight/LeftWeight/RightWeight, minWidth/maxWidth/
minHeight/maxHeight, isMask, maskType, expanded, textTruncation,
autoRename, insertCharacters, deleteCharacters, primaryAxisSizingMode,
counterAxisSizingMode, counterAxisAlignContent, itemReverseZIndex,
strokesIncludedInLayout, layoutAlign, findAllWithCriteria.

FigmaAPI: add createComponentFromNode, getVariableById, getLocalVariables,
getLocalVariableCollections, getVariableCollectionById.

Fix layoutSizingHorizontal/Vertical for self-framing auto-layout nodes
with no auto-layout parent — now checks the node's own layoutMode.
2026-03-01 13:35:02 +03:00
Danila Poyarkov b6e792846f Harden FigmaAPI: hide internals via symbols, freeze arrays, fix layoutSizing
- Replace _id/_graph/_api with Symbol-keyed properties so eval scripts
  can't access SceneGraph internals directly
- fills/strokes/effects return frozen structuredClone copies
- Fix currentPage getter to define selection only once per proxy
- Fix layoutSizingHorizontal/Vertical: respect parent layout direction
  even when the node itself is an auto-layout frame
- Add 6 new tests: layoutSizing, frozen arrays, internals not exposed
2026-03-01 13:12:26 +03:00
Danila Poyarkov 9a7d170887 Add 17 CLI integration tests for eval command
Tests cover: page name, primitives, findAll queries, node creation,
auto-layout, text, top-level await, stdin piping, JSON output,
syntax/runtime errors, no-code error, undefined output, --output write,
array serialization, getNodeById on real .fig files.
2026-03-01 13:07:06 +03:00
Danila Poyarkov 0cba4fb4b7 Add FigmaAPI + eval CLI command with 60 unit tests
FigmaAPI in @open-pencil/core provides a Figma Plugin API-compatible
interface backed by SceneGraph. Covers node creation, property access,
tree operations, auto-layout, text, traversal, grouping, components,
selection, and serialization.

CLI: bun open-pencil eval <file> --code 'figma.createFrame()'
2026-03-01 12:21:07 +03:00
Danila Poyarkov c59d5fc397 Add Code tab with JSX export and syntax highlighting
- sceneNodeToJsx() in @open-pencil/core: converts SceneNode subtree to JSX
- CodePanel.vue with Prism.js highlighting, line numbers, copy button
- Third tab in properties panel: Design | Code | AI
- 14 new tests covering shapes, text, layout, effects, multi-selection
2026-03-01 12:12:45 +03:00
Danila Poyarkov bf8636c677 Merge chat-panel: AI chat with OpenRouter, tool execution, model selector 2026-03-01 11:55:57 +03:00
Danila Poyarkov 921a9cd261 Fix tool name extraction from UIMessage parts, add tool call e2e test
Tool part type is 'tool-create_shape' (name embedded in type field),
not a separate toolName property. Extract via part.type.replace('tool-', '').

Mock transport now emits proper UI message stream protocol:
tool-input-start → tool-input-delta → tool-input-available → tool-output-available.
2026-03-01 11:51:38 +03:00
Danila Poyarkov 4032108be0 Move AI models to @open-pencil/core constants, add Kimi K2.5 (vision+code), benchmark-ranked tags 2026-03-01 11:38:40 +03:00
Danila Poyarkov 9b59d8e816 Update model list to current versions (Claude 4.6, Gemini 3.1, GPT-5.3, DeepSeek V3.2, Qwen 3.5) 2026-03-01 11:34:38 +03:00
Danila Poyarkov 299b668e7b Show selected model name in selector trigger 2026-03-01 11:33:12 +03:00
Danila Poyarkov f6434bc4f9 Fix chat reactivity (markRaw), add Playwright tests with mock transport
Use markRaw to prevent Vue from deep-proxying the Chat class
instance, which broke its internal Vue refs. Add dedent for
multiline prompts.

Tests use mock transport by default. Set TEST_REAL_LLM=1 and
OPENROUTER_API_KEY to run against real OpenRouter.
2026-03-01 11:30:30 +03:00
Danila Poyarkov 13f562adc6 JSX renderer in @open-pencil/core
TreeNode builder functions (Frame, Text, Rectangle, etc.) produce a
lightweight tree structure. Two rendering paths:

- renderTreeNode(): tree → scene graph (browser, no deps)
- renderJsx(): JSX string → esbuild → tree → scene graph (CLI/headless)

Tailwind-like shorthand props: w/h, bg, rounded, flex, gap, p/px/py,
justify, items, shadow, blur, etc.

27 tests covering all node types, layout props, effects, and nesting.
2026-03-01 10:42:06 +03:00
Danila Poyarkov ca422147b4 Add .fig roundtrip tests: real file parsing, property invariants, encode/decode cycle 2026-03-01 10:33:58 +03:00
Danila Poyarkov f24f6e0dc6 Add .fig test fixtures (LFS) and fix O(n²) import bottleneck
- Add Material 3 Design Kit and Nuxt UI v4 community files as test fixtures
- Track tests/fixtures/*.fig with Git LFS
- Fix O(n²) getChildren() in fig-import: build children index upfront
  - material3.fig (87K nodes): 37s → 535ms (69x faster)
  - nuxtui.fig (314K nodes): minutes → 2.3s
- Optimize kiwi ByteBuffer: inline readVarUint, use TextDecoder for strings
2026-03-01 09:55:07 +03:00
Danila Poyarkov d30ab4c5d5 Add variables support: collections, modes, bindings, UI panel
Core:
- Variable, VariableCollection types on SceneGraph
- resolveVariable with alias chain resolution + cycle detection
- Mode switching (activeMode per collection)
- Bind/unbind variables to node properties (fills/strokes colors)
- Renderer resolves variable bindings before painting

Import:
- Parse VARIABLE type NodeChanges from .fig files
- Extract paint variable bindings (fills/strokes)

UI:
- VariablesPanel with reka-ui Tabs (collections) + Editable (names/values)
- FillSection: variable picker (reka-ui Popover + Combobox)
- Bound fills show purple variable name badge with detach button
- Toggle variables panel from bottom of layers sidebar

7 new unit tests (83 total)
2026-03-01 01:47:52 +03:00
Danila Poyarkov f77ad708b8 Live component-instance sync with override support
Editing a main component now propagates to all instances:
- Instance children are mapped to component children via componentId
- syncInstances() copies properties from component to instances,
  skipping any property marked in the overrides record
- New children added to a component appear in existing instances
- Store calls syncIfInsideComponent after updateNode, commitMove,
  commitResize — changes propagate automatically
- Demo creates real instances (Button, Badge) in the App Preview

Tests added: instance creation with child mapping, sync propagation,
override preservation, new child addition, detach.
2026-02-28 19:57:52 +03:00
Danila Poyarkov 07f0f559da Add fig-import unit tests for Tier 1 rendering features
23 tests covering: node type mapping (ROUNDED_RECTANGLE, COMPONENT,
INSTANCE, SYMBOL, POLYGON), gradient fills (linear, radial), image
fills with hash, effects (drop shadow, inner shadow), stroke options
(cap, join, dash pattern), text properties (auto-resize, font weight
mapping with space normalization), arc data (partial ellipse, donut),
constraints, blend modes, independent stroke weights, stacked fills.
2026-02-28 16:43:43 +03:00
Danila Poyarkov 0524092f3e Merge branch 'autolayout-tests' 2026-02-28 14:04:30 +03:00
Danila Poyarkov 27f57da340 Improved demo: section with Desktop/Mobile frames, color swatches
- 'Design System' section containing Desktop and Mobile frames
- Desktop: header, hero card, avatar, two content cards
- Mobile: status bar, banner, list items, FAB button
- Color palette swatches (Primary, Success, Warning, Danger, Purple)
- Updated E2E tests for new demo structure
- Reverted section title pill to always use fill color (not blue)
2026-02-28 14:02:46 +03:00
Danila Poyarkov 27f83c721e Add layout tests, fix cross-axis sizing bugs
- Fix axis mapping for nested frames with different layout directions
  (e.g. vertical child inside horizontal parent had swapped sizing)
- Fix stretch alignment not applying to leaf children when set on parent
- Fix layoutGrow being ignored on children with FIXED primaryAxisSizing
- Add 42 unit tests covering all layout scenarios
2026-02-28 13:59:39 +03:00
Danila Poyarkov 1d79473bc6 Pages support: multi-page documents like Figma
Document structure: Document (root) → Pages (CANVAS) → Nodes.
Each page has its own viewport (pan/zoom) and background color.

- CANVAS node type for pages
- SceneGraph: addPage(), getPages(), getAbsolutePosition stops at CANVAS
- Store: currentPageId, switchPage(), addPage(), deletePage(), renamePage()
- All operations (create, select, paste, hit test, render) scoped to current page
- Per-page viewport state saved/restored on page switch
- Pages list in LayersPanel with add/switch/double-click rename
- .fig import creates proper pages from DOCUMENT→CANVAS hierarchy
- Renderer takes pageId to render correct page's children
- Unit test for pages, updated E2E tests for page-scoped graph
2026-02-28 13:12:27 +03:00
Danila Poyarkov fd0b02642f Fix layers panel reactivity and accidental reparenting on click
- Force TreeRoot re-mount via :key when tree structure changes,
  fixing Reka UI not reacting to items prop updates
- Only reparent into frames on mouseup when user actually dragged,
  preventing click-to-select from moving nodes into overlapping frames
- Expose store on window for test introspection
- 6 new E2E tests for layers panel
2026-02-28 11:15:19 +03:00
Danila Poyarkov 9d5c85667f Canvas rulers with selection highlight
Horizontal and vertical rulers rendered directly in CanvasKit:
- Dark background, tick marks at adaptive intervals
- Coordinate labels scale with zoom level
- Blue highlight showing selected node extent on each axis
- Corner square at ruler intersection
2026-02-28 09:35:50 +03:00
Danila Poyarkov e37d2b216a Fix silent rendering crash, no-chrome test mode, error detection
- Fix this.this.selColor() typo in renderer (6 occurrences) — broke
  selection/handles rendering since the constants extraction refactor
- Add preserveDrawingBuffer to WebGL surface for reliable screenshots
- Add ?no-chrome URL param to hide panels/toolbar in tests
- CanvasHelper collects pageerror + console.error, assertNoErrors()
  fails tests on any browser-side exception
2026-02-28 08:39:19 +03:00
Danila Poyarkov bcd8bf6bca Add IconsResolver for auto-import, explicit imports only for dynamic maps 2026-02-28 08:29:39 +03:00
Danila Poyarkov 3ddc671bc5 Replace emoji icons with Lucide via Iconify (unplugin-icons)
- unplugin-icons + @iconify-json/lucide for compile-time icon bundling
- Toolbar: proper cursor, frame, square, pen, type, hand icons
- Layers panel: per-type node icons + chevron-right for expand/collapse
- Remove icon field from ToolDef (icons mapped in components)
2026-02-28 08:24:42 +03:00
Danila Poyarkov f0d817c747 Refactor: vendor kiwi-schema, extract types/constants, culori colors
- Fork kiwi-schema from source TypeScript (evanw/kiwi), drop npm dep + patch
- Deduplicate GUID/Color types into src/types.ts
- Extract UI colors and default fills into src/constants.ts
- Replace hand-rolled hex parsing with culori library
- Extract demo shapes from App.vue into src/demo.ts
- useUrlSearchParams from VueUse for test mode detection
- Move fig-file/fig-import into kiwi/ (format layer)
- Delete PoC files (poc-yoga.ts, poc-test.ts)
2026-02-28 08:08:04 +03:00
Danila Poyarkov e03a01dbe3 Add bun:test unit tests for SceneGraph (15ms) 2026-02-28 07:45:50 +03:00
Danila Poyarkov c1fbf57df2 Skip stability check: toMatchSnapshot, 2.1s → 1.3s 2026-02-28 07:44:20 +03:00
Danila Poyarkov b5eb7f751f Faster E2E: data-ready attr + rAF wait, 4.6s → 2.1s 2026-02-28 07:43:05 +03:00
Danila Poyarkov e8c01fb93c Speed up E2E tests: reuse page, 14s → 4.6s 2026-02-28 07:35:52 +03:00
Danila Poyarkov 40f7b3a648 Playwright visual regression testing with Figma CDP reference
- Playwright E2E tests for OpenPencil: draw, move, delete, undo shapes
- Visual screenshot baselines with toHaveScreenshot()
- SwiftShader for headless WebGL/CanvasKit rendering
- Figma CDP helper for reference tests (connect to Figma Desktop)
- ?test URL param for clean canvas without demo shapes
- Scripts: test, test:update, test:figma, figma:debug
2026-02-28 07:32:15 +03:00