Commit graph

1018 commits

Author SHA1 Message Date
Danila Poyarkov da2df1296b Extract shared sortChildren, reduce kiwi-convert export surface 2026-03-03 23:34:02 +03:00
Danila Poyarkov 9044b1b183 Sort auto-layout children by geometric position on import
Figma's parentIndex.position strings represent z-order, not visual
layout order. In auto-layout frames, children must be sorted by their
x (horizontal) or y (vertical) coordinate to match the visual order.
Fixes scrambled calendar dates and other auto-layout child ordering.
2026-03-03 23:31:23 +03:00
Danila Poyarkov 157c776c0c Apply instance symbolOverrides on clipboard paste
Each instance in Figma's clipboard carries symbolData.symbolOverrides
with per-child property changes (text, fills, visibility). The override
targets a child via guidPath which matches the overrideKey field on the
component's child node.

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

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

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

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

- Shared composable (use-multi-props) for isMulti, active, activeNode
- ScrubInput supports MIXED symbol as model-value
- Per-node previous values for correct undo on multi-edit
- Fix flip transform encoding (scale first column only)
- Fix npm publish auth for MCP package
- 22 new tests: flip, alignment, multi-node merging, kiwi roundtrip
2026-03-03 20:35:51 +03:00
Danila Poyarkov 5a77f2c1cc Fix flip and single-node alignment
Flip horizontal/vertical now toggles flipX/flipY with scale
transform instead of manipulating rotation. Import detects flip
from negative transform determinant. Single-node alignment uses
parent frame bounds instead of bailing out.
2026-03-03 18:35:30 +03:00
Danila Poyarkov 9c9bfc38b2 Release v0.5.1 2026-03-03 17:30:09 +03:00
Danila Poyarkov 06fb0a3e7a Fix save: map LAYER_BLUR to FOREGROUND_BLUR for kiwi export 2026-03-03 17:29:43 +03:00
Peter 31d614a272
fix(mcp): harden HTTP transport defaults (#26) 2026-03-03 17:19:08 +03:00
Danila Poyarkov 5eac653f4c Release v0.5.0 2026-03-03 14:51:56 +03:00
Danila Poyarkov c36f3c4667 Only show component labels for COMPONENT and COMPONENT_SET, not instances 2026-03-03 14:40:29 +03:00
Danila Poyarkov 0c0be3f28b Hide internal pages and render page dividers
Decode internalOnly from kiwi field 142, filter internal pages
in SceneGraph.getPages() by default (pass true to include them).
Pages with names of only dashes/asterisks/spaces and no children
render as horizontal divider lines.
2026-03-03 14:37:56 +03:00
Anton A S f4cb76b2e2 Translate reference & development pages prose for all locales 2026-03-03 12:57:17 +03:00
Danila Poyarkov 93c549bad0 Reuse GL context on panel resize
Panel resizing now creates a new surface from the existing
GrDirectContext instead of destroying and recreating the entire
renderer. All caches (paints, fonts, node pictures, scene picture,
filters) are preserved across resizes.
2026-03-03 12:48:51 +03:00
Anton A S 8ca1bc790f Add reference & development pages for all locales, fix nav missing menu items 2026-03-03 12:46:35 +03:00
Anton A S e97a9283d5 Fix VitePress i18n: per-locale themeConfig with translated nav/sidebar, typed labels 2026-03-03 12:19:27 +03:00
Anton A S c39e1fd71a Add i18n docs: German, Italian, French, Spanish, Polish translations (95 pages) 2026-03-03 12:15:39 +03:00
Anton A S 311a0d8208 Merge origin/master: P2P collab, MCP server, multi-file tabs, 29 tools, performance 2026-03-03 11:56:46 +03:00
Danila Poyarkov a32b134eba
Improve zoom smoothness, add absolute position cache (#21)
* Improve trackpad pinch-zoom smoothness

- Replace Math.pow(0.99, delta) with Math.exp(-delta/100) zoom curve
  for natural, symmetric scaling that handles both tiny trackpad deltas
  and large mouse wheel jumps gracefully
- Normalize wheel deltaMode (LINE → 40px, PAGE → 800px) so external
  mice on Firefox produce consistent zoom behavior
- Clamp per-flush scale factor to 0.75–1.25 to prevent jarring jumps
  from discrete mouse wheel ticks

* Add per-frame absolute position cache for SceneGraph
2026-03-03 11:44:31 +03:00
Danila Poyarkov a9a1f9ac1f
Shadow rendering performance: per-node SkPicture cache (#25)
* Replace saveLayer with MaskFilter for drop shadows

Drop shadows now draw directly with MaskFilter.MakeBlur instead of
saveLayer + ImageFilter. Eliminates per-shadow offscreen buffer
allocation, significantly improving render throughput for scenes
with many shadowed nodes.

Text shadows still use saveLayer (MaskFilter can't be applied to
text glyphs individually).

Benchmark: 50 shadow nodes render at 1.55ms/frame (scene change).

* Cache per-node SkPictures for effect rendering

Nodes with visible effects (shadows, blurs) are recorded into
individual SkPictures and replayed on subsequent scene redraws.
Only the modified node re-records; unchanged shadow nodes replay
from cache.

- MaskFilter.MakeBlur for drop shadows instead of saveLayer
- Reusable effectLayerPaint and cached ImageFilters/MaskFilters
- invalidateNodePicture() called on node update
- invalidateAllPictures() on font load; invalidateScenePicture()
  preserves node cache for scene-only invalidation
- Shadow benchmark: 50 nodes at 1.47ms/frame (scene change)

* Update changelog with shadow rendering performance improvements
2026-03-03 11:37:01 +03:00
Danila Poyarkov 46215cf0f3 Remove accidentally committed webgpu files 2026-03-03 10:13:05 +03:00
Danila Poyarkov 8ced3201cf Cache effect ImageFilters and reuse layer paint
Eliminates per-frame WASM allocations for shadows and blurs:
- Reusable effectLayerPaint instead of new Paint() per effect
- ImageFilter cache keyed by params (drop shadow, blur, decal blur)
- Zero allocations during SkPicture recording for cached effects
2026-03-03 10:12:02 +03:00
Danila Poyarkov 8ab7148294
Fix effects rendering: shadows, blur, spread (#24)
* Fix shadow rendering, implement blur effects, add spread support

- Fix drop shadow rendering order: draw shadows before fills so they
  appear behind opaque shapes instead of on top (#23.1)
- Implement shadow spread: expand/contract shadow shape by spread value
  for both drop shadow and inner shadow (#23.2)
- Implement background blur: clip to node shape and apply blur filter
  to content behind the node (#23.3)
- Fix text shadows: use saveLayer with ColorFilter.MakeBlend to apply
  shadows to text glyphs instead of the bounding box (#23.4)
- Implement layer blur with actual saveLayer + blur filter instead of
  the no-op comment (#23.3)
- Implement foreground blur: clip to node shape and blur content in
  front (#23.7)
- Add inner shadow spread support with rounded rect handling
- Add makeRRectWithSpread and makeRRectWithOffset helpers

Closes #23

* Add Effects section to demo showcasing shadow and blur fixes

Drop shadows (subtle, medium, heavy, with spread), inner shadows
(inset, with spread, on ellipse, combined), text shadows on glyphs,
layer blur, and glassmorphism background blur card.

* Fix drop shadow, layer blur, and background blur rendering

Drop shadow: use MakeDropShadowOnly to produce shadow-only output
that doesn't bleed through opaque fills. The sigma is radius/2 to
match Figma's blur convention.

Layer blur: wrap entire node content in a saveLayer with blur filter
at the renderNode level instead of trying to blur after-the-fact
in renderEffects.

Background/foreground blur: extract clipNodeShape helper to reduce
duplication in blur effect rendering.

Update tests to match new rendering approach.

* Add visual regression tests for effects rendering

9 snapshot tests covering drop shadow, inner shadow, spread,
ellipse shapes, text glyphs, layer blur, and combined effects.

* Make Effects demo section background white

* Clean up renderer code and tests

- Extract applyClippedBlur to deduplicate background/foreground blur
- Remove comments that repeat what code does
- Format with oxfmt

* Add undo/redo support for effect property changes

ScrubInput controls (offset, radius, spread, opacity) now use
scrubEffect for live preview and commitEffect on release to create
a single undo entry per interaction.
2026-03-03 09:53:39 +03:00
Danila Poyarkov 4ee3a2dcc3 Add set_text_properties and set_layout_child tools (from #11)
Cherry-picked the 2 unique tools from PR #11:
- set_text_properties: alignment, auto-resize, decoration
- set_layout_child: sizing, grow, align_self, positioning

The other 10 tools in the PR were already covered by the
port-tools merge (73 → 75 tools).
2026-03-02 21:12:44 +03:00
Danila Poyarkov 7a5f6b3707 Add MCP server integration tests (closes #19)
13 tests using InMemoryTransport from @modelcontextprotocol/sdk:
- Tool registration (all 76 tools listed with descriptions)
- new_document, open_file, save_file lifecycle
- Tool execution through MCP protocol (create, fill, delete, query)
- Error paths: no document loaded, nonexistent node, invalid file path
- Full workflow: new → create → nest → query → delete

Fix @open-pencil/core dep in MCP package: workspace:* for local dev
(pnpm publish resolves this to the actual version at publish time).
2026-03-02 21:07:25 +03:00
Danila Poyarkov da28a0ed93 Merge branch 'multi-file-tabs' 2026-03-02 19:52:03 +03:00
Danila Poyarkov c3869908ee Format core package 2026-03-02 19:43:44 +03:00
Danila Poyarkov ef78ffd2e4 Render text from SkPicture cache when fonts are missing
On copy: serialize each text node's paragraph into an SkPicture and
embed the bytes (base64) in the OpenPencil clipboard payload.

On paste: if the font isn't available, render from the cached SkPicture
instead of the broken fallback. Text looks pixel-perfect without the
font installed — same behavior as Figma.

Missing font indicator:
- Orange ⚠ badge on canvas over text nodes with unavailable fonts
- Amber warning banner in the Typography panel listing missing families
- isFontLoaded() check in @open-pencil/core for querying font status
2026-03-02 19:35:04 +03:00
Danila Poyarkov 4e1c15c4ff Extract color constants: BLACK, TRANSPARENT, DEFAULT_SHADOW_COLOR
Replace 8 inline { r: 0, g: 0, b: 0, a: 1 } literals across core
with shared constants from constants.ts. Also reuse CANVAS_BG_COLOR
in fig-export.ts instead of duplicating the value.
2026-03-02 19:34:35 +03:00
Danila Poyarkov 537663aa3c Port figma-use tools: variables, set_*, node ops, boolean, paths, viewport
Add 40+ new tool definitions ported from figma-use:

Granular set tools: set_rotation, set_opacity, set_radius, set_minmax,
set_text, set_font, set_font_range, set_text_resize, set_visible,
set_blend, set_locked, set_stroke_align

Node operations: node_bounds, node_move, node_resize, node_ancestors,
node_children, node_tree, node_bindings, node_replace_with

Variable CRUD: get_variable, find_variables, create_variable,
set_variable, delete_variable, bind_variable

Collection CRUD: get_collection, create_collection, delete_collection

Boolean operations: boolean_union, boolean_subtract, boolean_intersect,
boolean_exclude

Vector path tools: path_get, path_set, path_scale, path_flip, path_move

Create tools: create_page, create_vector, create_slice

Misc: flatten_nodes, viewport_get, viewport_set, viewport_zoom_to_fit,
page_bounds, list_fonts

Also adds createVariable/createCollection to SceneGraph, and viewport,
boolean operation, and flatten methods to FigmaAPI.
2026-03-02 19:27:42 +03:00
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 7d98201548 Release v0.4.2 2026-03-02 18:14:35 +03:00
Danila Poyarkov 8850625a6f Fix text not rendering + undo for Figma clipboard paste
- letterSpacing is an object {value, units} not a number — was NaN,
  which silently breaks CanvasKit paragraph layout
- Add mapLetterSpacing() to handle PIXELS and PERCENT units
- Force textAutoResize to NONE (Yoga has no text measurement callback)
- Record undo entry for Figma paste with full subtree snapshots
- Add stackChildAlignSelf, fontWeight to NodeChange interface
2026-03-02 17:59:18 +03:00
Danila Poyarkov e1b5f1901e Import missing layout/text properties from Figma clipboard
- layoutAlignSelf (stackChildAlignSelf → STRETCH/AUTO)
- clipsContent (from frameMaskDisabled, inverted)
- textAutoResize (NONE/HEIGHT/WIDTH_AND_HEIGHT/TRUNCATE)
- fontWeight (from fontName.style via styleToWeight when not explicit)
- italic (from fontName.style)
- lineHeight (PIXELS only, PERCENT deferred)
- letterSpacing
2026-03-02 17:41:42 +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 7eb74c3969 Release v0.4.1 2026-03-02 15:51:34 +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 f4b45d4f13 Replace "fig-kiwi" format name with "Kiwi binary" in docs
fig-kiwi is the magic header string in .fig files, not a format name.
The format is Kiwi binary — used for both .fig files and clipboard.
2026-03-02 15:31:25 +03:00
Danila Poyarkov 75cc653efc Fix Kiwi format description — same binary format as .fig files, not a clipboard format 2026-03-02 15:28:24 +03:00
Danila Poyarkov 9103d1b554 Highlight copy & paste with Figma in README and docs 2026-03-02 15:26:06 +03:00
Danila Poyarkov cc77c68143 Fix core exports: node → dist, default → src 2026-03-02 15:03:40 +03:00
Danila Poyarkov 5856233832 Fix core exports: types → src for workspace typecheck 2026-03-02 15:02:43 +03:00
Danila Poyarkov 8fea6c086d Bump all packages to 0.4.0, fix Node.js ESM compatibility
- tsgo + fix-esm-import-path for .js extensions in dist/
- Core exports: bun condition → src, import → dist
- MCP depends on core ^0.4.0
2026-03-02 14:59:59 +03:00
Danila Poyarkov 1f27add8c7 Fix MCP build script, bump to 0.3.3 2026-03-02 14:45:48 +03:00
Danila Poyarkov e7209171fa Build MCP package to JS for Node.js bin compatibility
tsgo emits dist/*.js with shebangs. Bins point at dist/ so
openpencil-mcp and openpencil-mcp-http work via npm/npx.
2026-03-02 14:42:15 +03:00
Danila Poyarkov d93bcab0b6 Add install instructions for @open-pencil/mcp 2026-03-02 14:28:56 +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 eeaef12c27 Update Penpot comparison to reflect WASM renderer (render-wasm/v1)
Penpot 2.x ships with Rust/Skia WASM rendering enabled by default.
Update architecture diagram, rendering pipeline, and file format sections
to reflect the current state instead of the old SVG-based renderer.
2026-03-02 13:35:09 +03:00
Danila Poyarkov 518ddb0c5b Release v0.3.2 2026-03-02 11:36:18 +03:00
Danila Poyarkov 527be98f2a Re-apply SkPicture scene caching with visual regression tests 2026-03-02 11:33:45 +03:00
Danila Poyarkov 1b414bf680 Release v0.3.1 2026-03-02 11:14:15 +03:00
Danila Poyarkov 38aeade46e Revert SkPicture scene caching (breaks text rendering on hover) 2026-03-02 11:13:46 +03:00
Danila Poyarkov d5ca92e01f Add MIT license 2026-03-02 08:31:54 +03:00
Danila Poyarkov c2e77ff63d Fix Figma MCP timeline, update tool counts to 26, add AI providers to roadmap 2026-03-02 08:19:08 +03:00
Danila Poyarkov f495f4072a Release v0.3.0 2026-03-01 22:46:06 +03:00
Danila Poyarkov a9c096a69d Fix npm publish: pnpm for workspace resolution, update README, read CLI version from package.json 2026-03-01 22:44:56 +03:00
Danila Poyarkov 9431918634 SkPicture scene caching for fast pan/zoom 2026-03-01 22:28:03 +03:00
Danila Poyarkov 80beba3348 Cache vector paths and pen paints across frames 2026-03-01 22:21:20 +03:00
Danila Poyarkov 97f4727e26 Cache ruler paints, disable preserveDrawingBuffer outside tests 2026-03-01 22:16:13 +03:00
Danila Poyarkov 5e209eca1c Fix trackpad pinch-to-zoom on Safari macOS 2026-03-01 22:07:47 +03:00
Danila Poyarkov 13a95bf517 Release v0.2.1 2026-03-01 21:25:52 +03:00
Danila Poyarkov e8b685ad0a Update screenshot 2026-03-01 18:44:52 +03:00
Anton A S 0e8866265d Sync specs & docs with master: docs→packages/docs, Safari save, .fig export fix, LOC stats 2026-03-01 18:33:36 +03:00
Danila Poyarkov 6e4332a6ba Release v0.2.0 2026-03-01 18:27:12 +03:00
Anton A S 5fa8284766 Merge origin/master 2026-03-01 18:23:32 +03:00
Danila Poyarkov f90e1400b7 P2P collaboration: cursors, follow mode, cleanup
- Broadcast cursor position from canvas mouse move via awareness
- Broadcast selection changes via reactive watcher on selectedIds
- Follow mode: click peer avatar to track their viewport (pan + zoom)
- Zoom broadcast via watcher on store.state.zoom, not just mouse move
- Figma-style cursor arrows: colored fill, white border, name pill
- Stale cursor cleanup: removeAwarenessStates on peer leave
- MQTT signaling (replace Nostr), STUN + TURN ICE servers
- Collab constants extracted to src/constants.ts
- crypto.getRandomValues() replaces Math.random() everywhere
- Provide/inject for collab composable (COLLAB_KEY)
- Update README (collab section, tech stack), CHANGELOG, AGENTS.md
- AGENTS.md: release process, CI workflows, documentation guidelines
2026-03-01 18:11:07 +03:00
Danila Poyarkov 7a2694b6bc Emphasize no subscription, bring your own API key 2026-03-01 17:26:30 +03:00
Danila Poyarkov 3427b7fd3c Replace Durable Object relay with Trystero P2P + y-indexeddb
- Remove packages/collab/ (CF Worker + Durable Object server)
- Replace WebSocket relay with Trystero (serverless WebRTC P2P)
  - Peers connect via Nostr relays for signaling (free, public)
  - All sync traffic flows directly peer-to-peer
  - Automatic chunking for large Yjs updates
- Add y-indexeddb for local persistence
  - Each client persists Y.Doc to IndexedDB
  - Room survives browser refresh, loads instantly
- Yjs sync protocol over Trystero actions:
  - yjs-update: incremental doc updates
  - sync-step1/sync-reply: full state sync on peer join
  - awareness: cursor, selection, presence
- Zero server cost at any scale
2026-03-01 17:25:46 +03:00
Danila Poyarkov 8695ba6d1b Emphasize no subscription, bring your own API key 2026-03-01 17:24:54 +03:00
Danila Poyarkov 2add3ac4c0 Real-time collaboration with Yjs CRDT over Cloudflare Durable Objects
- Cloudflare Worker + Durable Object server (packages/collab/)
  - One DO per room with WebSocket Hibernation for near-zero idle cost
  - Yjs sync protocol for document state (CRDT merge)
  - Yjs awareness protocol for cursors, selection, presence
  - Persists doc state to DO storage
  - CORS headers for cross-origin WebSocket upgrade

- Client composable (src/composables/use-collab.ts)
  - Y.Doc ↔ SceneGraph bidirectional binding
  - Awareness: broadcasts cursor position, selection, name, color
  - Auto-join from /share/:roomId URL
  - Suppresses echo loops between Yjs and graph updates

- CollabPanel component (src/components/CollabPanel.vue)
  - Share button generates room link, copies to clipboard
  - Join flow: paste link or room ID
  - Avatar stack with colored initials and tooltips
  - Name input persisted to localStorage

- Remote cursor rendering in CanvasKit
  - Colored arrow cursor + name label pill per remote peer
  - Remote selection outlines in peer's color
  - Only shows cursors on the same page

- SPA redirect for /share/* routes (Cloudflare Pages _redirects)
2026-03-01 16:44:37 +03:00
Danila Poyarkov 9f8242bce5 Add download links to README, docs landing, and getting started 2026-03-01 16:32:45 +03:00
Danila Poyarkov 1e873d64b1 Add repository field and provenance to package configs 2026-03-01 16:23:30 +03:00
Danila Poyarkov c4e6514ea2 Update repo references to open-pencil/open-pencil 2026-03-01 16:18:11 +03:00
Danila Poyarkov 74d4b6862e Use app favicon for docs site (symlink to public/favicon-32.png) 2026-03-01 16:14:06 +03:00
Anton A S c6d70b501b Sync specs & docs with master: eval command, app menu, autosave, unified tools 2026-03-01 16:07:40 +03:00
Danila Poyarkov 77509d8979 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 16:07:35 +03:00
Danila Poyarkov 484c3b6a9a Extract DEFAULT_STROKE_MITER_LIMIT constant 2026-03-01 16:07:33 +03:00
Danila Poyarkov fb03c84ecc Fix bogus fig-import mappings: expanded and strokeMiterLimit
expanded: was mapped from frameMaskDisabled (unrelated Kiwi field),
now defaults to true. strokeMiterLimit: was mapped from speculative
strokeMiterAngle field that doesn't exist in Kiwi codec, now uses
hardcoded default 4.
2026-03-01 16:07:32 +03:00
Danila Poyarkov d08eb4f3b4 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 16:07:31 +03:00
Danila Poyarkov 2c9153d97d 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 16:07:30 +03:00
Danila Poyarkov 0b654e2d40 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 16:07:28 +03:00
Danila Poyarkov 10848d8319 Add app.openpencil.dev links to docs 2026-03-01 16:02:08 +03:00
Danila Poyarkov 0f0ca8db4b Update screenshot, remove duplicate from repo root
Single screenshot lives in packages/docs/public/. README references it
from there. Custom VitePress layout places it between hero and features.
2026-03-01 15:57:01 +03:00
Danila Poyarkov c886daded2 Fix .fig export: map COMPONENT/COMPONENT_SET to SYMBOL (Kiwi enum)
The Kiwi binary schema uses SYMBOL (15) for components, not COMPONENT.
Import already mapped SYMBOL→COMPONENT; export now reverses it.
2026-03-01 15:35:20 +03:00
Danila Poyarkov 1e1feb9cb0 Merge branch 'add-vitepress-docs' 2026-03-01 15:32:45 +03:00
Danila Poyarkov 96d8ab5789 Move docs to packages/docs, add Cloudflare Pages deploy
- Move docs/ → packages/docs/ as @open-pencil/docs workspace package
- Add screenshot.png as hero image on landing page
- Add GitHub Actions workflow for Cloudflare Pages deploy (openpencil.dev)
- Remove vitepress from root devDependencies
- Root docs:* scripts delegate via bun --filter
2026-03-01 15:16:16 +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 123dbe491b Extract DEFAULT_STROKE_MITER_LIMIT constant 2026-03-01 13:53:25 +03:00
Danila Poyarkov 3e9cd236e9 Fix bogus fig-import mappings: expanded and strokeMiterLimit
expanded: was mapped from frameMaskDisabled (unrelated Kiwi field),
now defaults to true. strokeMiterLimit: was mapped from speculative
strokeMiterAngle field that doesn't exist in Kiwi codec, now uses
hardcoded default 4.
2026-03-01 13:48:48 +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 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 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 6268d8b119 Fix double/triple-click text selection
- Track click count with timing/proximity in onMouseDown
- Double-click in text: select word (was only handled in dblclick)
- Triple-click in text: select all
- Double-click to enter editing: select word at click position
- Add selectLine/selectLineAt to TextEditor
2026-03-01 11:36:49 +03:00
Danila Poyarkov 20827a27d3 Deduplicate fig-kiwi parsing, font weight mapping, layout, style runs, text editor
- Extract buildFigKiwi, parseFigKiwiChunks, decompressFigKiwiDataAsync to kiwi-serialize.ts
- Unify styleToWeight across fonts.ts, fig-import.ts, src/engine/fonts.ts
- Merge applyYogaLayout + applyYogaLayoutNested into single recursive fn
- Extract expandRuns helper in style-runs.ts
- Add prepareMove helper to TextEditor for selection extension boilerplate

jscpd: 18 → 9 clones, 1.7% → 0.62%
2026-03-01 11:29:26 +03:00
Danila Poyarkov e3ae61e99d Consolidate font weight mapping and FIG_KIWI_VERSION 2026-03-01 11:08:59 +03:00
Danila Poyarkov f92a8835f4 Deduplicate kiwi and serialization code
Replace src/kiwi/ copies with re-exports from @open-pencil/core.
Extract shared sceneNodeToKiwi into kiwi-serialize.ts, used by
both fig-export.ts and clipboard.ts.

jscpd: 15.6% → 1.7% duplication (35 → 18 clones, -4459 lines)
2026-03-01 10:59:35 +03:00
Danila Poyarkov a32d80f448 Deduplicate render internals: shared resolveToTree, remove dead code 2026-03-01 10:45:39 +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 36e3f14ac4 CLI: add analyze, node, pages, and variables commands
- analyze colors: color palette usage with clustering
- analyze typography: font/size/weight distribution
- analyze spacing: gap/padding values with grid check
- analyze clusters: repeated patterns (potential components)
- node: detailed properties by ID
- pages: list pages with node counts
- variables: list design variables and collections
2026-03-01 10:19:38 +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 88bfacdc9e Rich text style runs with per-selection bold/italic/underline
StyleRun model: array of {start, length, style} with
CharacterStyleOverride (fontWeight, italic, textDecoration, etc.).

Renderer uses ParagraphBuilder.pushStyle/pop for mixed-style text.
TextEditor delegates paragraph building to renderer.buildParagraph().

⌘B/I/U apply to selection range when text is selected, or toggle
whole-node style when no selection. Style runs adjust on
insert/delete to preserve formatting across edits.

Fig roundtrip: import characterStyleIDs + styleOverrideTable from
TextData, export back with deduped style table.

B/I/U/S toggle buttons in TypographySection.
2026-03-01 09:03:38 +03:00
Danila Poyarkov 33d3defcef Canvas-native text editing with phantom textarea
TextEditor class in core — cursor positioning, selection, word
boundaries, line navigation via CanvasKit Paragraph API
(getGlyphPositionAtCoordinate, getRectsForRange).

Renderer draws selection highlights and blinking caret directly
on the canvas. Blue outline around text node during editing.

Phantom textarea captures input, IME composition, clipboard,
arrow keys, Home/End, word/line movement (⌥/⌘ modifiers).

Replaces the old visible <textarea> overlay.
2026-03-01 03:46:43 +03:00
Danila Poyarkov 5ccae709a0 Add system font enumeration via font-kit
Rust: list_system_fonts and load_system_font Tauri commands using
font-kit crate for cross-platform system font access.

TS: font shim detects Tauri runtime and uses invoke() for font
listing/loading instead of queryLocalFonts (unavailable in WKWebView).
2026-03-01 02:56:12 +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 eb6f8914a6 Point core exports to src/index.ts for Vite compatibility 2026-03-01 01:27:04 +03:00
Danila Poyarkov 22e9e7af87 Prepare packages for npm publishing
- Core: conditional exports (bun→src, import→dist), prepublishOnly builds
- CLI: publishConfig, files field
- bun publish resolves workspace:* → actual versions
2026-03-01 01:24:54 +03:00
Danila Poyarkov 33435cc1d1 Make CLI available as bun open-pencil in workspace 2026-03-01 01:21:29 +03:00
Danila Poyarkov 1975fae36e Add @open-pencil/cli with headless .fig file operations
Commands:
- open-pencil info <file> — document stats, node types, fonts
- open-pencil tree <file> — visual node tree with agentfmt
- open-pencil find <file> — search by name/type with rich output
- open-pencil export <file> — render to PNG/JPG/WEBP at any scale

Uses canvaskit-wasm/full for headless CPU rasterization.
All commands support --json for machine-readable output.
agentfmt provides consistent colored terminal formatting.
2026-03-01 01:19:24 +03:00
Danila Poyarkov 6d1cec1606 Extract @open-pencil/core package
Move engine (scene-graph, renderer, layout, codec, kiwi) into
packages/core/ as a Bun workspace package. The app's src/engine/
files become thin re-export shims so all existing imports still work.

- Bun workspace with packages/core
- Zero DOM dependencies in core
- Headless-friendly CanvasKit init (locateFile option)
- Constants split: engine constants in core, UI constants in app
- All 76 unit tests pass, lint + typecheck clean, vite build works
2026-03-01 01:03:07 +03:00