Commit graph

416 commits

Author SHA1 Message Date
Danila Poyarkov e13e910bed Update changelog with zoom smoothness and abspos cache 2026-03-03 11:48:03 +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 c618d486e4 Remove effects showcase section from demo 2026-03-03 10:17:43 +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 faa02d8ab0 Update changelog with effects fixes, zoom optimization, and tests 2026-03-03 09:56:41 +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 6f4439855e Update README roadmap and tool counts 2026-03-02 23:43:39 +03:00
Danila Poyarkov 16db9feafa Add zoom/pan E2E tests and pipeline benchmark
Tests cover:
- Wheel zoom updates viewport (ctrlKey + wheel → applyZoom)
- Wheel pan updates viewport (plain wheel → pan)
- Rapid wheel events coalesce without errors (50 events in one frame)
- shallowReactive selection replace triggers UI update
- useRafFn loop picks up renderVersion changes (fill color change)
- useRafFn loop picks up selection changes (selection border)
- Pipeline throughput benchmark (500 iterations each)
2026-03-02 23:43:39 +03:00
Danila Poyarkov a5fe5b0cf4 Optimize zoom/pan smoothness with shallowReactive, useRafFn, and input coalescing
- Switch editor state from reactive() to shallowReactive() to avoid deep
  Proxy wrapping of Set, arrays, and nested objects
- Replace Vue watch on renderVersion with useRafFn dirty-flag loop that
  polls for changes without going through Vue's watcher scheduler
- Coalesce wheel events: accumulate deltaX/deltaY/zoomDelta between
  frames and flush once per rAF instead of per event
- Coalesce Safari gesturechange events: buffer the latest scale/position
  and apply once per rAF
- Fix Set.delete() on shallowReactive state to use replace pattern
2026-03-02 23:43:39 +03:00
Danila Poyarkov b2aa28cc77
Merge pull request #20 from tela/test/mcp-edge-cases
Add MCP server edge-case tests
2026-03-02 23:38:49 +03:00
Tela Andrews 274c9f7d61 Add MCP server edge-case tests for find_nodes and Zod validation
Covers a few gaps in the existing MCP test suite:
- find_nodes filtering by type (wasn't exercised at all)
- create_shape with an invalid type enum (Zod enforcement)
- create_shape with a missing required param (Zod enforcement)
2026-03-02 11:16:19 -08:00
Danila Poyarkov 3e178edb50 Exclude non-app directories from Vite file watcher
Prevents unnecessary HMR triggers from desktop (Rust), cli, mcp,
docs, tests, openspec, worktrees, .github, and .pi directories.
2026-03-02 21:36:54 +03:00
Danila Poyarkov 69afd3311f Update changelog with AppSelect and font picker fixes 2026-03-02 21:33:37 +03:00
Danila Poyarkov 25233294a1 Replace native <select> with reka-ui AppSelect component
- New AppSelect.vue: generic wrapper around reka-ui Select primitives
- Replace all 4 native <select> elements (font weight, effect type,
  export scale, export format)
- Fix font picker dropdown truncating names (min-w-56)
- Show explanation when local fonts unavailable (Safari/Firefox)
2026-03-02 21:33:04 +03:00
Danila Poyarkov 412e614d07 Fix font picker dropdown truncating font names
Set min-w-56 on popover so it doesn't shrink below 224px
when the trigger button is narrow.
2026-03-02 21:18:57 +03:00
Danila Poyarkov 4f492d1f8a Update changelog with new tools and MCP server tests 2026-03-02 21:16:46 +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 6f88a2e47e Merge branch 'port-tools' 2026-03-02 19:51:11 +03:00
Danila Poyarkov afb480d05d Update changelog with layers context menu and CI improvements 2026-03-02 19:46:04 +03:00
Danila Poyarkov 90e29f13a2 Add context menu to layers panel
Extract menu items into shared NodeContextMenuContent component,
used by both the canvas and layers panel context menus.
Right-clicking a layer selects it and shows the same menu.
2026-03-02 19:45:09 +03:00
Danila Poyarkov c3869908ee Format core package 2026-03-02 19:43:44 +03:00
Danila Poyarkov 7534c46583 Truncate long node names in export button 2026-03-02 19:43:42 +03:00
Danila Poyarkov 5147bc1fec Update changelog with new tools and color constants 2026-03-02 19:35:18 +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 a3236a08a0 Update changelog with multi-file tabs and clipboard fix 2026-03-02 18:56:59 +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 b7a634f1ae Skip already-published versions in CI npm publish
Prevents re-run failures when a package was already published.
Real errors (auth, build) still fail the job.
2026-03-02 18:37:00 +03:00
Danila Poyarkov af9b9fd0fa Replace store thunks with a late-binding proxy
useEditorStore() now returns a Proxy that delegates to the current
active store at access time. No more store() wrapper functions,
try/catch guards, or passing stores through function parameters.
2026-03-02 18:36:27 +03:00
Danila Poyarkov a6634b9543 Wire native Tauri menu to tab actions
Map File → New to createTab() and File → Close Tab to closeTab().
Rename menu labels from 'New File'/'Close Window' to 'New'/'Close Tab'.
2026-03-02 18:36:27 +03:00
Danila Poyarkov 1f652b5e90 Use reka-ui Tabs for tab bar accessibility
Arrow key navigation, role=tablist/tab, aria-selected,
focus management, and screen reader support.
2026-03-02 18:36:27 +03:00
Danila Poyarkov b3438cd272 Fix active tab text invisible against same-color background 2026-03-02 18:36:27 +03:00
Danila Poyarkov 1b4dc08c54 Add multi-file tabs
Open multiple documents in tabs within a single window, like Figma.

- Tab bar appears when 2+ documents are open, with close (×) and new (+) buttons
- ⌘N / ⌘T create a new empty tab
- ⌘W closes the active tab (last tab closes and reopens as Untitled)
- ⌘O opens .fig files in a new tab (reuses current tab if Untitled with no changes)
- Middle-click closes a tab
- Each tab has its own SceneGraph, UndoManager, viewport, and selection state
- Switching tabs remounts the canvas to rebind the Skia renderer to the active store
- Keyboard shortcuts, menu actions, and AI tools all operate on the active tab
2026-03-02 18:36:27 +03:00
Danila Poyarkov 7d98201548 Release v0.4.2 2026-03-02 18:14:35 +03:00
Danila Poyarkov 877350a3af Auto-populate GitHub Releases from CHANGELOG.md
Use ffurrer2/extract-release-notes@v2 to extract the version section
and pass it as releaseBody to the Tauri action.

Switch changelog headings to Keep a Changelog format (## [X.Y.Z])
to match the action's expected pattern.
2026-03-02 18:12:36 +03:00
Danila Poyarkov 8bb62d908f Update changelog with clipboard import fixes 2026-03-02 18:07:59 +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 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 f54871fae2 Compute layouts after clipboard paste 2026-03-02 17:34:53 +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 e0c5820183 CI: publish releases as non-draft 2026-03-02 15:54:43 +03:00
Danila Poyarkov 7eb74c3969 Release v0.4.1 2026-03-02 15:51:34 +03:00
Danila Poyarkov ebb85e8f20 Improve scene cache regression test for font load race 2026-03-02 15:51:02 +03:00