Commit graph

87 commits

Author SHA1 Message Date
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
Danila Poyarkov 2715f0d8be Text tool: start empty with cursor, blue editing outline
- Create text nodes with empty text instead of 'Text' placeholder
- Immediately enter editing mode on creation (blinking cursor)
- Blue border on textarea during editing (matches Figma)
2026-02-28 08:47:42 +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 bcd8bf6bca Add IconsResolver for auto-import, explicit imports only for dynamic maps 2026-02-28 08:29:39 +03:00
Danila Poyarkov 3ddc671bc5 Replace emoji icons with Lucide via Iconify (unplugin-icons)
- unplugin-icons + @iconify-json/lucide for compile-time icon bundling
- Toolbar: proper cursor, frame, square, pen, type, hand icons
- Layers panel: per-type node icons + chevron-right for expand/collapse
- Remove icon field from ToolDef (icons mapped in components)
2026-02-28 08:24:42 +03:00
Danila Poyarkov bed4e2fdd7 Remove FIGMA_UI3_CDP_FINDINGS.md (research notes, not source) 2026-02-28 08:15:22 +03:00
Danila Poyarkov 2306457b5f Simplify README, add desktop build instructions 2026-02-28 08:14:28 +03:00
Danila Poyarkov 040f3df22f Update README 2026-02-28 08:12:18 +03:00
Danila Poyarkov 71bcc4e6ad Remove src-tauri symlink, Tauri finds desktop/ by scanning 2026-02-28 08:10:59 +03:00
Danila Poyarkov 23f00189e8 Rename src-tauri → desktop, symlink for CLI compat 2026-02-28 08:10:09 +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 e03a01dbe3 Add bun:test unit tests for SceneGraph (15ms) 2026-02-28 07:45:50 +03:00
Danila Poyarkov c1fbf57df2 Skip stability check: toMatchSnapshot, 2.1s → 1.3s 2026-02-28 07:44:20 +03:00
Danila Poyarkov b5eb7f751f Faster E2E: data-ready attr + rAF wait, 4.6s → 2.1s 2026-02-28 07:43:05 +03:00
Danila Poyarkov e8c01fb93c Speed up E2E tests: reuse page, 14s → 4.6s 2026-02-28 07:35:52 +03:00
Danila Poyarkov 40f7b3a648 Playwright visual regression testing with Figma CDP reference
- Playwright E2E tests for OpenPencil: draw, move, delete, undo shapes
- Visual screenshot baselines with toHaveScreenshot()
- SwiftShader for headless WebGL/CanvasKit rendering
- Figma CDP helper for reference tests (connect to Figma Desktop)
- ?test URL param for clean canvas without demo shapes
- Scripts: test, test:update, test:figma, figma:debug
2026-02-28 07:32:15 +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 db45e09c76 Fix vector node fills set to undefined on pen commit 2026-02-28 06:38:31 +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 8e6d0a7f2f Fix snap guides using sibling nodes in absolute coordinates
Snap targets are now siblings of the moving node (not root children),
converted to absolute coordinates. This fixes guides not reflecting
resized objects.
2026-02-28 01:21:45 +03:00
Danila Poyarkov 464a3b6e9c Fix handle/rotation hit testing on rotated nodes
Un-rotate mouse position around node center before testing against
axis-aligned handle positions. Fixes resize handles being unclickable
after rotating a shape.
2026-02-28 01:18:17 +03:00
Danila Poyarkov 3fe58c3d44 Use native grab cursor for rotation handle 2026-02-28 01:14:51 +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