Commit graph

115 commits

Author SHA1 Message Date
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 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 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
Danila Poyarkov 3f176d2b59 Fix layers panel: include visible in LayerNode interface
LayerNode was missing the visible field, so item.value.visible was
always undefined — eye-off icon showed on every layer.
2026-02-28 09:28:13 +03:00
Danila Poyarkov de83a716cd Remove prop drilling from property sections
All sections now get selected node from useNodeProps() composable
instead of receiving it as a prop from PropertiesPanel.
2026-02-28 09:27:03 +03:00
Danila Poyarkov 2af2a280c6 Appearance section: opacity + corner radius + visibility toggle
- Opacity with blend icon, corner radius with radius icon
- Eye/eye-off toggle in section header
- Layers panel: hidden layers dimmed (opacity-50) with permanent
  eye-off indicator
- Removed duplicate corner radius from LayoutSection
2026-02-28 09:20:31 +03:00
Danila Poyarkov bfe9726035 ScrubInput: entire field is draggable, click-to-edit, suffix styling
- Value + suffix area is now the drag handle (cursor: ew-resize)
- Click without drag enters edit mode (2px dead zone)
- Suffix rendered as separate muted span (right-aligned)
- No more tiny icon-only drag area
2026-02-28 09:16:02 +03:00
Danila Poyarkov c4a3ee6b4a Replace plain number inputs with ScrubInput for drag-to-adjust
Fill opacity, stroke opacity, and stroke weight now use ScrubInput
for consistent drag-to-adjust behavior matching W/H/position inputs.
2026-02-28 09:11: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 babeecc76e Polish fill/stroke sections: editable hex, opacity %, eye toggle, minus
- Hex input is now editable (was read-only label)
- Per-fill/stroke opacity percentage input
- Eye/eye-off icon for visibility toggle (on hover)
- Minus button for removal (replaces ×)
- Stroke weight shown below stroke list
2026-02-28 09:06:04 +03:00
Danila Poyarkov 7b0f6c10f7 Fix text alignment icons: use direct template tags for auto-import 2026-02-28 09:01:54 +03:00
Danila Poyarkov 7e9cd46e0b Split PropertiesPanel into sections, add Typography panel
- PropertiesPanel.vue: 513 → 67 lines (composition only)
- Extracted: PositionSection, LayoutSection, AppearanceSection,
  FillSection, StrokeSection, TypographySection
- Shared useNodeProps() composable for update/commit logic
- Typography panel: font family picker with search (queryLocalFonts),
  weight select, font size, line height, letter spacing, text alignment
- Font picker shows each family in its own typeface
- Loads selected font into both CanvasKit and browser on pick
2026-02-28 09:00:25 +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