Commit Graph
16 Commits
Author SHA1 Message Date
BFlatandClaude Opus 5.5 70ab71bee6 M2: auto layout keyboard reordering and section hide contents (#78)
* M2: auto layout keyboard reordering, nested auto layout fixes, section hide contents and dev status

- Arrow keys move auto layout children one place: along the flow (past hidden and
  absolute siblings), between wrapped rows, to the neighbouring grid cell, and into
  the neighbouring nested auto layout frame; no undo step when nothing moves.
- Sections: section.toggleContents (hide contents), object.wrapInSection, frames in
  sections are top-level frames (labels, flows, navigation, PDF export), navigating
  to a section shows its frame shown last.
- Ready for dev turns "Changed" when contents are edited; dev.resolveChange with a reason.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M2: README and ROADMAP for auto layout arrow keys and sections

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M7: roadmap refresh after #68–#77

Parity table rows for canvas, selection and transform, layers, effects,
text, prototyping and comments now reflect the merged work; weighted
feature depth ~76%; README command count and hand-off features.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M2: the dev change reason resolves only on Enter or Resolve

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 14:36:02 +07:00
BFlatandClaude Opus 5.5 60e23584c3 M8: fix a flaky render test (shared image ids) (#75)
`band_tests` and `lod_tests` both stored a different PNG under the image id "img". Image ids are
content hashes in real documents and key the process-wide decode cache, so when the tests ran in
parallel one could draw the other's picture (`inner_shadow_leaves_the_interior_alone` failed about
one run in twenty, a pixel off by one). Each fixture now has its own id; 40 consecutive runs pass.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 13:03:58 +07:00
BFlatandClaude Opus 5.5 f9b4609631 M3: effects fidelity: backdrop blur in clipped frames, exact shadow spread, glass, noise and texture (#69)
- Background blur sees the whole backdrop under the layer: the renderer keeps the stack of
  isolated surfaces it draws into (clipping frames, rounded clips, faded groups, masks) and
  flattens it for the layer's area, reading past the layer's own box. A blur inside a clipping
  frame now shows the frame's fill instead of the sharp layers below, on the canvas and in PNG
  exports. The PDF export draws background blur and glass through the same code from a render of
  what lies under the node (`backdrop::render_beneath`, which the PDF's backdrop now shares), and
  its images match the PNG's pixels.
- Shadow spread offsets the outline: the shape filled and stroked twice the spread wide with
  mitred joins (grow) or cut away (shrink), the stroked bands cached per outline. Ellipses stay
  ellipses, stars keep sharp points, text keeps its letterforms, rounded corners grow their
  radius. Inner shadows shrink the outline the same way. Canvas, PNG and PDF share the shadow
  code; SVG writes spread shadows as the same geometry and now exports inner shadows.
- Glass effect (light angle and intensity, refraction, depth, dispersion, frost, splay): the
  frosted backdrop bent towards the inside within `depth` of the edge, colour fringes, light on
  the rim, from an exact distance transform with smoothed normals.
- Noise: mono, duo and multi colour modes, grain size, density, opacity and blend mode; grains
  live in the layer's coordinates (stable when panning and zooming). Texture is now drawn
  (pixels scattered by the radius, grain size, clip to shape).
- Design panel rows for noise mode/colours/opacity, texture clip to shape and every glass value;
  `effect.add` takes `glass`; REST import of NOISE, TEXTURE and GLASS; Inspect rows and CSS;
  the scale tool scales grain sizes, texture radius, glass depth and frost.
- Tests in new modules: render backdrop/spread/grain tests, PDF backdrop tests, SVG, doc,
  engine and REST effect tests.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 11:59:59 +07:00
BFlatandClaude Opus 5.5 5f54fecca2 M8: 50k-layer zoomed-out renders and faster save/load (#63)
Canvas previews zoomed out to 50% or less draw levels of detail
(crates/render/src/lod.rs): subtrees under 2 device px are splatted as
their averaged paint colours (box filter, no paths or effects), text
under 4 px is greeked into bars, small solid rectangles are splatted and
rectangular clip layers are composited without the blend pipeline.
Image paints and masks keep the exact path. Exports, thumbnails and
every other render stay exact (Options::lod is off by default); render
bands are assembled with a row copy instead of a pixmap draw.

.figcraft documents are serialized straight into the compressor and
parsed straight from the text (a header pass reads version and image
index without building a value tree); layer and text fields at their
default values are left out (63 -> 22 MB of JSON at 50k layers, older
files still load); JSON members use deflate level 3.

At 50k layers: zoomed-to-fit render ~58 -> ~8 ms, save ~1.0 s -> ~0.09 s,
load ~0.86 s -> ~0.16 s.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 10:27:51 +07:00
BFlatandClaude Opus 5.5 1184dd4177 M4: variable width strokes (#58)
A stroke can carry a width profile: widths at positions along the path
(each subpath by length), interpolated smoothly (monotone cubic).

- doc: WidthProfile / WidthPoint / WidthPreset (uniform, wedge, taper both
  ends, eye, rounded, pinch) on StrokeProps, serde-defaulted so older files
  load; paint reach and hit testing follow the widest point.
- pathops: path measuring by length and the variable outline (offset along
  the normals, round/miter/bevel joins, caps, dashes keep their width,
  Inside/Outside clip as before; bounded samples and dashes, uniform
  fallback). Canvas, PNG, SVG (filled outline), PDF and outline stroke use it.
- engine: stroke.widthProfile {preset | points}, node.set widthProfile,
  inspect output; the Variable width tool (Shift+W) in vector edit mode:
  drag the path to add a width point, slide points, drag their handles,
  Delete removes the selected one; vector.edit.widthPoint add/set/remove.
  Every gesture is one undo step.
- UI: width profile dropdown with previews drawn in code and a flip button
  in the stroke settings; the tool in the edit toolbar (new icon drawn in
  code); width handles and a hover preview on the canvas; Position/Width
  fields for the selected width point.
- Tests: profile maths, outlines, render regressions, tool gestures,
  commands, exports and junk sweeps. ROADMAP, README, docs updated.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 10:12:21 +07:00
BFlatandClaude Opus 5.5 d3dbc6f495 M3: paint: colour profiles (Display P3), image adjustment presets, style picker (#54)
* M3: colour profiles: sRGB or Display P3 per file, convert or keep values, tagged exports

- color: ColorProfile (srgb, displayP3) with conversions derived from the primaries (shared D65
  white and sRGB curve; out-of-gamut channels clip), table-driven pixel conversion, a generated
  ICC v4 matrix/TRC profile and PNG cICP code points.
- doc: Document.color_profile (serde default sRGB, written only when set); every colour value
  (paints, text runs, effects, grids, page and prototype backgrounds, overlay backdrops, styles,
  colour variables, document colours) can be mapped; set_color_profile converts or reinterprets.
- engine: file.colorProfile {profile?, convert?} (File > Color profile...; without a profile it
  opens the dialog); document.inspect reports colorProfile.
- render: images are taken as sRGB and managed into the file's space; encode_export tags PNG
  (sRGB chunk, or iCCP + cICP for Display P3) and embeds the ICC profile in Display P3 JPG/WebP;
  SVG and PDF export Display P3 files converted to sRGB. The CLI exports the same way.
- ui: Color profile dialog (keep appearance / keep values) and a Color profile row on the page
  panel; the canvas, presenter, thumbnails, swatches and colour picker show a Display P3 file's
  colours converted to the screen's sRGB, and Copy as PNG converts too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M3: image adjustment presets: built-in, saved in the file, reset

- color: ImageFilters::PRESETS (Vivid, Muted, Warm, Cool, Black & white, Fade, Reset), lookup by
  id or label, and sanitized values.
- doc: Document.image_presets (name + adjustments; written only when there are some).
- engine: image.adjust {ids?, target?, index?, preset?, filters?} sets the adjustments of image
  paints from a preset (built-in or saved) with values on top, one undo step; image.savePreset,
  image.deletePreset and image.presets.
- ui: the colour picker's image section gets a presets menu named after the preset in effect
  ("Custom" otherwise), saved presets with delete, "Save as preset...", a Reset button, and
  aligned adjustment sliders.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M3: style picker: search, folders, list or swatch grid, edit, create, detach

The Fill, Stroke, Effects and Layout grid sections' Styles button opens a popover laid out like
the variable picker (crates/ui-egui/src/style_picker.rs):

- search over style names; styles grouped by the folders of their `/`-separated names, which
  collapse (searching opens them); colour styles as a list or a grid of swatches;
- click applies, a check marks the applied style; the row's edit button (or a right-click) edits
  the name, description and values: colour style paints open the colour picker on the style,
  effect and grid styles update from the selection; delete;
- `+` creates a style from the selection's current values and opens it for naming; detach;
- colour slots still list colour variables to bind;
- the applied style chip opens the picker; new icons Adjust, ViewList, ViewGrid (drawn in code);
- control channel: ui.set {stylePicker, stylePickerEdit, stylePickerView}.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 07:38:17 +07:00
BFlatandClaude Opus 5.5 a5d5c265fd M7: performance: retained canvas renders, virtual layers panel, shared text layouts, cargo xtask bench (#35)
Large documents stay interactive: the canvas re-renders only what a pan uncovers or an edit
touches, the layers panel lays out only the rows in view, and per-frame work no longer walks the
whole page.

- Benchmark: `cargo xtask bench [SIZES]` builds synthetic pages (`figcraft_engine::synthetic`:
  screens with an auto layout header, a card group, button instances and shapes/text) at
  1k/10k/50k layers and prints engine/render timings (`figcraft-cli perf --layers 1000,10000`) and
  headless whole-UI frame timings (an ignored ui-egui test).
- Retained canvas rendering (`figcraft_render::Retained`, used by the render worker): views snap
  to whole device pixels, so a pan shifts the previous image in place and renders only the
  uncovered strips; an edit renders only where changed layers were and are
  (`Document::changed_ids` diffs the two versions skipping shared structure; ancestors that paint
  from their children's pixels - booleans, effects, masks - count whole; background blur reach
  and noise are allowed for). Zoom, page, options, font or page-level changes, or large changes,
  render the whole view as before. `ui.inspect` reports `canvas.renderWork`.
- The texture image is converted on the render thread, not the UI thread.
- Layers panel: only visible rows are laid out (stable ids while scrolling); rows are cached per
  document version, page, expanded set and filter.
- Frame labels and layout grids of off-screen top-level frames are skipped; the Prototype tab's
  connection sources are cached per document version.
- Text: the layout cache keys by a cheap fingerprint confirmed by `TextData` equality (no Debug
  formatting per lookup, 32k entries) and hands out shared layouts; a layout generation marks
  renders stale when fonts arrive. Overflowing text is culled by its glyphs, so it no longer
  vanishes while its box is off screen.

Before -> after (release, Windows, 1440x900 view, ms; 10k / 50k layers):
- move one layer, re-render @100%:     7.65 / 6.49  -> 0.18 / 0.81
- move one layer, re-render zoomed out: 11.95 / 24.10 -> 0.16 / 0.77
- pan 40 px @100%:                     7.53 / 6.82  -> 0.69 / 0.81
- pan 40 px zoomed out:                12.07 / 24.30 -> 1.62 / 4.17
- full render @100% / zoomed to fit:   7.78 / 44.22 -> 6.56 / 38.83 (10k @100%, 50k fit)
- UI frame, every screen expanded in layers: 6.16 / 39.07 -> 0.24 / 0.57
- UI frame, scrolling that panel:      6.11 / 40.18 -> 0.43 / 0.40
- UI frame, layers filtered by name:   1.03 / 6.92  -> 0.17 / 0.36
- UI frame, Prototype tab:             0.89 / 6.45  -> 0.18 / 0.80
- UI frame, idle:                      0.31 / 1.43  -> 0.11 / 0.39
Unchanged and within budget: single-layer edits through the engine ~0.01 ms, normalize after a
one-node edit < 0.01 ms, hit test 26 / 93 us. Left: save/load at 50k (~0.7 s each), zoomed-out
renders at 50k (~40 ms).

Tests: retained renders match full renders after pans, moves, recolours, hide/remove/add, undo,
masks, overflowing text, edits under a background blur and an inner-shadow/noise layer, a drop-
shadowed group, and a blur panned in from the view's edge; zoom/page/options/page background
render full; junk views don't panic. Layer-row and prototype-source caches follow edits, undo,
page switches, expansion and the filter. `changed_ids`, the text cache, and the perf CLI.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 05:19:05 +07:00
BFlatandClaude Opus 5.5 15ccc379ab M1: fix inner shadow bands: shadow from the shape, not the layer's crop; banded rendering matches one piece (#24)
The canvas renders large views in parallel horizontal bands, and each band crops a layer to its
own rows. Three effects went wrong at those crops:

- Inner shadow (the reported bug): the padded shadow buffer treated everything beyond the cropped
  layer as outside the shape, so every band edge cast shadow inwards, drawing dark bands across
  the layer (any shape and fill; also at the view's edge when a layer was partly off screen). The
  shape itself is now rasterised over the padded buffer at the shadow's offset (sub-pixel
  accurate), so only its real edges cast shadow.
- Layers starting above or left of the pixmap (inside the blur margin) were sized from the
  clamped origin and came out short by the margin: a drop-shadowed or background-blurred layer
  crossing a band edge lost a strip at its far side.
- Background blur read a backdrop that stopped at the band's edge (streaks at band edges). Bands
  now overlap by what background blurs read (3 sigma + box rounding) and keep only their own
  rows; a blur wider than a band renders the view in one piece.

Regression tests (crates/render/src/band_tests.rs): an image-filled ellipse and a rotated solid
rectangle with inner shadows (spread, offsets) under a background-blurred panel, at 1x and 2x,
rendered in 2, 3 and 7 bands, must match the one-piece render row by row; the ellipse's interior
far from its edge must equal the render without the shadow on every row; a huge backdrop blur
falls back to one piece.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 04:09:04 +07:00
BFlatandClaude Opus 5.5 840d3ac5d2 M1: mask types: alpha, vector outline and luminance masks (#17)
- Layers gain `maskType` (alpha by default, so files are unchanged): a vector mask clips by the
  mask layer's outline whatever its fills and opacity; a luminance mask shows content where the
  mask is bright and hides it where it is dark.
- `node.set {maskType}` (validated), `node.get` reports it, and the Appearance section shows a
  Mask dropdown when the selection is masks.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 03:45:45 +07:00
Brandon ThomasandClaude Opus 5.5 7aae256b4c Image crop mode: on-canvas crop handles (move, proportional corner scale); Crop defaults to the Fill placement
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 13:57:32 -04:00
Brandon ThomasandClaude Opus 5.5 ab5914485e Perf: persistent node map (imbl) and dirty tracking; commands now scale with what changed (node.create 0.2 ms at 20k nodes)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 13:49:04 -04:00
Brandon ThomasandClaude Opus 5.5 b26b449fe3 Fixes from agent reports: hex colours everywhere, camelCase in/out, unique variable ids, strict batch, ui.key modifiers, export rounding, Fit images, inner shadows on all edges
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 13:34:25 -04:00
Brandon ThomasandClaude Opus 5.5 b36fd771ab M0.9: MCP server + figcraft-cli, PDF export, web app, release/CI packaging, vector edit mode, workspace lint cleanup
- crates/mcp: JSON-RPC MCP server (18 tools, resources, prompts), headless and attached backends, end-to-end agent tests
- apps/figcraft-cli: info/export/convert/render/run/commands/mcp/perf
- crates/pdf: vector PDF export via krilla with embedded text; file.exportFramesPdf
- apps/figcraft-web + packaging/{macos,windows,linux,freebsd,web} + release/ci workflows
- engine vectoredit: edit points/handles/bend/add/delete-heal; canvas overlay and edit strip

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 13:31:53 -04:00
Brandon ThomasandClaude Opus 5.5 aec69a53ac Perf: parallel band rendering, text layout cache
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 13:23:06 -04:00
Brandon ThomasandClaude Opus 5.5 56a312ba5f M0.5: layout (auto layout + constraints), format, svg, engine (commands, history, tools, components, styles, variables)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 12:40:18 -04:00
Brandon ThomasandClaude Opus 5.5 b367392c4b M0.4: text (shaping, layout), pathops (booleans, aligned strokes), render (tiny-skia scene renderer)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 12:12:34 -04:00