* 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>
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>
- Versions now belong to the document (DocState.versions) instead of a process-global map keyed by
document uid, so they survive closing the file. A new, renamed or deleted version marks the file
as changed.
- .figcraft: `versions/index.json` (id, name, description, time, image hashes) plus one deflated
`versions/<id>.json` snapshot per version; images only versions use are stored once and kept with
them. Snapshots read from a file are parsed on first use (and written back as-is). Readers that
don't know versions ignore the entries; files without versions load unchanged. Damaged, missing,
oversized or duplicate entries are skipped and reported, never failing the load. At most 100
versions per file; names and descriptions are clipped.
- Commands: `file.saveVersion {name?, description?}` returns the version; `version.list`;
`version.restore {id | index}` (one undo step, keeps the file name, stays on the current page);
`version.rename {id | index, name, description?}`; `version.delete`; `version.duplicate` (opens
the snapshot in a new tab). Indexes are validated: a negative index used to restore version 0.
`req_index` param helper shared with the guide commands.
- Version history panel in the left sidebar in place of Pages and Layers (File > Show version
history; Save to version history opens it with the form): save form (title, description),
"Current version" on top and the versions newest first on a timeline with name, relative time and
description; per-version menu (also right-click): Restore, Rename (title and description inline),
Duplicate, Delete (confirmed inline). New shared `widgets::button`.
- `figcraft-cli run --out x.figcraft` keeps the version history.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- New `figcraft_format::rest`: imports the public REST API file JSON (`GET /v1/files/:key`, and
the nodes endpoint's response) using the published reference only. Pages (name, background,
flow starting points), the layer tree, names, visibility, lock, opacity, blend mode, masks.
- Geometry from `relativeTransform` (children of groups/booleans checked against
`absoluteBoundingBox` to settle their space), else absolute bounds + rotation; sizes, corner
radii (uniform and per corner), smoothing, clip content, constraints, export settings, layout
grids.
- Paints: solid, linear/radial/angular/diamond gradients (handles map 1:1), image fills (fill,
fit, tile, stretch, rotation, filters) with bytes from a resolver or a grey placeholder keyed by
the imageRef; paint opacity, visibility and blend. Strokes: weight, per-side weights, align,
caps, join, dashes, miter angle. Effects: drop/inner shadow (offset, radius, spread, show behind),
layer and background blur.
- Vectors from `fillGeometry` (stroke-only vectors use the `strokeGeometry` outline), ellipse arc
data, stars/polygons, boolean operations. Text: characters, type style (family, style or weight
+ italic, size, line height px/%/auto, letter spacing, case, decoration, alignment, auto resize,
truncation, paragraph spacing/indent, OpenType flags, links), per-character overrides → styled
runs (UTF-16 aware), list type.
- Auto layout: direction incl. grid, alignment, padding, gaps, wrap, hug/fixed/fill sizing (legacy
and per-axis fields), absolute positioning, min/max, grid cells.
- Components and sets with descriptions and property definitions (names lose their `#id`
suffix); instances of components in the file become instances with their property values and
listed overrides; outside components and component cycles stay frames. Shared styles are created
from the layers that use them, named from the metadata.
- Unknown types and fields are skipped with a counted warning list; sizes, depth (256), layer count
(200,000), path length and list lengths are capped; nothing panics on junk.
- Engine: `file.open` / `file.import` / CLI detect the JSON by content, link imported instances to
their mains (sources and overrides), read image fills from an `images` folder next to the file,
never save over the JSON, and return `warnings`. `file.import` now remaps ids across all
imported pages at once (instances keep their main and layer sources) and brings styles and page
backgrounds along. `figcraft-cli info` prints load notes.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>