Commit graph

186 commits

Author SHA1 Message Date
Danila Poyarkov 8eff364dcd Use sceneVersion instead of renderVersion for UI panels
renderVersion bumps on every pan/zoom/hover — it's the canvas redraw signal.
sceneVersion only bumps when the scene graph actually changes.

Panels (properties, layers, context menu, pages, export preview) now
depend on sceneVersion, so they don't re-render during navigation.
2026-03-01 00:33:14 +03:00
Danila Poyarkov fee028b49f Fix export preview flicker: keep old image until new one is ready, avoid renderVersion deps 2026-03-01 00:32:17 +03:00
Danila Poyarkov ee16ce89de Split requestRender/requestRepaint to avoid export preview updates on pan/zoom 2026-03-01 00:29:42 +03:00
Danila Poyarkov cb6c042c07 Merge remote-tracking branch 'origin/export-image' 2026-03-01 00:20:00 +03:00
Danila Poyarkov 103c5617cf Re-encode JPG/WEBP via OffscreenCanvas instead of CanvasKit
The default CanvasKit WASM build lacks JPEG/WEBP encoders. Instead of
switching to the full build (+1MB), render PNG via CanvasKit then
re-encode to the target format using OffscreenCanvas.convertToBlob().
2026-03-01 00:11:48 +03:00
Danila Poyarkov 7d4b821e77 Use full CanvasKit build for JPEG/WEBP encoding support
The default canvaskit.wasm doesn't include JPEG/WEBP encoders
(encodeToBytes returns null). The full build is only ~1MB larger.
2026-03-01 00:09:35 +03:00
Danila Poyarkov 7a5a77c098 Render export preview at panel-appropriate resolution
Scale preview to fit 480px width (covers retina panels) instead of
fixed 0.5x which was far too blurry.
2026-03-01 00:05:19 +03:00
Danila Poyarkov 2fb22786c1 Add types/shared code and test strategy to CLI plan 2026-03-01 00:03:26 +03:00
Danila Poyarkov e8223287df Validate all CLI risks: CanvasKit CPU, headless fonts, extraction scope 2026-03-01 00:00:31 +03:00
Danila Poyarkov 55b0b37789 Add image export (PNG/JPG/WEBP) with shared rendering
Extract renderThumbnail and renderNodesToImage into render-image.ts,
reusing the same offscreen CanvasKit surface approach that fig-export
used for thumbnails.

- ExportSection in properties panel with scale/format selectors,
  multi-export support, and live preview with checkerboard background
- Export via context menu and ⇧⌘E shortcut
- Tauri save dialog, File System Access API, and download fallback
- JPG renders with white background, PNG/WEBP with transparency
2026-02-28 23:59:39 +03:00
Danila Poyarkov 44e914d410 Detailed CLI & headless implementation plan 2026-02-28 23:58:47 +03:00
Danila Poyarkov f61d8c0d83 Add headless rendering plan (CanvasKit CPU rasterizer) 2026-02-28 23:54:10 +03:00
Danila Poyarkov 7ff16f70e0 Fix app identifier to net.dannote.open-pencil, add CLI/headless plan to PLAN.md 2026-02-28 23:51:22 +03:00
Danila Poyarkov 89e48c553e Update README features, highlight ~5 MB install and offline 2026-02-28 23:38:46 +03:00
Danila Poyarkov 00a2923007 Add screenshot to README 2026-02-28 23:36:55 +03:00
Danila Poyarkov d1979473c3 Add figma-use reference to AGENTS.md 2026-02-28 23:27:36 +03:00
Danila Poyarkov 7a803772a5 Expand AGENTS.md with lessons from all project sessions
Added from ~/Development sessions: scene graph behaviors,
auto-layout gotchas, section/frame rendering rules, .fig
round-trip testing, Tauri permissions, icon conventions,
ScrubInput cursor handling, culori usage, vueuse hooks.
2026-02-28 23:23:46 +03:00
Danila Poyarkov 199520ae86 Expand AGENTS.md with lessons from all sessions 2026-02-28 23:22:07 +03:00
Danila Poyarkov fdb3fec236 Add AGENTS.md 2026-02-28 23:18:55 +03:00
Danila Poyarkov d33efa1228 Update PLAN.md with Yoga CSS Grid PR tracking details 2026-02-28 23:17:39 +03:00
Danila Poyarkov 70c88dd72b Replace relative imports with @/ alias
Add @/ → src/ alias in vite.config.ts and tsconfig.json.
Rewrite all ../ imports across 32 files to use @/ paths.
Same-directory ./ imports kept as-is.
2026-02-28 23:08:45 +03:00
Danila Poyarkov 6eca2f8432 Consolidate shared types and remove inline duplicates
- Move Vector, Matrix, Rect to src/types.ts as shared primitives
- GradientTransform is now a type alias for Matrix
- Consolidate Window API declarations (File System Access,
  queryLocalFonts) in src/global.d.ts
- Remove duplicate KiwiNodeChange from fig-export.ts and
  clipboard.ts, reuse NodeChange from kiwi/codec.ts
- Add missing fields to codec NodeChange: frameMaskDisabled,
  stackWrap, stackPositioning, stackChildPrimaryGrow, vectorData
- Widen codec Paint.type and Effect.type unions to match scene-graph
- Replace inline Rect definitions across 5 files with shared type
- fig-export.ts fillPaints now constructs typed Paint objects
2026-02-28 23:06:47 +03:00
Danila Poyarkov 18609e00c3 Fix all lint, typecheck, and formatting issues
- 0 lint warnings (was 66), 0 type errors (was 30+)
- Add typed interfaces for Paint.stops/transform, Effect.blendMode,
  NodeChange.strokeCap/strokeJoin/dashPattern in kiwi codec
- Add CompiledSchema.encodePaint/encodeNodeChange methods
- Remove all no-explicit-any: use Window declaration merging for
  queryLocalFonts, showOpenFilePicker, showSaveFilePicker
- Remove all non-null assertions: add guards, use ?. operator,
  ?? fallbacks throughout
- Add SceneGraph.copyProp typed helper for instance sync
- Suppress kiwi-schema lint warnings via oxlint override
- Remove unused imports, variables, parameters
- Use proper CanvasKit StrokeCap/StrokeJoin enums in renderer
- Run oxfmt with import sorting
2026-02-28 22:57:33 +03:00
Danila Poyarkov 046f23b138 Format codebase with oxfmt 2026-02-28 22:41:34 +03:00
Danila Poyarkov e4eebf2a87 Move number input spinner hiding to global CSS 2026-02-28 22:40:01 +03:00
Danila Poyarkov d43fac94ca Hide number input spinners in Firefox
Add [appearance:textfield] alongside the WebKit spinner hide rule.
Firefox ignores ::-webkit-inner-spin-button but respects
appearance: textfield to remove native number spinners.
2026-02-28 22:38:36 +03:00
Danila Poyarkov 97715ba662 ScrubInput: move cursor and pointerdown to container
Move ew-resize cursor and pointerdown handler from inner spans
to the outer div for simpler event handling.

Note: ew-resize/col-resize cursors are broken in Safari 26.2
(WebKit bug 303845, fixed in Safari 26.3 Beta).
2026-02-28 22:36:39 +03:00
Danila Poyarkov 872fb4f540 Fix splitter resize handle cursor and hit area
Use a real 8px-wide element with negative margins instead of a
pseudo-element on a zero-width handle. The visible line is 1px
centered inside, highlighted on hover and drag. Works in Safari.
2026-02-28 22:28:22 +03:00
Danila Poyarkov 04479460d0 Throttle WebGL surface recreation during panel resize
ResizeObserver was recreating the surface on every tick. Now
coalesced via rAF so it fires at most once per frame — surface
always matches the canvas size so no skew.
2026-02-28 22:26:05 +03:00
Danila Poyarkov a89e886981 Make left and right panels resizable with reka-ui Splitter
Uses SplitterGroup/SplitterPanel/SplitterResizeHandle. Panels
default to 15% width, min 10%, max 30%. Layout persists across
reloads via auto-save-id. Resize handle highlights blue on hover.
2026-02-28 22:22:39 +03:00
Danila Poyarkov ffc1b7f56b Fix Polygon/Star tools not drawing on canvas
Missing from TOOL_TO_NODE map in use-canvas-input.
2026-02-28 22:19:01 +03:00
Danila Poyarkov 2ab75ad25a Add Polygon and Star tools with rendering and flyout shortcuts
- POLYGON (triangle icon) and STAR (star icon) in the shapes flyout
- Renderer draws regular polygons and stars using pointCount and
  starInnerRadius properties on SceneNode
- Flyout dropdown now shows keyboard shortcuts (R, L, O) aligned
  right like Figma's context menu
- Polygon defaults to 3 sides, Star to 5 points with 0.38 inner ratio
- Fill, stroke, hover highlight, and selection outline all work
2026-02-28 21:04:02 +03:00
Danila Poyarkov af3db9f0f1 Add GitHub Actions workflow for Windows + macOS builds
Triggers on version tags (v*) or manual dispatch.
Builds .msi/.exe for Windows x64/arm64 and .dmg for macOS
x64/arm64. Creates a draft GitHub release with all artifacts.
2026-02-28 20:55:26 +03:00
Danila Poyarkov cd9b096a98 Align + and − buttons across Fill, Stroke, Effects sections
Both are now size-5 flex-centered boxes with the same hover style,
so they sit at the same right edge and have equal hit targets.
2026-02-28 20:53:39 +03:00
Danila Poyarkov b220fe13e6 Show effect controls inline instead of floating popover
The properties panel has overflow-y: auto which clips absolute
positioned popovers. Expand controls inline below the effect
row instead — simpler and always visible.
2026-02-28 20:49:23 +03:00
Danila Poyarkov c4f6f1caaa Redesign Effects section to match Figma's layout
- Compact row: color swatch + type dropdown + eye toggle + minus
- Expanded controls in a floating popover anchored to the left
- Labeled rows: Position (X/Y), Blur, Spread, Color with hex + opacity
- Type dropdown in popover header with X close button
- Eye/minus icons hidden until row hover (matches fill/stroke rows)
2026-02-28 20:40:03 +03:00
Danila Poyarkov 8b0bbc85b7 Merge branch 'effects-panel' 2026-02-28 20:13:34 +03:00
Danila Poyarkov daa949e9e3 Add Effects section to properties panel
Support adding, removing, and editing drop shadows, inner shadows,
layer blurs, background blurs, and foreground blurs. Each effect row
has visibility toggle, type dropdown, and remove button. Expanded
view shows offset, blur, spread, and color controls for shadows,
or just blur radius for blur effects.
2026-02-28 20:12:37 +03:00
Danila Poyarkov 1a0c6f62df Add independent corner radius controls to Appearance section 2026-02-28 20:12:31 +03:00
Danila Poyarkov a862cc6ca6 Remove 'Create Instance' button from properties panel
Figma doesn't have this — instances are created by dragging from
the Assets panel or duplicating an existing instance.
2026-02-28 20:05:06 +03:00
Danila Poyarkov 4886d465ad Fix ⌥⌘K / ⌥⌘B / ⇧⌘K / ⇧⌘H / ⇧⌘L shortcuts on Mac
Option key on Mac transforms e.key into special characters
(e.g. ⌥K → ˚), so modifier+letter shortcuts must check
e.code (physical key) instead of e.key (character produced).
2026-02-28 20:01:50 +03:00
Danila Poyarkov f77ad708b8 Live component-instance sync with override support
Editing a main component now propagates to all instances:
- Instance children are mapped to component children via componentId
- syncInstances() copies properties from component to instances,
  skipping any property marked in the overrides record
- New children added to a component appear in existing instances
- Store calls syncIfInsideComponent after updateNode, commitMove,
  commitResize — changes propagate automatically
- Demo creates real instances (Button, Badge) in the App Preview

Tests added: instance creation with child mapping, sync propagation,
override preservation, new child addition, detach.
2026-02-28 19:57:52 +03:00
Danila Poyarkov b3954a7d01 Coalesce renders with requestAnimationFrame
Multiple state changes between display refreshes (e.g. rapid
trackpad wheel events during zoom) now produce a single render
per frame instead of one per event. Zoom feels identical but
CPU usage drops since redundant intermediate renders are skipped.
2026-02-28 19:21:14 +03:00
Danila Poyarkov 86a5edfab5 Reuse Paint objects instead of allocating per frame
Pre-allocate auxFill, auxStroke, and opacityPaint on the renderer.
Reconfigure them before each use instead of new/delete cycles.
Reduces per-frame WASM Paint allocations from 29 to 10 (the
remaining 10 are cold-path: pen tool, rulers, one-off overlays).

Hot paths covered: renderNode opacity layers, hover/drop highlights,
selection handles (called 8-16× per frame), section title pills,
component label icons+text, size pill, marquee, layout indicator,
rotation handle, component set dashed border, drop shadows.
2026-02-28 19:19:30 +03:00
Danila Poyarkov bb5b4b57e5 Viewport culling: skip rendering off-screen nodes
Compute the world-space viewport from pan/zoom and skip nodes
whose bounds don't intersect it. Only culls leaf nodes and
clipped containers — unclipped parents are still traversed since
their children may extend beyond the parent bounds.

Handles rotation by expanding the bounding box to the diagonal.
Section titles and component labels also culled during collection.
Export path (renderSceneToCanvas) uses infinite viewport to render all.
2026-02-28 19:14:00 +03:00
Danila Poyarkov e39f81078f Fix text disappearing on window resize
On resize the renderer is recreated with a fresh TypefaceFontProvider.
loadFont() had an early return for cached fonts that skipped registering
with the new provider, so ParagraphBuilder had no fonts to render with.
2026-02-28 19:08:16 +03:00
Danila Poyarkov c4c4339f77 Run auto layout after demo creation
Children of auto-layout frames were positioned at their createShape
coordinates instead of being laid out. Call computeAllLayouts() after
building the demo tree so everything is properly arranged on load.
2026-02-28 18:59:05 +03:00
Danila Poyarkov 5a2265db3b Richer demo: components, auto layout, gradients, text, dashboard
Showcases the full feature set:
- Components section with Button/Primary, Button/Secondary, Tag,
  Avatar, Card (auto layout + progress bar), Input, Badge
- Color palette swatches
- App Preview section with a dashboard layout: sidebar nav,
  stat cards with auto layout, bar chart with gradient fills,
  table header with space-between layout
- Gradient cards, typography scale, mixed shapes
2026-02-28 18:32:27 +03:00
Danila Poyarkov 33fa850573 Fix component interaction: click selects component, drag by label, no white fill
1. Hit test: components/instances are opaque containers — clicking
   their children selects the component itself. Double-click enters
   and selects the child inside (like Figma).
2. Component label hit testing: clicking the purple name label
   selects the component, enabling drag-by-title (same as sections).
3. Components created from wrapping have no fill (transparent) —
   was incorrectly adding a white background rect.
2026-02-28 18:28:55 +03:00
Danila Poyarkov 058373123f Always-visible purple labels for components, component sets, instances
Like Figma: component/instance names render as permanent purple text
with a diamond icon prefix, at fixed screen-space size (doesn't scale
with zoom). Drawn in the screen-coordinate pass alongside section titles.

- COMPONENT: single diamond ◆ + name, above top-left
- COMPONENT_SET: 4-diamond grid ◆◆◆◆ + name, above top-left
- INSTANCE: single diamond ◆ + name, above top-left
- Variants inside a set: label inside top-left instead of above
- Remove component names from selection labels (no longer needed)
2026-02-28 18:23:46 +03:00