Commit graph

61 commits

Author SHA1 Message Date
Danila Poyarkov f31ded3c08 Fix .fig export: include thumbnail.png required by Figma
Figma's importer requires thumbnail.png to exist in the ZIP archive.
Without it, import fails with 'Internal error during import' regardless
of how correct the Kiwi data is.

Also: STORE compression for canvas.fig (already internally compressed),
and match Figma's default DOCUMENT/CANVAS fields.
2026-02-28 15:52:35 +03:00
Danila Poyarkov dc3c5a47b1 Fix .fig import/export round-trip
- Parse fig-kiwi container format properly: strip header, extract
  schema and data chunks, decompress (Zstd or deflate)
- Import uses the schema embedded in the fig-kiwi container (supports
  newer Figma files with unknown fields)
- Export uses the codec's schema via getCompiledSchema/getSchemaBytes
- Consolidate schema loading: remove duplicate figma-schema.bin fetch
  from clipboard and export; both now use codec's initCodec()
- Fix IS_TAURI for non-browser environments (SSR/Bun)
2026-02-28 15:12:24 +03:00
Danila Poyarkov 495e419e79 Zstd compression via Tauri Rust command, deflate fallback for browser
- Added zstd crate to Rust deps and zstd_compress Tauri command
- Export uses native Zstd in Tauri, deflate in browser (our reader
  handles both)
- Removed zstd-wasm dependency (decompress-only, didn't work)
2026-02-28 14:56:06 +03:00
Danila Poyarkov 197323162f Fix .fig export: Zstd compression, IS_TAURI constant, fs permissions
- Replace deflate with Zstd compression via zstd-wasm for .fig export
  (Figma expects Zstd, not zlib deflate)
- Extract IS_TAURI constant from repeated window check
- Fix Tauri fs permissions: use allow-write-file / allow-read-file
  with glob scope instead of bare allow-write / allow-read
2026-02-28 14:48:22 +03:00
Danila Poyarkov 150bd7a256 Merge branch 'save-fig' 2026-02-28 14:26:47 +03:00
Danila Poyarkov 0524092f3e Merge branch 'autolayout-tests' 2026-02-28 14:04:30 +03:00
Danila Poyarkov 55c3ea35dc Section title text inverts based on pill background luminance 2026-02-28 14:03:57 +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 59a98f9186 Show frame name label for direct children of sections 2026-02-28 13:51:18 +03:00
Danila Poyarkov 584661315f Section title hit test uses exact pill width from font metrics
Moved section title hit testing from scene-graph (no font access) to
renderer where exact pill width is computed from glyph widths. Only
the title pill area is clickable, not the full width above the section.
Hit test passed through use-canvas → use-canvas-input via callback.
2026-02-28 13:46:39 +03:00
Danila Poyarkov 60ed0f195d Fix missing import for section title hit testing 2026-02-28 13:43:02 +03:00
Danila Poyarkov 715f6e3281 Nested section titles inside body, auto-adopt siblings on creation
- Top-level section titles render above the body
- Nested section titles render inside at the top-left
- Drawing a section auto-reparents any siblings fully contained
  within its bounds, adjusting coordinates to relative
- Undoable adopt operation with snapshotted positions
2026-02-28 13:39:04 +03:00
Danila Poyarkov adc5595f25 Section titles: fixed screen size, ellipsis truncation
Title pill rendered in screen space (UI overlay layer), not canvas
space. Font size stays constant regardless of zoom. Text truncates
with ellipsis when section is narrower than the full name.
Pill width capped to section's screen width.
2026-02-28 13:35:07 +03:00
Danila Poyarkov cadbd1330e Merge hover-highlight, implement sections
Sections (Figma-like):
- SECTION node type with rounded rect body + title pill above
- S shortcut, Frame flyout includes Section
- Default dark gray fill + subtle stroke, 5px corner radius
- Sections are containers with click-through hit testing
- Drag outside section reparents to parent

Toolbar flyouts with Reka UI DropdownMenu:
- Split button: tool icon activates tool, chevron opens dropdown
- Frame flyout: Frame / Section
- Rectangle flyout: Rectangle / Ellipse / Line
- Active sub-tool icon shown on parent button
2026-02-28 13:31:21 +03:00
Danila Poyarkov 086fb7d652 File → Save / Save As (.fig export) 2026-02-28 13:28:21 +03:00
Danila Poyarkov 3f7f30aaf0 Merge branch 'hover-highlight' 2026-02-28 13:17:48 +03:00
Danila Poyarkov adc95c7379 Shape-aware hover highlight for ellipses, rounded rects, vectors 2026-02-28 13:12:53 +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 b10d427296 Highlight objects on hover (Figma-style) 2026-02-28 13:08:33 +03:00
Danila Poyarkov ab9b4ac37c Extract remaining magic numbers to constants
Move all hardcoded numeric values from renderer and canvas-input
into named constants in constants.ts:

- Pen tool: vertex radius, close threshold, path stroke width
- Selection: handle half size, rotation handle radius, pill
  dimensions, marquee/dash/drop-highlight alphas
- Layout indicator stroke width
- Rotation: snap degrees, hit offset
- Text defaults: initial width/height
- Zoom sensitivity
- Ruler: target pixel spacing, major tick tolerance
- Parent outline: dash length
2026-02-28 12:56:38 +03:00
Danila Poyarkov ffd305c125 Page properties panel with canvas background color picker
When nothing is selected, the right panel shows Page section with
a color picker for the canvas background (like Figma). The page
color is stored in reactive state and passed to the renderer.
2026-02-28 11:42:13 +03:00
Danila Poyarkov 159ee6b7c9 Fix production build type errors 2026-02-28 11:37:02 +03:00
Danila Poyarkov ef65ff8b83 Default clipsContent to false (matching Figma) 2026-02-28 11:25:26 +03:00
Danila Poyarkov 66e68f7349 Drag outside frame reparents to parent, clip content toggle
- Dragging a child fully outside its parent frame's bounds
  automatically reparents it to the grandparent (like Figma)
- clipsContent property on SceneNode (default true for frames)
- Renderer respects clipsContent — unclipped frames show
  children that overflow
- Clip content checkbox in Layout panel for frames
2026-02-28 11:21:03 +03:00
Danila Poyarkov cb8bbcf289 Extract magic numbers to constants
Move ruler config (colors, sizes, tick ratios, badge dimensions),
font sizes, pen handle radius, and outline alpha from renderer
inline values to src/constants.ts.
2026-02-28 09:52:56 +03:00
Danila Poyarkov 68dc0ee888 Align ruler tick labels and badge text to same baseline
Horizontal: all text at R * 0.65 y-position
Vertical: translate to R * 0.65 before rotation
Badge text uses same baseline as tick labels
2026-02-28 09:49:14 +03:00
Danila Poyarkov 175565bb16 Ruler coordinate badges at selection start/end
Blue rounded-rect badges with white text showing exact coordinates
at both edges of the selection highlight on each ruler axis.
2026-02-28 09:44:59 +03:00
Danila Poyarkov 5b10d3b7fd Fix ruler highlight: translucent band, remove coordinate labels
Selection highlight uses 30% opacity blue instead of solid opaque.
Removed white coordinate labels on highlight bands — tick numbers
are sufficient.
2026-02-28 09:42:43 +03:00
Danila Poyarkov 2152a9b2d6 Fix vertical ruler text clipping
Clip both ruler strips to their bounds, reposition vertical text
to stay centered within the 20px ruler width.
2026-02-28 09:39:08 +03:00
Danila Poyarkov 9767ead116 Add ?no-rulers URL param to disable canvas rulers 2026-02-28 09:36:39 +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 8d35d4dbaf Deduplicate hex formatting: colorToHexRaw() in engine/color.ts
Removes duplicate hexString() wrappers from FillSection, StrokeSection,
and ColorPicker — all now use culori-based colorToHexRaw().
2026-02-28 09:08:40 +03:00
Danila Poyarkov e3c80fc790 System font loading via Local Font Access API
- Font service (src/engine/fonts.ts) loads system fonts on demand
- Same font bytes registered in both CanvasKit and browser (FontFace API)
- Eliminates rendering mismatch between canvas and text editing overlay
- queryLocalFonts() for enumeration and blob() for font data
- Bundled Inter-Regular.ttf as fallback when API unavailable
- Renderer uses font service instead of hardcoded fetch
2026-02-28 08:54:53 +03:00
Danila Poyarkov 5c0afd4d06 Pass fontWeight to both CanvasKit text style and textarea overlay 2026-02-28 08:49:28 +03:00
Danila Poyarkov 98eeb9582c Fix text editing: hide canvas text/selection during edit, load Inter font
- Skip rendering text node on canvas while textarea overlay is active
- Skip selection handles for the node being edited
- Remove border from textarea (was causing double outline)
- Add @font-face for Inter-Regular.ttf so textarea matches CanvasKit
2026-02-28 08:45:37 +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 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 f741eaa296 Fix open paths rendering as closed
Only close a loop path when the last segment ends at the first
segment's start vertex.
2026-02-28 07:15:07 +03:00
Danila Poyarkov b4c7700f05 Decode/encode Figma vectorNetworkBlob binary format
Reverse-engineered the undocumented binary blob format that Figma uses
to serialize vector networks in both .fig files and clipboard data:

  Header:  [numVertices:u32, numSegments:u32, numRegions:u32]
  Vertex:  [styleOverrideIdx:u32, x:f32, y:f32]
  Segment: [styleOverrideIdx:u32, start:u32, tsX:f32, tsY:f32, end:u32, teX:f32, teY:f32]
  Region:  [windingRule:u32, numLoops:u32, {numSegs:u32, segIdx...}...]

- decodeVectorNetworkBlob() and encodeVectorNetworkBlob() in vector.ts
- Clipboard paste (Figma → OpenPencil) resolves blob references
- Clipboard copy (OpenPencil → Figma) encodes blobs into the message
- .fig file import resolves vectorNetworkBlob references from blobs array
2026-02-28 07:14:17 +03:00
Danila Poyarkov 77d8000b36 Fix pen tool preview line to (0,0) on first click
penCursorX/Y now null until mouse actually moves, preventing
the preview line from drawing to canvas origin on first vertex.
2026-02-28 06:42:14 +03:00
Danila Poyarkov bf054e386b Pen tool with vector network model
Full Figma-compatible vector network: vertices, segments (cubic
bezier with tangent offsets), and regions (filled closed paths).

Pen tool interaction:
- Click to place vertices, click+drag to set bezier tangent handles
- Click first vertex to close path (with visual highlight)
- Enter to commit open path, Escape to cancel
- Preview line from last vertex to cursor
- Tangent handle visualization (lines + control points)

Rendering:
- Vector nodes rendered via CanvasKit Path API
- Converts vector network to path: regions as closed fills,
  segments as open strokes
- Proper cubic bezier from tangentStart/tangentEnd offsets

Data model:
- VectorNetwork, VectorVertex, VectorSegment, VectorRegion types
- vectorNetwork field on SceneNode (nullable)
- computeVectorBounds for node sizing
- HandleMirroring enum for future handle behavior
2026-02-28 06:37:25 +03:00
Danila Poyarkov 179acd0edb Wire undo/redo into all editor operations
Undoable operations:
- Create shape (undo deletes it)
- Delete (undo restores with original z-order)
- Move (was already wired, cleaned up)
- Resize (commitResize on mouseUp)
- Rotate (commitRotation on mouseUp)
- Duplicate (undo deletes copies, restores selection)
- Paste (undo deletes pasted nodes)
- Text editing (undo restores previous text)
- Wrap in auto layout (undo unwraps and restores positions)
- Set layout mode (undo restores all layout properties)
- Fill/stroke add/remove/toggle (via updateNodeWithUndo)
- Alignment and padding changes
- Properties panel scrub inputs: emit commit event on
  pointerup/blur with previous value for undo push
- UndoManager.push() added for entries where forward was
  already applied
2026-02-28 06:28:17 +03:00
Danila Poyarkov bd107634d5 Rotation-aware snap guides and selection bounds
Both snap targets and moving selection bounds now compute AABB
from rotated corners, so guides align to the visual edges of
rotated shapes.
2026-02-28 01:24:23 +03:00
Danila Poyarkov 946ea74eba Frame name label and size widget on selection
- Blue frame name above top-level frames (root children only)
- Blue pill with W × H below selection bounding box
- Pre-create labelFont/sizeFont once on font load to avoid
  CanvasKit raw-to-smart pointer error
2026-02-28 01:11:20 +03:00
Danila Poyarkov d60045ab16 Only show dashed parent outline for deeply nested frames
Top-level frames (direct children of root) don't get the dashed
outline — matching Figma where it only appears for nested hierarchy.
2026-02-28 01:05:20 +03:00
Danila Poyarkov 362e5d2cee Dashed parent frame outline on child selection
When selecting a node inside a frame, the parent frame gets a
dashed blue outline (50% opacity, 4px dash) showing hierarchy
context — matching Figma's behavior.
2026-02-28 01:04:34 +03:00
Danila Poyarkov f6cee76d22 Auto-layout drag reordering with insertion indicator
Dragging inside auto-layout frames:
- 8px dead zone before breaking out of auto-layout flow
- Blue insertion indicator line between children, computed from
  cursor position relative to child midpoints
- Drop reorders children in the parent's childIds array and
  recomputes layout via Yoga
- Dragging from outside into an auto-layout frame shows the
  indicator and inserts at the correct index

SceneGraph.reorderChild() added for index-based child insertion.
Renderer draws the indicator as a 2px blue line (horizontal for
vertical layout, vertical for horizontal layout).
2026-02-28 00:00:45 +03:00
Danila Poyarkov 4de83542d3 Auto-layout with Yoga WASM integration
Layout engine (src/engine/layout.ts):
- Yoga WASM adapter mapping SceneNode layout props to Yoga nodes
- Supports vertical/horizontal direction, padding (per-side), gap,
  primary/counter axis alignment, HUG/FIXED/FILL sizing, wrap,
  absolute positioning, flex grow, stretch
- Bottom-up computation: nested auto-layout frames resolve correctly
- Layout runs automatically on any property change to affected frames

Scene graph:
- New layout properties on SceneNode: layoutMode, itemSpacing,
  padding (4 sides), primaryAxisAlign, counterAxisAlign,
  primaryAxisSizing, counterAxisSizing, layoutWrap, layoutGrow,
  layoutPositioning, layoutAlignSelf, counterAxisSpacing

Properties panel:
- Auto-layout section for frames: direction toggles (none/vertical/
  horizontal), gap, padding (4 inputs), primary and counter axis
  alignment dropdowns

Import/export:
- .fig import maps stackMode/stackSpacing/stackPadding* fields
- Clipboard paste maps all Figma stack* properties
- Clipboard copy exports layout as Kiwi stackMode/stackSpacing/etc.
2026-02-27 23:51:21 +03:00
Danila Poyarkov 49e7514dcf Fix copy: build fig-kiwi binary synchronously in copy event handler
navigator.clipboard.write() inside .then() loses user gesture context
and gets blocked by the browser. The schema is prefetched on app init
and cached, so encoding is done synchronously in clipboardData.setData
within the native copy event.
2026-02-27 23:32:56 +03:00