mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-24 10:27:00 -06:00
main
228
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e917d3c2e6 |
M1: frames, groups, sections and masks behaviour suite and fixes
**A frames, groups, sections and masks behaviour suite written from the public documentation, and fixes for every divergence it found.** Each documented rule (frames vs. groups when resized, constraints, Cmd/Ctrl-resize, clip content, Frame selection, grouping and ungrouping, group bounds and opacity, X/Y in groups, click targets, drawing a frame or section around layers, dropping into and out of frames, where sections may go, resizing sections, top-level frames as screens and exports, Use as mask, what a mask covers, alpha / vector / luminance masks, groups as masks, clicks on masked-out areas, a mask group's box and exports) is a table-driven case with the result the rule implies and a one-line derivation; where the documentation is silent the case says so and pins today's behaviour. **Fixes (the suite failed on 7 tests before them)** - **Masked-out areas don't take clicks**: hit testing and Select layer skip what a visible mask hides (the nearest mask below a layer; for a group or frame used as a mask, what its layers draw), so a click there reaches what's behind. Checked only once something is hit, and skipped in files without a mask (`Document::has_masks`, worked out once after each change), so hit testing costs what it did. - **A mask group's box and exports are the mask's**: a group's bounds and export area count a mask for the layers it masks (layers under the mask still count), so the selection box, W/H, snapping and PNG/SVG/PDF exports crop to the mask instead of the whole masked layer (`Document::box_children`). - **Sections stay on the page or in sections**: moving a section into a frame or group (`node.reparent`, refused at the root in `Document::reparent`), dragging one over a frame (it lands in the section around the frame, or on the page), grouping or framing a section, creating a section in a frame and pasting one into a selected frame (it goes on the page) no longer put sections where they can't be. - **Drawing a frame or section around layers puts them in it**: the layers fully inside the drawn box (its siblings; visible and unlocked; a frame never takes a section; not in auto layout) move into it, in their order, without moving on the page; one undo step. - **Resizing a section leaves its contents** where they are on the page (no constraints in sections), from a field, a handle (also the left/top edges) or `transform.resize` with any anchor. - **Cmd/Ctrl while resizing a frame leaves its layers as they are**: #133 (merged in) keeps their places; this branch also keeps their sizes (the frame is resized without running constraints, so a left & right or scale child no longer stretches and is only moved back). - **X/Y are measured in the frame, not the group**: groups and boolean groups have no coordinate space of their own, so a layer in a group shows (Design panel, `node.get`, `inspect_document`) and takes (`node.set`, `node.create` with a group parent) X/Y in the frame the group is in, or on the page (`Document::frame_relative`). Rotated groups too. Already there and now covered by the suite: constraints when a frame resizes, groups scaling their layers, clip content (drawing, hits, export size), Frame selection's size and place, grouping/ungrouping keeping world transforms (rotated groups too), nested groups, empty groups going away, group opacity on the composite, click / Cmd-click / double-click targets (frames in sections count as top-level frames), dropping by the pointer and pulling out, group layers staying in their group when dragged, top-level frames as prototype screens and export boards, Use as mask (bottom layer, "Mask group", at the topmost layer's place, single-layer toggle), masks covering every layer above them, hidden masks, alpha / vector / luminance masks, a group as a mask, masks in SVG and PDF exports. No new commands or UI strings (no i18n changes); each edit stays one undo step. |
||
|
|
f3012ce6e4 |
M3: styles and variables behaviour suite and fixes
**A styles and variables behaviour suite written from the public documentation, and fixes for every divergence it found.** Each documented rule (what a style holds, creating from a selection, applying, editing, detaching, deleting a used style, organising by folders and order, descriptions; variable types, modes and the default mode, aliases, scopes, mode inheritance, bindings by field and type, deleting a bound variable, code syntax, number variables in pixels) is a table-driven case with a one-line derivation; where the documentation is silent the case says so and pins the behaviour.
**Fixes (10 of the suite's 18 tests failed on the old code, counting the missing settings below)**
- **Editing a styled property unlinks that style**: a fill, stroke, effect or layout grid list edited away from its style (through `node.set`, a paint row, an added fill, or a variable bound to it) drops the link, as text chars already did. Before, the layer kept a stale link and the next style edit silently overwrote the user's edit. Moving or renaming a layer keeps its links.
- **Applying a style replaces variables bound to what it sets** (the fill list for a paint style, the typography for a text style); other bindings stay. Before, the variable won again on the next edit.
- **Text styles go on text layers only**: applying one to a mixed selection leaves the other layers alone (they used to get a stray text style link); a selection without text is refused.
- **Deleting a bound variable unbinds the layers using it**: they keep the value they show; variables aliasing it take its values per mode (docs silent on aliases). Before, the binding dangled. `variable.deleteCollection` now does the same for aliases from other collections into it.
- **Bindings are type-checked**: a variable of a type the field doesn't take (a colour on `width`, a number on `visible`) is refused; scopes still only filter pickers. Number variables can fill text content (shown as text).
- **Aliases are checked when a variable is created** (`variable.create {value: {alias}}` took any name, of any type).
- **Style names** are trimmed per `/` folder part; empty names are refused.
**Missing documented settings, added**
- **Organising styles**: `style.move {styles, before?, folder?}` reorders and moves into a folder (renaming the style to match), `style.duplicate` puts "<name> copy" right below the original, `style.renameFolder` renames a folder's styles. The page's **Local styles** list (nothing selected) is now grouped by type and folder with swatches and text sizes: click opens the style editor, right-click offers Edit style / Duplicate / Delete, a row dragged by its grip moves (into the folder it lands in), right-click on a folder renames it.
- **Modes**: Duplicate mode (`variable.addMode {from}`), Set as default and Move column left/right (`variable.moveMode {collection, mode, to}`); layers on Auto follow a new default, layers set to a mode keep theirs.
- **Code syntax** per platform (Web, iOS, Android): `variable.update {codeSyntax}`, three fields in Edit variable, carried by the variables JSON (import and export); generated CSS and the CSS variables export use the Web name (`var(--x)` or `--x`).
- **Number variable scopes**: font weight, paragraph spacing, paragraph indent and text content (and `fontWeight`, `paragraphSpacing`, `paragraphIndent` bind with their scopes).
New strings go through `tr(...)` with `en.toml` / `es.toml` entries, new commands have Spanish labels; the new rows, fields and menus have accessible names. Each edit stays one undo step.
|
||
|
|
b65cc2e4fe |
M5: prototyping behaviour suite and fixes
**A prototyping behaviour suite written from the public documentation, and fixes for every divergence it found.** Each documented rule on triggers, actions, overlays, animations, easing, scrolling and interactive components is a case with what the rule implies and a one-line derivation; where the documentation is silent the case says so and pins today's behaviour. The model rules are table-driven tests in the engine; the player's are driven through egui headless (real pointer, key and timer input into the presenter) or on the player directly.
**Fixes (the suite failed on these before them)**
- **On drag follows its animation**: with a move, push or slide animation, only a drag that way starts it (a drag another way can scroll instead), and the animation follows the pointer while the button stays down, then plays on from there when it lets go (it played by time, and any drag direction fired it).
- **Key combinations**: a key trigger can be a combination (`Shift+K`, `Ctrl+Alt+Delete`, other spellings of the modifiers accepted); the modifiers held must match, so `K` no longer fires for Shift+K. The interaction editor's recorder records the modifiers held, and REST files' modifier key codes import as the combination (they imported as the key alone, with a warning).
- **Overlay timers**: an overlay's after-delay timer starts when the overlay opens, and again each time it opens (it was timed from when the screen under it arrived, so a toast opened late closed at once, and it fired only the first time).
- **Interactive components**: every instance plays the interactions on the variant it shows; an interaction on the instance with the same trigger wins. Before, instances played only interactions copied onto them when placed: an instance placed before its main got interactions did nothing, and one placed after kept the first variant's interactions after changing (a toggle went on but never off). New instances no longer copy them, and copies in older files give way to the variant shown (`proto_run::play_interactions`, on the player's copy).
- **Change to on nested instances** changes the instance whose variant set holds the destination (`proto_run::change_to_target`), not the innermost instance (an icon inside a button).
- **Springs**: the presets are physical springs (mass 1; stiffness / damping gentle 100/15, quick 300/20, bouncy 600/15, slow 80/20; they were ad-hoc curves), and every spring plays until it settles instead of being squeezed into the duration field's time; the settling time accounts for over-damped springs. The interaction editor shows the spring's duration in place of the duration field, with a tooltip.
- **Smart animate overshoot**: matched layers follow spring and back easings past their end and back (they snapped to the end once the curve passed 1).
- **Fixed layers on top**: fixed layers draw and take clicks above the content scrolling under them, wherever they are in the layer list (the file's order doesn't change).
**Missing documented setting, added**
- **Scroll to offset**: `{type: "scrollTo", dest, offset: [x, y]}` stops the layer that far from the top-left of the innermost frame scrolling it (room for a fixed header); X / Y fields in the interaction editor; files carry it only when set.
- Already there and now covered by the suite: click / mouse down / mouse up / while pressing / while hovering / mouse enter / mouse leave / after delay, nested hotspot priority and click-through, navigate (closing overlays), back with the reversed animation, restart, overlay stacking / swap / close / close on a click outside, change to with smart animate, reversed directional transitions, easing curves.
New strings go through `tr(...)` with `en.toml` / `es.toml` entries. No new command ids. The player still draws nothing while idle: the drag-follow only runs during an animation, and fixed layers and instance interactions are worked out once per layout, not per frame.
|
||
|
|
62db8d4e36 |
M3: components behaviour suite and fixes
**A components and variants behaviour suite written from the public documentation, and fixes for every divergence it found.** Each documented rule of instances (what an instance overrides and what it can't, how overrides persist when the main changes, reset), variants (which overrides survive a switch, the default variant, property order, duplicate combinations), swaps (instance menu vs Assets panel), exposed nested instances, detach / reset all / push changes / go to main, resizing and auto layout instances is a table row with the values the rule implies and a one-line derivation; where the documentation is silent the row says so and pins today's behaviour. Nested instance overrides stay covered by `nested_override_tests`, slots by `components_tests`, accepting library updates with overrides kept by `library_tests` / `components_scale_tests` (#91). **Fixes (9 of the 10 new tests fail on the old code)** - **Overrides that were silently lost**: auto layout **padding**, **gap** and **alignment** (three override groups, so a padding override still follows the main's gap), **layout grids**, **export settings** and **prototype interactions** on layers inside instances, and every **text property** (resizing, paragraph spacing and indent, lists, max lines, OpenType features) were reset by the next edit of the main. They are override groups now: kept when the main changes that property, carried by variant switches and swaps, pushed to the main, reset per group, and listed in the Overrides section. REST imports map the matching `overriddenFields`. An instance's own export settings and interactions stay its own, like its place (as before). - **What an instance can't override is refused** (rolled back with a message, one place for every path: commands, MCP, gestures): moving, rotating, flipping, nudging or aligning a layer inside an instance (auto layout items included: their place is the layout's), resizing it (text boxes keep their main's bounds; text whose box follows its content still grows), changing its constraints or resizing (hug / fill / min / max / absolute), and an instance's auto layout direction or other auto layout settings (only padding, gap and alignment are its own). Before, these became `position` / `size` overrides or were lost on the next main edit. Resizing the instance itself still works and its layers follow the main's constraints. Existing `size` / `position` overrides in files are still read and kept. - **Variant switches** keep an override only where the layer's name path matches *and* both variants had the same value for that property; text layers keep all their changes (by name and hierarchy). Before, every same-named override survived, even where the new variant differs (a hover background overridden back to the default one). The instance's own overrides follow the same rule. Undo brings the dropped overrides back. - **Alt-drop swaps from the Assets panel** (`instance.swapAt`, `library.swapAt`) keep only text layers' overrides; the instance menu's swap (`instance.swap`) still keeps every override on same-named layers. - **Default variant = the top-left one** (was the first in layer order): new instances of a set, the set's Assets thumbnail, generated code, `component.properties` defaults and the panel's default dropdown. Picking another default moves that variant to the top-left corner (one undo step). - **Hidden exposed instances hide their properties** (`instance.exposed`). - **Push changes from a nested layer** goes to the outer instance's main, never into the nested instance's own main; it reuses the override copy code, so every group above is pushed too. **Added** - **Variant property order**: `component.editProperty {name, index}` moves a variant property; every variant's name follows. - **Duplicate combinations**: `component.properties` reports `conflicts` (variants sharing one combination of values), and the component panel warns until each has a unique one. - **Design panel**: Position, Rotation, Constraints, Dimensions / resizing and grid cell fields are greyed out (with a tooltip) for layers inside an instance; the auto layout direction, wrap, strokes / canvas stacking and add / remove auto layout are greyed out or hidden for instances. New strings go through `tr(...)` with `en.toml` / `es.toml` entries; disabled controls are announced as disabled. |
||
|
|
5923e7dfcf |
M3: embedded image colour profiles (#136)
## What **Images are colour managed from their own profile into the file's colour space.** Until now every placed image was taken as sRGB. Now an image is converted once, when it is decoded (cached by image hash, filters and file profile, as before), from the space it says it is in into the file's working space (sRGB or Display P3), so a wide-gamut or tagged photo looks right on the canvas and in exports. - **Where the profile comes from**: the embedded ICC profile of PNG (`iCCP`), JPG (APP2), WebP (`ICCP`) and TIFF (ICC tag); a PNG without one uses its `sRGB` chunk, or its `gAMA` (+ `cHRM`) chunks: `gAMA` 1/2.2 with sRGB (or no) primaries means sRGB, which is what PNG writers put next to an `sRGB` chunk; any other gamma/primaries become a matrix/curve profile. - **Conversion**: moxcms, a pure-Rust colour management crate the `image` crate already builds (now a direct dependency of `figcraft-render`, same version and features: nothing new compiles). It runs matrix/TRC profiles (v2 and v4: `curv` gamma and tables, `para` curves, chromatic adaptation), LUT-based RGB profiles and grey (`kTRC`) profiles, so sRGB, Display P3, Adobe RGB, ProPhoto and camera/phone profiles all work. - **Fallback**: no profile, a profile that isn't RGB or grey (CMYK, Lab), a damaged one, one over 4 MB or one moxcms can't build a transform for: the image is taken as sRGB exactly as before (same pixels, same filter order). - **Exports**: the original file stays in the document. PNG/JPG/WebP exports carry the converted pixels tagged with the file's profile (unchanged mechanism); PDF exports still embed the original file, with its ICC profile; SVG exports embed the original file. - **UI**: the image fill's details in the paint picker show *Color profile: Adobe RGB (1998)* (the profile's own name), or *Unknown — treated as sRGB*; strings through `tr()` with `en.toml` / `es.toml` entries. No new command ids. - **Agents**: `image.place` / `file.placeImage` report `imageProfile` (the name, or `null` when taken as sRGB); `docs/mcp.md` updated. Untrusted input: profiles over 4 MB are ignored before parsing, moxcms's own caps on curve and LUT sizes apply, colorants must be finite and a transform must build before a profile is used; anything else falls back to sRGB without panicking. The parser is swept with 600 mutated profiles (byte flips, truncation, absurd tag counts and offsets) through the whole decode path. ## Verified - **Desktop app** over the control channel (own target dir, separate `APPDATA`, stopped by PID): placed a synthetic 200×200 PNG with an embedded Adobe RGB (1998) profile built by a script (v2, `curv` gamma 563/256, published D50 colorants) next to the same pixels without a profile; the screenshot shows the tagged one more saturated (as it should be: its values mean wider-gamut colours), the untagged one unchanged. The paint picker reads *Color profile: Adobe RGB (1998)* for the first and *Color profile: Unknown — treated as sRGB* for the second; `image.place` returned `"imageProfile": "Adobe RGB (1998)"` and `null`. - **MCP**: `figcraft-cli mcp` (headless) placed both images, exported each layer to PNG in an sRGB file and in a Display P3 file, and read the pixels back: the tagged image's four patches match the published Adobe RGB → XYZ → sRGB / Display P3 matrices exactly (sRGB: (0,201,67), (230,79,56), (0,90,174), (129,129,129); P3: (0,197,84), (213,90,67), (14,88,168), (129,129,129)); the untagged image exports its original values in the sRGB file and the usual sRGB → P3 values in the P3 file; the P3 exports carry the ICC profile. - **Web**: `cargo xtask wasm` passes (part of the gate). - `cargo +1.99.0 xtask ci` passes (fmt, clippy, tests, layers, assets, wasm). ## Tests - `render` (`image_profile_tests`, 9 tests, every ICC profile built in code from published constants; no profile files): Adobe RGB PNG into sRGB and Display P3 against the published matrices (alpha kept, untagged pixels untouched); the profile read from PNG, JPG, WebP and TIFF; `curv` table and `para` curves; a Display P3 profile staying put in a P3 file and matching the file conversion in an sRGB one; grey `kTRC`; CMYK, junk, truncated and oversized profiles falling back to sRGB; PNG `sRGB` / `gAMA` / `cHRM` (sRGB-equivalent, linear, Adobe primaries, nonsense values); profile names cached and cleaned of control characters; the junk sweep. ## Not in this PR - PNG `cICP` without `iCCP`, and GIF/BMP profiles, are ignored (taken as sRGB); FigCraft's own P3 PNG exports carry `iCCP` too, so they re-import correctly. - The `image` crate drops the ICC tag of very small TIFFs (16×16 lost it, 64×64 kept it in testing); real photos are far larger. - A PNG that has only `gAMA`/`cHRM` (no ICC) is embedded as is in PDF exports, where viewers take it as sRGB; the canvas and pixel exports convert it. - Image adjustments on a profiled image apply to the converted values (on an untagged image they still apply before the sRGB → P3 conversion, as before). - Progress docs: area 5 (Paint) 87%/66% → 90%/67%, 4–7 h → 3–6 h; feature breadth, ready for real work and full parity hours unchanged at this rounding (target-app-parity); PNG, JPG, WebP and TIFF rows in file-format-parity. |
||
|
|
cc30a261e7 |
M1: layers panel keyboard (box selection, F2 rename, context menu) and stroke styles (#135)
## What
**Layers panel from the keyboard** (two of the three open items of area 3, plus long-list QA)
- **F2 renames** the selected layer (a second key for `object.rename`, next to Cmd/Ctrl+R) from the canvas, after a click on a row, or from a layer row that has the keyboard (F6 mode, where the canvas shortcuts are off, so the layers branch of `a11y::keys` runs it). When the rename ends the row gets the keyboard back (`a11y::refocus_row`); Escape in the field cancels and stays in the panel instead of jumping to the canvas.
- **Shift+F10 opens the context menu** (`view.contextMenu`, a new command that only asks the UI): at the focused layer row when the layers panel has the keyboard (F6 or a clicked row), else on the canvas just below the selection (the middle of the canvas with nothing selected). The menu takes the keyboard like the other menus opened from the keyboard (`a11y::popup`), and Escape gives it back to the row or the canvas. The Menu key can't be supported: egui 0.36 has no key for it.
- **Keyboard box selection** (`select.area`, F8): for keyboard-only users, a selection box appears around the selection (or in the middle of the view); the arrows move it, Shift+arrows move its far corner (10 screen pixels a press), Enter selects what it covers exactly like a marquee (Shift+Enter toggles, Cmd/Ctrl+Enter takes nested layers), Escape or a click drops it. A toast and the canvas's screen-reader description say the box's size and the keys. With a box (`{x, y, width, height, add?, deep?}`) the command selects directly, for agents. The marquee's rules (top-level layers, frames only when covered, the children of the frame the box starts in, the focus view, Shift toggling) moved into `Session::marquee_within` / `marquee_selection`, shared by the pointer marquee and the command.
- **Long lists (10k layers)**: a test that a layer near the bottom of a 10k-layer list is revealed (parents expanded, list scrolled) and that F6 lands the keyboard on its row and the arrows walk on from it; the UI bench (`perf_ui_frames`) gained rows for expanding every layer, arrow keys down the expanded list, revealing a layer at the bottom and collapsing everything. Release, 10k layers: first frame with every layer expanded 1.22 ms, an arrow key 1.04 ms, reveal 0.30 ms, collapse all 0.37 ms, idle 0.17 ms.
**Stroke styles** (area 6)
- What the public documentation describes for strokes was mostly already in: colour styles apply to strokes from the stroke section's style picker (`style.apply {target: "stroke"}`, linked, following `style.update`, one undo step, `style.detach {which: "stroke"}` keeping the colour), and the stroke weight binds to number variables. A test now covers that path.
- Missing piece, added: **each side's stroke weight binds to a number variable on its own** (`strokeTopWeight`, `strokeRightWeight`, `strokeBottomWeight`, `strokeLeftWeight`): `node.set` keys (setting or binding one side turns on individual strokes with the other sides at the weight), the stroke-weight variable scope, the stroke override group for instances, and REST `boundVariables` import. The Design panel's per-side fields are bindable, two to a row like the corner radii so a variable's name fits.
New strings go through `tr(...)` with `en.toml` / `es.toml` entries, and the two new commands have Spanish labels. Shortcuts are only in `keymap.rs`. Nothing redraws while idle (the box draws only when a key moves it). Selecting from the box leaves no undo step; each stroke edit is one.
## Verified
- **Desktop app** over the control channel (`target/debug/figcraft.exe` from this worktree's target dir, `--control <free port>`, separate `APPDATA`, keys sent as real `ui.input` key events, stopped by PID): F2 on a selected layer opens its name field in the layers panel (screenshot), typing and Enter rename it; F6 to the layers panel, Shift+F10 opens the menu at the Avatar row with Copy focused (screenshot), Escape gives the keyboard back to the row, F2 + typing + Enter renames it and the row keeps the keyboard ("Profile"); Shift+F10 on the canvas opens the menu for the selected frame with Copy focused (screenshot); F8 shows a box around the selected frame, Shift+arrows stretch it over a second layer (screenshot with the hint toast), Enter selects both; a rectangle with the colour style "Brand/Accent" on its stroke and its top weight bound to a number variable (screenshot: the style chip in the stroke section, the red stroke thicker at the top, the per-side field showing the variable).
- **MCP**: `figcraft-cli mcp` (headless): created a paint style, `style.apply {target: "stroke"}`, `object.rename`, bound `strokeLeftWeight` to a variable and ran `select.area` over the layer; `inspect_document` shows the name "Outlined card", `styles.stroke: "S:1"` with the style's colour on the strokes, `strokeSides [2, 2, 2, 6]`, `boundVariables {strokeLeftWeight}`, and one undo step per command.
- **Web**: `cargo xtask wasm` passes (part of the gate).
- **Performance**: release UI bench at 10k layers (numbers above).
- `cargo +1.99.0 xtask ci` passes (fmt, clippy, tests, layers, assets, wasm), after merging #131 and #132.
## Tests
- `ui-egui` `layers_keyboard_tests` (3): F2 from the canvas and from a focused row (the row gets the keyboard back, Escape cancels without leaving the panel); Shift+F10 at a focused row, on the canvas below the selection and after a clicked row, the menu taking and giving back the keyboard; F8: the box around the selection, the screen-reader description, Shift+arrows sizing, Enter selecting, arrows moving it without nudging layers, Escape, the middle of the view with nothing selected, a click dropping it.
- `ui-egui` `perf_tests::a_layer_revealed_at_the_bottom_of_10k_layers_gets_the_keyboard_on_its_row`, plus the new bench rows.
- `engine` `select_area_tests` (2): marquee rules through the command (negative sizes, frames, starting inside a frame, deep, add toggling, empty spots), the UI request with no box, bad boxes refused (the junk sweep covers both new commands too).
- `engine` `stroke_style_tests` (2): a colour style on strokes (link, undo, style edits, detach); stroke weight and per-side weights bound to variables following the frame's mode, `node.set` per-side keys, scopes and override groups.
## Not in this PR
- The Menu (application) key: egui 0.36 doesn't deliver it.
- Enter doesn't rename: on the canvas and a focused row it already selects children / activates the row.
- The keyboard box doesn't pan the view when it is moved off screen (zoom and pan keys still work while it shows).
- Shift+F10 does nothing while a toolbar or properties control has the keyboard.
- Progress docs: area 3 95%/78% → 98%/82%, 2–4 h → 1–2 h; area 6 90%/72% → 95%/75%, 1–3 h → 1–2 h; mainstream depth 67.8% → 68.1%; percentages unchanged at this rounding; full parity ~323–621 h → ~322–618 h (target-app-parity, ROADMAP); ui-parity rows (context menus, keyboard box selection, layers from the keyboard); `docs/mcp.md` documents `object.rename` without a name, `view.contextMenu`, `select.area` and stroke styles and variables.
|
||
|
|
894f25c4c4 |
M1: selection and transform behaviour suite and fixes (#133)
A selection and transform behaviour suite derived from public documentation, with fixes: Alt-resizing shrinks as well as grows; groups, Scale-constrained frames and multiple selections resize each layer along its own axes keeping rotation and flip; several layers flip together past an edge; a flipped layer reads back the typed rotation; resizes land on whole pixels with the pixel grid; presses inside tiny layers move them; locked layers get no handles; a marquee can start on a top-level frame's background; select inverse skips hidden layers. Adds equal-spacing snapping with labelled gaps and Cmd/Ctrl to ignore constraints while resizing a frame. |
||
|
|
2f49fcc490 |
M6: .fig import (best effort) (#132)
.fig files open through file.open, file.import, drag and drop, the dialogs, the CLI and MCP. A generic reader written from the public format description decodes the zip, the deflate/zstd chunks and the kiwi schema and message, then maps nodes by field name onto the REST importer: layer tree, geometry, paints with styles resolved, strokes, effects, text runs, vectors as outlines, auto layout including grid, constraints, components, sets, instances expanded from their main with overrides, styles, variables with modes and bindings, prototyping and image fills. Unsupported layers come back as warnings. Fuzzed; tested on fixtures generated in code. New dependency: ruzstd (pure-Rust zstd decoder). |
||
|
|
d01d18cd88 |
M2: auto layout behaviour suite and fixes (#130)
A 64-case behaviour suite derived from public documentation (sizing, padding, strokes, gaps, alignment, wrap, absolute position, grid, stacking) plus command-level tests. Fixes: frames never shrink below their padding, only the frame's own inside stroke counts when strokes are included (now the default for new auto layout), auto gaps stop at 0, grid fr tracks never go below their content, and canvas stacking (reverseZ) now drives drawing, exports and hit testing. New settings: evenly/around auto gaps, vertical wrap with a column gap, per-child grid cell alignment, strokes included/excluded and canvas stacking in the Design panel. |
||
|
|
24e457240e |
M5: Display P3: gamut warning and soft-proof sRGB (#127)
True wide-gamut display is blocked upstream (egui-wgpu 0.36 always picks an 8-bit sRGB surface and draws sRGB-encoded UI), so the colour picker warns when a Display P3 colour is outside sRGB and View > Soft-proof sRGB stripes the canvas pixels sRGB can't show. Exports keep wide-gamut colours as before. |
||
|
|
f49142dad9 |
M1: several windows (#126)
File > New window, Open in new window on a tab, and dragging a tab out open files in separate OS windows (egui deferred viewports, redrawn only when needed), sharing one session, preferences, libraries and clipboard. Closing a window asks about unsaved files (the main window now asks too); closing the last window quits. window.new / window.list / window.focus and per-window screenshots for the control channel and attached MCP. The browser keeps one window. |
||
|
|
a2e2012fdc |
M7: localization layer (#128)
UI strings come from per-language TOML catalogs compiled in (English text as the key and fallback, named placeholders, one/other plurals, context suffixes); command labels are translated at display time while ids, CLI, MCP and control output stay English; accessibility names follow. Preferences > Language (System / English / Español, live; the system language from the OS or the browser), pref.language for scripts. A key check test guards the catalogs. The Spanish catalog is a machine draft awaiting native review; docs/translating.md explains adding a language. |
||
|
|
76df2d4136 |
M9: hosting figcraft-server: wss in the desktop app, server releases, deployment guide (#125)
The desktop app joins wss:// sessions (rustls with webpki roots, certificates always checked, clear errors; FIGCRAFT_CA_FILE for private authorities), with one TLS session shared by the reader and writer threads. figcraft-server stops gracefully on SIGTERM/SIGINT/SIGHUP/Ctrl+C, ships in Linux x86_64/aarch64 release archives, and docs/server.md gains a Deploy on a Linux server guide with a hardened systemd unit, Caddyfile, nginx config and Dockerfile in packaging/server. |
||
|
|
a003e3de4f |
M7: plugin panels and settings (#124)
Plugin ABI v2: a plugin can show a panel (text, number, checkbox, select, colour, list, notes, buttons) rendered with FigCraft's widgets, react to events with its state kept between calls (each editing callback one undo step, full sandbox limits on every call), and store per-plugin settings (64 KB). plugin.ui / plugin.event / plugin.close drive panels from the CLI and MCP; v1 plugins run unchanged. The Swatch grid example gains a Palettes panel that remembers the last palette. |
||
|
|
09a3b74466 |
M8: canvas fixes: boolean hit testing, group drags, frame outlines and slices view, a flaky idle test (#123)
Clicks on a boolean group test its computed result, so a click in a subtracted hole reaches what is behind it. Layers inside a group, boolean group or instance stay in their parent when dragged (one shared rule). View > Frame outlines and Slices now draw. The idle test's flake came from the render worker clearing its in-flight flag before waking the UI; jobs now carry sequence numbers. |
||
|
|
13ab3f150f |
M6: code connect: link components to real code (#122)
Components (or sets) can be linked per language to a real code component (import path, name, and a template whose {prop} and {prop ? "a" : "b"} placeholders take the instance's property values, escaped per language). Dev Mode's Inspect panel shows the connected snippet with its import for instances, a Connect to code editor with live preview, and read-only plugins. Links export to and import from figcraft.connect.json.
|
||
|
|
cb5435d5ff |
M4: stretch and scatter brushes, pen pressure (#120)
Strokes can use a brush: eight presets drawn in code (four stretched along the path, four scattered with spacing, size, rotation and offset jitter from a fixed seed), drawn by the shared stroke-outline code in canvas, PNG, SVG, PDF and outline stroke, with caps on copies and steps. Pointer events carry pen pressure; the Pencil records it as the stroke's width profile, which brushes follow (Windows pens and browser styluses; constant width elsewhere). |
||
|
|
f62cb4b1f6 |
M4: text on a path and pattern fills (#118)
Text layers can follow a path (start offset, flip, alignment; edited in place; canvas, PNG, SVG, PDF; SVG textPath imports as real text on a path). A new pattern paint tiles a source layer in grid or hex rows with scale, spacing and alignment, live as the source changes (SVG pattern, PDF vector tiling pattern). |
||
|
|
09600d6da2 |
M9: team libraries on the server; swap library (#117)
A file's library can be published to figcraft-server and enabled in other files by a share link (view access is enough); placing, styles, variables and library updates with overrides kept work as for local libraries, with a background check while the Assets panel is open. library.swap re-points instances, styles and variables to another library by name (one undo step, unmatched items listed). The server keeps per-file usage reports (library.usage). |
||
|
|
7f9f2a0873 |
M9: branches: create, update from main, review and merge (#119)
Branches live inside the .figcraft file (base and head snapshots, own id space). Update from main and merge are three-way merges over the collaboration change sets; a conflict (same property changed on both sides, or delete vs edit) must be resolved branch-or-main before merging, which is one undo step on main. The review dialog reuses Dev Mode's compare view; branch tabs show a banner; File > Create branch; branch.* commands for CLI and MCP. |
||
|
|
ee82cdd138 |
M3: extended variable collections, variables import/export, gradient stop and variant bindings (#114)
Library collections can be extended in a consuming file (inherited modes, per-mode overrides with reset, extra variables; library updates keep overrides). Variables export to the REST JSON shape or CSS custom properties and import back (merged by name, validated by the REST reader, one undo step). Gradient stops and variant properties bind to variables. Also fixes clicking a gradient stop adding a new stop. |
||
|
|
07fa4d92ed |
M9: standalone collaboration server with share links and permissions (#112)
figcraft-server hosts many files over the same hub the app uses for sessions, persists each as a checksummed snapshot plus an append-only log (synced before replies, compacted, damaged files skipped and reported) so restarts lose nothing confirmed, and gives per-file share links with edit or view access (hashed 128-bit tokens, revocable). The app can publish a file to a server and join server-hosted files; view-only tabs refuse edits. Protocol version 2. |
||
|
|
08d91869cb |
M4: live boolean editing, arc fields, shape builder on strokes, one-sided widths (#111)
Boolean operands are editable in place with live results (per-operand operations, cached prefixes so a drag recomputes from the edited operand up); a Design panel Arc section; the shape builder takes strokes and open paths as outlined areas; variable width points can be wider on one side. |
||
|
|
1444754f66 |
M9: collaboration polish: unique ids for styles, variables and comments; live text carets (#107)
- Styles, variables, collections, modes and comment threads created in a live session take ids from the creator's client space (figcraft_doc::ids: a number from the node id allocator, client_id << 32 | counter); outside a session ids are made exactly as before. The server refuses a new entry in another connected client's space (removed entries can be put back by undo), keeps removed entries in its saved state, and starts client ids above every space used by any kind of id. - Others' text carets and selected characters show on the canvas in their colour at the glyph positions, with their name above the caret (no blinking, no frames of their own). Typing in a layer someone else is typing in says so (toast and tag); collab.peers reports who types where. - Toasts wrap at a readable width. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
435415770f |
M9: live sessions in the browser; join links and dialog polish (#106)
- Browser client (step 4): the web build joins sessions a desktop app hosts
through the browser's WebSocket API (web-sys, binary frames, the same
message encoding). Its connection logic is a target-independent state
machine (collab-net driver.rs): hello on open, changes and presence held
until the welcome, throttled presence, keep-alive pings, a silent host
counted as dropped, reconnect with backoff, final refusals. Tested natively
against a scripted socket and a real host over loopback. Hosting stays
desktop-only (LiveLink::can_host; collab.peers canHost). https pages are
told up front that they can only reach ws:// sessions on this computer.
- Shared join logic (join.rs: GuestConfig, outcomes, taking in host messages)
and transport limits (limits.rs) used by the native guest and the browser.
- Invite links: `figcraft figcraft://join?...` joins on start (a bad link
opens the Join dialog with it); file:// arguments are paths. Windows
registers a per-user figcraft:// opener (HKCU\Software\Classes\figcraft)
behind a Registry trait (tests use a fake): collab.registerLinks {on?} and a
Preferences checkbox, default on for installed builds, never for builds
from source. macOS Info.plist declares CFBundleURLTypes; the Linux desktop
entry declares x-scheme-handler/figcraft and passes %U.
- Join dialog: Escape closes it and no longer reaches the canvas (selection
kept); its field has the keyboard when it opens.
- Docs: collab-protocol (browser client, links, mixed content), mcp,
control-protocol, README, web hosting notes, collaboration wording in the
progress docs (numbers unchanged).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
94ff3d4df6 |
M9: collaboration: per-user undo, rejections, reconnect (#105)
* M9: collaboration: per-user undo, rejections, reconnect
Step 3 of docs/collab-protocol.md, the parts that don't need the transport:
- Per-user undo and redo (figcraft_collab::Client::{undo, redo}): each undoable batch is a step
(values before/after by target, nodes created/deleted). Others' batches ordered after the step
take its targets for good; creates invert to deletes (not when others built inside), deletes
re-create the subtree. An undo or redo waits for every earlier batch and is remade from the
confirmed state when it goes out; derived data follows as a follow-up batch.
- Rejections: refused batches leave the history (with an undo waiting on them) and queue a
Notice; the engine shows it through Session::message (the UI's toast).
- Offline queue and reconnect: Hello.resume {session, client_id, seq}; the server keeps a
bounded log and answers with the missed batches or a snapshot, plus what it did with the
client's batches (processed {last_batch, refused}); queued batches then go out.
Server::state / restore persist the session (client-id counter, log, session id).
- Presence carries editing_text {node, caret, selection}.
- Engine: documents in a live session (LiveDoc, Session::collab_*): commands, gestures and
groups sync as one step each; edit.undo / edit.redo route to the per-user history; remote
edits show under a gesture in progress. Outside a session snapshot undo is unchanged.
- Sim: dropped connections, undo/redo, and a checker that no undo reverts a change by someone
else its author had seen. New tests in undo_tests, reconnect_tests, collab_live_tests,
collab_undo_tests.
- CLAUDE.md / AGENTS.md: collab in the L2 layer list.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M9: collaboration wording in the progress docs after per-user undo and reconnect
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M9: protocol doc: what the engine's live-session tests cover
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
4d7a9187ff |
M9: live sessions: host from the app, invite links, live cursors and presence (#104)
* M9: real-time collaboration protocol (design) docs/collab-protocol.md: WebSocket sessions with a server-ordered, per-property last-writer-wins document sync, client-scoped ids, fractional positions for children, ephemeral presence (cursors, selections, follow), per-user undo, hosting from the app first and a self-hosted server next, and the implementation plan. ROADMAP gains M9. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * M9: collaboration core: ids, change sets, server ordering, convergence Step 1 of docs/collab-protocol.md, no networking: - Client-scoped node ids: Document::set_client_id makes alloc_id hand out client << 32 | counter; single-user files allocate as before; the id counter is no longer part of document equality. - crates/collab (layer 2): the synced object/property model (every saved Node field, plus document-level styles, comments, variables, images, dev snapshots, library data), Change batches, an O(changed) diff, strict and lenient apply, fractional sibling positions, snapshots. - Server (ordering, validation, rejects with fixes) and Client (optimistic view, replay under remote batches, author-side repair of derived data) as pure state machines; binary (version byte + postcard, JSON values) and JSON message forms, fuzzed decoders. - Engine: normalize as the recompute hook, collab.diff. - Tests: round trips over random real commands, an N-client convergence fuzz test over delayed in-order links, junk batches. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * M9: fmt after merge * M9: live sessions: host from the app, invite links, live cursors and presence Step 2 of docs/collab-protocol.md, networking and presence half: - crates/collab-net (layer 6): WebSocket transport (sync tungstenite, a reader and a writer thread per connection, no async runtime), the session host the app embeds (runs figcraft_collab::Server as the authority, carries its messages), invite links, presence throttling (~30 Hz, latest wins), limits (sockets, hello/idle timeouts, message size, rate, slow clients), keep-alive, clean close, reconnect with backoff; a browser stand-in until the web-sys client (step 4). - engine: live session state (status, peers, presence, follow) and the collab.host/stop/join/leave/peers/follow/cursor commands; the network is reached only through the LiveLink the app installs. - ui-egui: remote cursors (pointer drawn in code, name tag, gliding), selection outlines (once documents sync), avatars in the right panel with follow, the Share popover (start, invite link + URL with Copy, stop), File > Join live session. - desktop app and MCP headless session install the link; xtask layers keeps networking crates out of L0-L5. - Documents don't sync between apps yet (follow-up). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * M9: live sessions: document sync between apps - The host's session runs figcraft_collab::Server seeded with the shared document; every app (the host's too, through the hub without a socket) is a collab Client. Guests open the host's file from the welcome as a new tab; leaving keeps it as a local copy. - Engine: after each local action (a gesture once it ends; nothing during version or text previews) the session document is diffed into one Changes batch; server batches go through Client::receive with EngineRecompute and are installed with Session::apply_remote, which never touches the undo history. Batches arriving mid-gesture wait. - Transport: Changes/Reject/Welcome reach the engine as Event::Sync; LiveLink::send carries batches in order; hellos are limited to 64 KiB, joined clients to the core's message size. - Tests: engine sync against a real Server (ids in the client's space, one batch per gesture, remote edits skip undo, deferral, closing the file leaves), three apps over loopback converging (create, move, recolour, delete, rename) and a late joiner getting the server's state. - Docs: collab-protocol, mcp, README, roadmap docs; the UI's "cursors only" note is gone, replaced by the per-person undo caveat. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
4924512f4e |
M9: collaboration core: ids, change sets, server ordering, convergence (#103)
* M9: real-time collaboration protocol (design) docs/collab-protocol.md: WebSocket sessions with a server-ordered, per-property last-writer-wins document sync, client-scoped ids, fractional positions for children, ephemeral presence (cursors, selections, follow), per-user undo, hosting from the app first and a self-hosted server next, and the implementation plan. ROADMAP gains M9. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * M9: collaboration core: ids, change sets, server ordering, convergence Step 1 of docs/collab-protocol.md, no networking: - Client-scoped node ids: Document::set_client_id makes alloc_id hand out client << 32 | counter; single-user files allocate as before; the id counter is no longer part of document equality. - crates/collab (layer 2): the synced object/property model (every saved Node field, plus document-level styles, comments, variables, images, dev snapshots, library data), Change batches, an O(changed) diff, strict and lenient apply, fractional sibling positions, snapshots. - Server (ordering, validation, rejects with fixes) and Client (optimistic view, replay under remote batches, author-side repair of derived data) as pure state machines; binary (version byte + postcard, JSON values) and JSON message forms, fuzzed decoders. - Engine: normalize as the recompute hook, collab.diff. - Tests: round trips over random real commands, an N-client convergence fuzz test over delayed in-order links, junk batches. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * M9: fmt after merge --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
041b3f4e5d |
M7: WASM plugins (#101)
Sandboxed WebAssembly plugins whose API is the command registry: - figcraft-plugin (new crate, L2): the figcraft-plugin.json manifest (menu commands with parameters, read/write access, command allowlist) and the sandbox: wasmi (pure Rust, no JIT, also in the web build) with fuel metering, a memory cap, no WASI and seven host functions (call, selection, reply_len, reply_read, log, notify, set_output). - Engine: plugin.list / plugin.import / plugin.remove / plugin.run, plugins.runLast (Alt+Cmd+P; Ctrl+Alt+Shift+P on Windows/Linux) and plugins.manage. A run is one undo step (Session::grouped, which command.batch now uses too) and is rolled back when it fails. Files, other documents, the undo history, dialogs and plugins are off limits; read-only plugins can't change the document. Installed plugins live in the preferences; development plugins re-read their module every run. - UI: installed plugins in the Plugins menu, a parameters form drawn from the manifest, Manage plugins, Plugins > Development > Import plugin from manifest. - plugins/: a no_std Rust SDK and two example plugins (Tidy names, Swatch grid), built for wasm32 by `cargo xtask plugins` (part of `cargo xtask wasm`). - Docs: docs/plugins.md (manifest, permissions, limits, ABI, SDK), docs/mcp.md, docs/control-protocol.md, README, ROADMAP, parity. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
abe5ff3a1a |
M6: Dev Mode compare changes, focus view, annotation categories (#97)
- Compare changes: a ready-for-dev frame or section keeps a snapshot of itself from just before
its first change (Document::dev_snapshots, saved only while changed; dropped on resolve, re-mark,
other status or delete). `dev.compare {id?, show?}` diffs it layer by layer: added/removed
layers, and property changes formatted like Inspect (`width 120 → 136`, `fill #FF0000 →
#E03030`, text, parent, order, component, auto layout, typography, effects). The compare view
shows before / now side by side or overlaid with a slider, changed layers outlined, the list of
changes and Resolve change.
- Focus view: `dev.focus {id? | direction}` / `dev.unfocus`: one frame drawn and hit alone
(render Options::solo), a bar with name, status, Compare changes, previous / next ready frame and
exit; Escape leaves.
- Annotation categories: defaults plus the file's own with colours (`annotation.categories`,
`addCategory`, `editCategory`, `removeCategory`), `category` on annotations, coloured callouts,
a category filter and editor in the Inspect panel; stored backwards-compatibly.
- Shared widgets::icon_button / canvas_banner / dropdown_lead; has_status moved to the engine.
- Tests: devcompare_tests, devfocus_tests, annotation_categories_tests, doc handoff_tests, render
solo_tests, UI dev_mode_tests.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
c49a72ea88 |
M8: accessibility: screen reader names, keyboard navigation mode (#96)
* M8: accessibility: screen reader names, keyboard navigation mode
- Screen readers: the custom-painted controls report a role, name, value and
state to AccessKit (number and text fields, dropdowns, segmented and toggle
buttons, tools with their keys, layer rows as tree items, pages, tabs, menu
items, the canvas describing the selection), from the shared widget helpers
and a `Tip` trait for ad-hoc icon buttons. Regions are named containers.
- Keyboard navigation: F6 / Shift+F6 (view.nextRegion / view.previousRegion)
cycle the toolbar, layers, canvas and properties; Tab / Shift+Tab move within
a region and wrap; Enter or Space activate; arrows move along the toolbar and
through the layer rows; menus and flyouts opened from the keyboard take it and
give it back; Escape returns to the canvas. A focus ring (new `focus` token)
shows until the next pointer press. Tab on the canvas selects the next layer
again instead of moving egui's focus to the tab strip; Enter that starts a
field edit no longer ends it.
- Reduced motion: pref.reduceMotion and the system setting (Windows
UISettings, macOS, GNOME, prefers-reduced-motion) turn egui's animations off.
- Contrast: light secondary and accent text meet WCAG AA.
- ui.inspect reports focus {region, keyboard, control} and reduceMotion;
ui.set {focusRegion}.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M8: parity report command count after merging main
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M8: accessibility: regression test against nested egui context locks
A debug build's temporary logging called the context inside a context closure and hung; the shipped code never does. Document the rule in a11y.rs and run every accessibility path with the AccessKit tree on, so a nested call fails the test (debug builds panic after 10 s) instead of hanging.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
c662e56de4 |
M5: drag to reorder prototype actions; variable mode lookups in expressions (#94)
- Interaction editor: actions (top level and inside conditional blocks) reorder by dragging their row's grip (list_drag, now also for lists shown in model order), one proto.action.move per drop; the open action stays open where it lands; Escape cancels a row drag without deselecting or closing what holds the list. The ... menu keeps Move up / Move down. - Prototype expressions: `accent[Dark]` reads a variable's value in one mode of its collection (Expr::InMode) and `mode(Theme)` is the name of the mode a collection is in where the interaction runs (Expr::Mode); parsed, type-checked, displayed, stored by id (names accepted in JSON), evaluated with the layer's modes. REST import maps VAR_MODE_LOOKUP over a variable and one of its modes. - Overlays opened through commands close on a click outside unless told not to, as in the editor. - Collection::find_mode / current_mode replace repeated lookups. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
3c63e6d7c8 |
M3: components at scale; Alt-drag to swap instances (#91)
* M3: components at scale; Alt-drag to swap instances - Synthetic design system (engine `synthetic_ds`): ~300 components (icons; button, input, card, nav and badge sets of size x state x type x theme variants), boolean/text/instance swap/slot properties, instances nested four deep, text and paint styles, light/dark variables bound across it, and 2,000 instances with overrides at every level. - Propagation: value, size and added/removed-layer edits of mains reach their copies directly; other edits rebuild each affected instance once, inner mains first; rebuilds keep unchanged layers shared; bound variables no longer touch unchanged layers; auto layout re-flows only where sizes or places changed. - Fixes: values from variables (theme switches, bound radii) are no longer recorded as overrides; a detached variable on an instance layer is an override that survives main edits; exposed nested instances stay exposed; swapped nested instances keep their layer names and the ids/overrides of layers their own main adds; renamed nested instances keep their name when detached out. - Instance swap properties have preferred values. `instance.swapAt` / `library.swapAt` and Alt-drop from the Assets panel onto an instance swap it (highlighted while dragging). - Instances pasted into other files follow their mains (library copies shared, published components used from their library, other mains left behind as a missing main). - Assets panel lays out only visible tiles and renders thumbnails lazily with a cache invalidated per changed component; instance panel lists components once per revision. - `figcraft-cli perf --instances`, UI frame bench and tests in components_scale_tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * M3: the swap perf case drops on a glyph the hit test reaches Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Merge origin/main into feat/components-scale; weighted depth 79% Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
fe550fc14a |
M6: PDF print options (bleed, marks, CMYK) (#92)
PDF export gains print options, saved on PDF export settings and taken by file.export, file.exportLayers, file.exportFramesPdf, export.add / export.update, figcraft-cli export and the MCP export tool: - a bleed (points or a length like 3mm / 0.125in, up to 2 in) that grows each page and shows what lies past the frame: its fills reach out to the bleed edge and its children are clipped there instead; a print page of one frame is trimmed to the frame's box; - crop and bleed marks: 0.25 pt hairlines outside the bleed in registration colour; - MediaBox / BleedBox / TrimBox; - CMYK colour: fills, strokes, text, gradient stops, images and rasterised effects converted from sRGB with the naive, deterministic K = 1 - max(R, G, B) formula; DeviceCMYK without an output intent (no CMYK ICC profile can ship in the repository), soft masks stay RGB. The Design panel shows a PDF row's print settings as a summary line that opens them; the Export dialog has print options for its PDF preset. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
e27f2751d4 |
M6: SVG import fidelity (#90)
SVG import comes in editable and looking right: groups named from their ids with transforms baked into the layers (rotated, scaled and skewed groups, viewBox scaling and <use> offsets used to be lost); rectangles, ellipses and lines recognised and kept as those shapes; gradients with exact handles, reflect/repeat and focal points; patterns as image fills; clip paths and masks as vector, luminance and alpha masks; shadow and blur filter chains as drop shadows, inner shadows and layer blurs; blend modes; text as text layers (it used to vanish); images and nested SVG images. What can't be represented is reported as import warnings (file.import / file.open warnings, figcraft-cli info notes). Linked files are never read, and documents nested too deep for the parser are refused instead of crashing. Pasted SVG is recognised after XML declarations, comments and doctypes and lands centred in the view (file.import center). SVG export writes vector masks as clip paths and alpha/luminance masks as the mask layer drawn, so masks survive a round trip. Tests: a corpus of 21 synthetic SVG files with hand-worked expected pixels (158 probes), structure checks and export round trips; unit tests for shape recognition, paints, filters, structure, mask export; hostile input and byte-mangled corpus fuzzing; engine file command tests. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
94aab893b0 |
M7: keyboard shortcut parity (#88)
One keymap (figcraft_engine::keymap) now holds every default shortcut, grouped like the public shortcut reference, with macOS and Windows/Linux keys where they differ and canvas / text-editing contexts. The menus, canvas, control channel, MCP key tool and Keyboard shortcuts panel all resolve keys there; the catalog keeps only the menu tree. - Fixed: Cmd/Ctrl+K made a link on the canvas (now Actions; link only while typing), Shift+1 and Ctrl+Shift+? never matched (key names are normalised), mask / bring to front / send to back / tidy up / layout grids on Windows and Linux, distribute on macOS, outlines (Shift+Cmd+O), pixel grid (Cmd+'), paint bucket B in vector edit mode. - Added: Cmd/Ctrl+/, + and - zoom, Cmd/Ctrl+0, hold Z zoom tool (view.zoomAt), two quick digits for exact opacity (object.opacityKeys), Alt+Shift+U/S/I/E booleans, Alt+/ and Shift+/ remove fill/stroke, pixel preview, Shift+E / Shift+D / Shift+Cmd+\ panel keys, Ctrl+Tab between files (doc.nextTab / doc.previousTab), Shift+I, Shift+Space preview, Home/End frames, Ctrl+C pick colour on macOS. - Keyboard shortcuts panel: docked at the bottom, a tab per group, search, keycaps per platform; tooltips show the platform's keys. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
61c2b8d0f0 |
M1: rulers and guides on rotated frames (#87)
A rotated frame's guides run along its own axes and span only the frame (drawn, hit-tested and dragged along them, with a cursor that follows the line); a guide dropped from a ruler onto a turned frame converts the page point into the frame's coordinates, along its axis nearer the ruler's. Layers inside a rotated frame snap in its space: to its guides, edges, layout grids and siblings along its axes, keeping whole pixels there. New engine module `guides` (segment, offset_at, frame_axis, line_in); measuring finds quarter-turned frames' guides too. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
70b2666c90 |
M3: progressive blur, inner shadows under strokes, noise and texture in SVG (#86)
- Inner shadows paint over the fills and under the children and strokes, as documented (canvas, PNG, PDF and SVG alike; text fills every run before the inner shadow and the strokes). - Layer and background blur gain a progressive type: the radius ramps linearly from a start radius at a start point to the end radius at an end point (unit-box points, like gradient handles). Rendered by blending a few geometric blur levels, each blurred only around the pixels that use it; bands and partial renders match one piece. Design panel type menu and Start/End fields, on-canvas handles while the settings are open, effect.add/effect.set params, startRadius variable bindings, scale tool, REST import (blurType, startRadius, startOffset, endOffset), Inspect row; CSS leaves progressive blurs out. - SVG export embeds renders for what filters can't match: noise grains over the vector layer (crisp, blended), texture and progressive layer blur whole, background blur and glass from what lies under the layer among the exported layers. PDF and SVG share the renderer's helpers. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
abf7a23fdd |
M4: arc handles on the canvas (#84)
* M4: arc handles on the canvas A selected ellipse shows its arc handles with the Move tool, as public documentation of Figma-class tools describes them: - Sweep, inside the right edge on the centre line: dragged round the ellipse it opens an arc (positive up, negative down, continuous through a full turn) and closes again back at the start. - Start (a dot inside), once there is an arc: turns the arc, keeping its sweep. - Ratio, at the centre or the inner edge halfway round a ring: drags out to a ring, and into the gap to show the other part. - Shift snaps angles to 15 degree steps and the ratio to 5% steps; a live readout (Sweep 75%, Start 90 degrees, Ratio 40%) shows beside the handle. - Rotated and flipped ellipses, any zoom; hidden under 40 px on screen. One undo step per drag through node.set's arcStart/arcSweep/arcInner; Escape cancels. The geometry lives in crates/engine/src/arc_handles.rs (tests in arc_handles_tests.rs); the gesture in tools.rs; ui.inspect / inspect_ui list tool.arcHandles and the overlay's arc readout. Docs, README and ROADMAP updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Merge origin/main into feat/arc-handles; weighted depth 78% Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
599495eef5 |
M5: build conditionals in the interaction editor; hotspot hints; inspect in the presenter (#82)
- Expressions as text: Expr::parse (variables by name, literals, and/or/not, comparisons,
arithmetic, text joins) with plain-English errors, type checks for conditions and set variable
values; display reads back as the same expression.
- New commands proto.action.add/set/remove/move and proto.conditional.addBlock/setCondition/
removeBlock edit single actions by path, inside conditionals too; `expression` text accepted by
these and proto.interaction.add/set.
- Interaction editor: an action list (open, move, remove, add), Add action > Conditional, if /
else if / else blocks with a condition builder (variable picker, operator, value or variable) or
an expression field with inline errors, nested actions, set variable expressions.
- Presenter: hotspot hints toggle (Options menu, proto.presentOptions {hotspotHints}), hotspots from
the engine (proto_run::hotspots) and in ui.inspect; an Inspect panel showing the hovered layer's
properties ({inspect}).
- Fields keep the keyboard on the click that opens them, and the Enter that commits a field no
longer reaches the shortcuts.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
4565c4ab93 |
M3: reorder paints, effects and layout grids in the Design panel; grid and instance property variable bindings (#81)
Fill, stroke, effect and layout grid rows drag to reorder by their grip (a handle shows over the
row, an insertion line where it lands, the row's label follows the pointer like a dragged layer;
Escape cancels; lists that differ across the selection have nothing to drag). New commands
`fill.move`, `stroke.move` and `grid.move {ids?, from, to}` join `effect.move` (lists bottom first,
one undo step; items keep everything, linked list styles are unlinked).
Variable bindings on list items follow their items when a list is reordered or shrinks:
paint colours bind per paint (`fills/<i>/color`, `strokes/<i>/color`; `fill`/`stroke` stay the
first paint's), so a moved bound paint keeps its variable and its neighbours are never recoloured.
`fill.remove`, `stroke.remove` and `grid.remove` refuse negative indices.
Layout grids get settings in the Design panel (size; count, type, width or height, margin or
offset, gutter; colour) whose numbers bind to number variables (`layoutGrids/<i>/<field>`),
re-applied on variable and mode changes. Instance boolean and text properties bind to boolean and
string variables (`componentProperties/<name>`, from the instance's properties panel), kept across
instance rebuilds, following property renames and dropped on delete, reset and detach.
Inspect names the variable of each paint.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
d16878588f |
M6: Dev Mode component code, annotations and ready for dev (#80)
* M6: Dev Mode component code, annotations and ready for dev - Component code: React (new), SwiftUI and Compose write components as reusable units whose component properties are parameters (variant unions/enums, boolean flags, text strings, instance swaps as views, slots as optional content); variant sets share one definition (React: data-* rules for the other variants); instances are calls passing only the values they change. Escaped, unique identifiers. `inspect.componentCode` returns the parts. - Annotations: notes with pinned properties on any layer, saved with the file (left out when empty), undoable `annotation.add/edit/remove`, `annotation.list` with live values; drawn as callouts on the canvas in the Inspect tab and edited in its Annotations section. Instances keep their own annotations. - Ready for dev: `inspect.readyForDev` lists frames and sections by handoff status; the Inspect tab sets the status and lists them with a filter. - Shared widgets: a multi-line field and the status dropdown are reused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * M6: test that bound variables become CSS custom properties Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * M6: regenerate the parity report Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
df2309f5b9 |
M1: measuring, scale tool handles and selection depth (#77)
Measuring (Alt/Option): distances from the selection to the hovered layer, its parent (over the selection itself or empty space) and ruler guides; Cmd+Alt for nested layers; containment both ways, overlaps between matching edges, gaps with dashed extension lines; rotated layers and several selected layers by their bounds; the measured box outlined. Alt while dragging shows live distances to the neighbours (or the parent's edges); Shift+Alt reads out an auto layout frame's padding and gaps. Labels are placed so they never overlap each other or the size label and stay the same size at every zoom. Commands: measure.distances, measure.padding. Scale tool (K) canvas handles: one factor about the Scale panel's anchor (tool.scaleAnchor, shared by the panel and the canvas; the opposite handle when the anchor is on the side being dragged), Alt from the centre, flips past the pivot, rotated layers and lines along their own axes, one undo step per drag, a live factor in the size label and the pivot marked. scale_node now also scales min/max sizes, layout grids, guides, fixed grid tracks, tiled images, paragraph indent and run letter spacing. Selection: Cmd/Ctrl-marquee selects nested layers, Shift-marquee toggles, the context menu's Select layer lists every layer under the click (select.layersAt), outlines are purple for every layer inside components and instances, hover outlines show with the Scale tool too. Fixed marquees selecting layers that only overlapped the marquee on one axis. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
50517a6431 |
M7: comments polish (#74)
Pins follow their layers (to other pages too) and stay where they were when the layer is deleted, re-pinned to the nearest surviving parent; your pins drag onto another frame; crowded pins cluster when zoomed out and a click zooms them apart; Shift+C shows or hides them. Threads: edit and delete your own messages (marked edited), emoji reactions, @mentions with a picker of the names used in the file (your name from Preferences), read/unread state per name, and Copy link (figcraft://comment/<id>; pasting it or comment.open goes to the page, zooms to the frame and opens the thread). Comments panel: search (text, names, frame), filters (open, resolved, yours, unread), sort by date or page, each thread's frame and page, Up/Down to step through threads. The presenter shows the presented frame's comments (Comments button or Shift+C) to read and reply. New commands: comment.react, comment.markRead, comment.open, comment.next, comment.zoom, comment.link, comment.people, pref.userName; comment.list gains filter/sort/query/frame, comment.move re-pins. Old files load unchanged (the first message stays on the comment; missing read state counts as read). Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
340b673666 |
M3: text depth: RTL names, text style browser, axis previews (#72)
Right-to-left and mixed-direction names in the panels: layers, pages, Assets, tab titles, the file header and canvas frame titles show in visual order (Unicode bidi, joined Arabic letters, mirrored brackets), elided at their end (the left end of a right-to-left name). egui shapes each font run itself, so shown names are laid out a cluster at a time. Rename fields keep egui's editing and draw the shown text over it: the caret, selection, clicks, drags and arrow keys follow what's on screen. System fonts are added for scripts the built-in UI fonts lack. The Typography section's text style browser: list and grid views with each style in its own font, search, folders, this file's and enabled libraries' styles, apply, detach, edit the typography, create from the selected chars, hover to preview on the canvas, and "Mixed" when the chars use several styles. Text styles now link chars (style_id on text styles), so a range can take a style; editing what a style sets unlinks the chars edited; files from before link on open. Live text previews (text.preview / text.endPreview): a hovered text style, font style or named instance and a dragged axis slider show on the canvas without an undo step; release commits one step, Escape cancels. ui.input sends raw window input over the control channel. Visual QA: list markers keep the paragraph's font, renames start with the name selected and centred, Max lines shows while truncating. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
6f9801790c |
M1: frame guides (#70)
Ruler guides can belong to a frame: `guide.add/move/remove/clear` take `frame` (as well as `page`), keeping the guide in the frame's own coordinates. Frame guides move with the frame, span only it on the canvas, are saved and copied with it, and snap the layers inside it. A guide dragged out of a ruler and dropped on a frame becomes that frame's guide; its label shows the offset in the frame. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
9ddf1b3a4e |
M5: responsive presentation, device colours and custom sizes, library modes (#66)
- Responsive presentation: `proto.settings {responsive}` (saved in the
page's prototype settings). The player resizes the presented frame to the
device's screen, or without a device to its whole area at 100%, and lays
it out again exactly as an editor resize does (constraints, auto layout,
text; an axis the frame hugs keeps hugging), live as the window, device or
file change. The player keeps its design document and lays the screen out
from it each time (`cmd::proto::respond`). In the presenter's Options menu
(Responsive beside the size options, or Responsive / Fixed size on a
device) and the Prototype panel.
- Device colours: each device model lists its colour variants
(`proto.settings {color}`), drawn in code as the body's band round black
glass; screen corners are masked exactly. Colour picker with swatches in
the Prototype panel.
- Custom size: a bare screen of any size (`proto.settings {width, height}`
picks it), W/H fields and a swap button in the Prototype panel, imported
from REST `CUSTOM` devices.
- Library collections in the Design panel's Variable modes section, under
their library's name (`library.setVariableMode`; copies whose library
isn't read stay listed).
- Flow descriptions in the presenter's flow picker (and its hover) and under
each flow in the Prototype panel.
- Tests in new modules: engine proto_responsive_tests, ui-egui
proto_responsive_tests; REST device test updated.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
044cfef834 |
M6: PDF export depth and REST conditional and variable-mode actions (#65)
PDF export
- Vector masks clip to the mask's outline, luminance masks are luminosity
soft masks (alpha masks as before).
- Background blur: the backdrop the page draws under the layer is rendered,
blurred and cut to its shape, under the layer's vector content.
- Text hyperlinks become clickable link annotations (one per line and link).
- Frames-to-PDF shares one path (`frames_pdf`): sRGB conversion for Display
P3 files, hidden frames left out, the file name as the PDF title.
- `figcraft-cli export -o x.pdf` and `convert x.figcraft x.pdf` write the
page's frames one PDF page each.
- Tests: mask types, backdrops, links, image deduplication, page sizes in
points, hostile inputs; CLI end to end. Checked by rendering the PDFs back
to PNG and diffing against the PNG export.
Prototyping
- Model: conditional actions (if / else if / else blocks), set variable mode,
and expressions (literals, variable references, + - * /, comparisons,
and/or/not, negate) for set-variable values and conditions; walk helpers
for nested actions (remap on paste, colours, destination checks, library
copies and pruning, canvas connections).
- REST import maps CONDITIONAL, SET_VARIABLE_MODE and SET_VARIABLE with
variable references and EXPRESSION values; unsupported functions (mode
lookups) skip the action with a warning.
- The presenter runs them in order (`figcraft_engine::proto_run`); the
Prototype panel summarises conditionals, edits set-variable-mode, and
`ui.set {protoEditor}` opens an interaction's editor.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
8dff0089e5 |
M4: vector edit transform box and point snapping (#64)
- Transform box around 2+ selected points (Move tool): corners/edges scale them (Shift keeps proportions, Alt from the centre), just outside a corner rotates them (Shift: 15° steps, the box turns with them), dragging inside moves them; one undo step each. Corners win over points, points over edges, edges over segments. - Commands: vector.edit.bounds, vector.edit.resize, vector.edit.rotate, vector.edit.transform; object.flipH/V and the 90°/180° rotations act on the selected points while editing (unless ids are given). - Design panel in edit mode: W/H for the selected points next to X/Y (the layer's dimensions are hidden while points are selected). - Point snapping for dragged points and handles and both pens: points and segment midpoints of this and other vectors, the usual layer targets and the pixel grid, with guides and a marker on the point landed on; Ctrl/Cmd turns object snapping off. Snap code generalised in snap.rs (Targets spans). - Fix: handle/bend drags read positions in the gesture snapshot's coordinates, so handles no longer lag when the preview re-fits the vector. - Shared resize math (tools::resized_box) and handle hit-testing for layer and point boxes; canvas draws both boxes through one helper. - Tests in vectoredit/transform_tests.rs (incl. junk sweep for the edit-aware commands). Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |