Commit graph

1926 commits

Author SHA1 Message Date
Danila Poyarkov 5aae8d01fc Include content size in Zstd frame header
Figma requires the Zstd content size to be present in the frame header.
Use zstd::Encoder with include_contentsize(true) instead of encode_all.
2026-02-28 16:03:58 +03:00
Danila Poyarkov 2b91f51de8 Generate thumbnail for .fig export
Render the current page to a 400×225 offscreen CanvasKit surface,
fit content with padding, and include as thumbnail.png in the ZIP.
Falls back to a 1×1 placeholder when renderer is unavailable (tests).
2026-02-28 15:57:15 +03:00
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 8d3d3444ad Tauri native dialog for Save/Open with fs plugin 2026-02-28 13:54:21 +03:00
Danila Poyarkov 59a98f9186 Show frame name label for direct children of sections 2026-02-28 13:51:18 +03:00
Danila Poyarkov b83dfcdbe4 Sections are top-level only: can't nest inside frames or groups
Per Figma behavior, sections can only live in pages (CANVAS) or
other sections. Reparent guard skips sections dropped into frames/
groups. Drop target highlight suppressed when dragging sections
over non-section containers.
2026-02-28 13:49:58 +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 fd2e38e5ee Fix page rename: blur commits, Enter/Escape just blur 2026-02-28 13:15:53 +03:00
Danila Poyarkov 803b69eee6 Split PagesPanel into separate component, inline page rename
- PagesPanel.vue: pages list with click to switch, double-click
  for inline rename (input field, Enter/Escape/blur commits)
- LayersPanel.vue: removed pages logic, composes PagesPanel
- Layer drag-drop uses currentPageId instead of rootId
2026-02-28 13:14:03 +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 9d6a78fd7c Add Quit and About to Windows/Linux menus
On macOS these live in the app submenu. On Windows/Linux:
- File → Quit (at bottom, with separator)
- Help → About OpenPencil (at bottom, with separator)
2026-02-28 13:00:23 +03:00
Danila Poyarkov 2ab99ac4fe Windows build support: conditional macOS app menu, devtools feature
- Gate OpenPencil app submenu (About/Services/Hide/Quit) behind
  #[cfg(target_os = "macos")] — Windows/Linux don't have it
- Enable tauri "devtools" feature in Cargo.toml (required for
  open_devtools/close_devtools on non-macOS)
2026-02-28 12:58:34 +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 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 ce46dea54d Sort nodes by position when wrapping in auto-layout, fix null node crash
Nodes are sorted top-to-bottom (then left-to-right) before
reparenting into auto-layout frame. Positions are snapshotted
as plain IDs before mutation to avoid reactive proxy issues.

Property panel sections guard against null node with v-if,
preventing crashes during selection transitions.
2026-02-28 10:36:43 +03:00
Danila Poyarkov e6756207a5 Group/ungroup selection (⌘G / ⇧⌘G)
Group wraps selected nodes in a GROUP node, preserving
z-order and visual position. Ungroup dissolves a group,
reparenting children to the group's parent. Both undoable.
2026-02-28 10:27:04 +03:00
Danila Poyarkov 2f33165f1d Add Developer Tools menu item (⌘⌥I) 2026-02-28 10:21:35 +03:00
Danila Poyarkov c9fd34bd48 Wire menu events to frontend
Rust emits 'menu-event' with item ID to webview.
useMenu() composable listens and dispatches actions.
Extract openFileDialog() to use-menu.ts (shared with use-keyboard).
2026-02-28 10:20:00 +03:00
Danila Poyarkov 914abb28d9 README: add cargo install tauri-cli step 2026-02-28 10:05:25 +03:00
Danila Poyarkov bf45ac0f65 Native macOS menu bar
- OpenPencil: About, Services, Hide, Quit
- File: New, Open, Save, Save As, Export, Close
- Edit: Undo, Redo, Cut, Copy, Paste, Paste in Place,
  Select All, Duplicate, Delete
- View: Zoom In/Out/Fit/100%, Toggle Rulers/Grid/UI, Fullscreen
- Object: Group, Ungroup, Frame Selection, Bring to Front,
  Send to Back, Flip H/V
- Window: Minimize, Maximize, Close
- Help: Keyboard Shortcuts

All with standard macOS accelerators.
2026-02-28 10:00:37 +03:00
Danila Poyarkov b0a35a4d24 Fix rotation icon to rotate-ccw, add rotate 90° button 2026-02-28 09:57:45 +03:00
Danila Poyarkov c66e466fb2 Position section: alignment buttons, rotation icon, flip H/V
- 6 alignment buttons (left/center/right, top/center/bottom) for
  multi-select alignment
- Rotation input with rotate-cw icon and degree suffix
- Flip horizontal and flip vertical buttons
- Matching Figma's Position panel layout
2026-02-28 09:55:02 +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 d0048114a7 Fix layers panel chevron direction 2026-02-28 09:29:57 +03:00