mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-26 11:26:50 -06:00
77b6f1fc12fd8bfa8498a859bee572bdb2c3ca3e
25
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7e467c2b82 |
M7: dev resources links
- Dev resources: links for developers (a URL and a name) on any layer, stored on the layer (`Node::dev_resources`, a new synced property `devResources` at the end of the property list), saved in the file and undoable.
- Commands: `devResource.add {id?, url, name?}`, `devResource.edit {id?, index, url?, name?}`, `devResource.remove {id?, index?}`, `devResource.list {id?}` (Spanish labels added).
- An instance lists its own links, then its main component's and that component's set's, read-only with `from`; layers inside an instance show their main layer's and take none of their own (refused with a hint). Instances don't copy the main's links (created empty, own links kept across main edits).
- URLs are data: http or https with a host, no whitespace or control characters, at most 2,048 characters; names clipped to 200; at most 32 per layer. Nothing is fetched: a link added without a name is named after its URL (scheme, `www.` and trailing slash dropped). The app opens a link only when it is clicked.
- Inspect tab: a Dev resources section (after Annotations) lists the links with a link icon, name and URL (or "From <main>"), opens on click, edits name and link inline (pencil), removes (minus) and adds from a "Paste a link" field (plus). New strings in the en and es catalogs; links are announced as links to screen readers.
- Agents: `get_node` / `inspect_document` return `devResources` (own and inherited); docs/mcp.md and docs/control-protocol.md describe them.
- The editor extension stays out of scope: the MCP server already serves design data, code and links to agents and editors.
|
||
|
|
bf82f7ebf9 |
M4: text lists, hanging punctuation and decoration styles
The tail of the text area, from the public documentation of the target app's text settings, with a table-driven behaviour suite (each case derived in one line; "silent" cases pin today's behaviour).
- **Lists per paragraph** (`TextData::paragraphs`, one `ParaList {list, indent}` per paragraph; empty when the layer has one list style at level 0, so old files load and save unchanged). The list buttons, Shift+Cmd+8 / Shift+Cmd+7, `text.listStyle` and Type settings act on the paragraphs the selection touches while editing (the whole layer otherwise). The per-paragraph list follows edits: a new paragraph copies the one it split from, joined paragraphs keep the first one's.
- **Nesting** up to 8 levels: Tab nests a list item, Shift+Tab moves it out; Enter on an empty item and Backspace at an item's start move it a level out, then out of the list. Typing `- `, `* ` or `1. ` at the start of a plain paragraph starts a list. Numbers count per level (1. a. i., then round again), bullets go • ◦ ▪; a plain paragraph, a shallower item or the other list kind starts the count again. Each level indents by one marker gutter; a nested bullet a font lacks comes from a fallback font.
- **List spacing** (space between two items of a list, in place of paragraph spacing), **hanging lists** (the outer gutter outside the box, item text lined up with plain paragraphs) and **hanging punctuation** (opening quotes and brackets outside the start edge of left-aligned and justified lines, closing punctuation outside the end edge of right-aligned and justified ones; hung punctuation takes no room when lines wrap).
- **Decoration look** per character (`TextStyle::decoration_look`, boxed, None by default): style (solid, dotted, wavy), thickness and underline offset (auto, px or % of the font size), skip ink (underlines break around descenders) and colour (its own style index in the layout, so canvas, PNG and PDF paint it without renderer changes). Decorations are drawn from `Decoration::path()` (bar, dots or wave) on the canvas, in PNG, PDF and outlined SVG.
- `node.set` keys: `listStyle` and `indentation` (range-aware with start/end), `listSpacing`, `hangingPunctuation`, `hangingList`, `textDecorationStyle`, `textDecorationThickness`, `textDecorationOffset`, `textDecorationSkipInk`, `textDecorationColor`; documented in `KEYS` and docs/mcp.md. `text.edit.key` documents Tab.
- Inspect: `listStyle`, or `lineTypes` and `lineIndentations` per paragraph; `listSpacing`, `hangingPunctuation`, `hangingList`, the decoration keys. CSS: `text-decoration-style`, `-thickness`, `-color`, `text-underline-offset`, `text-decoration-skip-ink: none` (browsers skip ink by default). Dev Mode rows: decoration style, list spacing.
- REST import: `lineTypes` with `lineIndentations` per paragraph (they collapsed to one list style for the layer before), `listSpacing`, `hangingPunctuation`, `hangingList`, the decoration style, thickness, offset, skip ink and colour; instance override names for them.
- Type settings ▸ Basics: decoration style, thickness and offset fields (with Auto), Skip ink, decoration colour; List spacing; Hanging punctuation and Hanging list checkboxes; the list control acts on the edited paragraphs. New strings in en/es.
- Components carry the new text fields in the characters and text-style override groups; scaling scales list spacing and px decoration lengths.
- `TextData::path` is boxed (`Option<Box<TextPath>>`) to keep `NodeKind` under clippy's enum size limit after the new fields.
|
||
|
|
2a54adb762 |
M4: shapes and vector behaviour suite and fixes
**A shapes and vector behaviour suite written from the public documentation, and fixes for every divergence it found.** Each documented rule (drawing with Shift and Alt, corner radius and which layers take one, point counts and star ratios, arcs, stroke joins and the miter angle, caps per end, boolean results and the look they take, flatten and outline stroke; entering and leaving vector edit mode, the pen, selecting points and segments, deleting vs. delete and heal, handle mirroring, the bend, paint bucket, cut, join and eraser tools, nudging and undo steps, what converting a shape keeps) is a table row or a step with what the rule implies and a one-line derivation; where the documentation is silent the row says so and pins today's behaviour.
**Fixes**
- **Rounded vector points were stored but never drawn**: a point's corner radius (edit mode's Corner radius field, `vector.edit.cornerRadius`) now rounds the corner between two straight segments on the canvas, in hit testing, booleans and every export (`VectorNetwork::rounded`, used by the fill and stroke paths); the radius shrinks to fit half of each segment, like a polygon's corners. A vector layer's own corner radius rounds every point. The points stay where they are in edit mode.
- **A boolean group's look follows its operation**: union, intersect and exclude take the top layer's fills, strokes and effects, subtract the bottom layer's (it was always the bottom one).
- **Alt draws a line or an arrow from its centre**, like the other shapes (it was ignored).
- **Stars keep to 3–60 points** (they went to 120; polygons keep 120, the documentation is silent); a junk `pointCount` is refused instead of becoming 5.
- **Delete and heal recomputes the curve**: the healed segment keeps its neighbours' tangents and passes through where the deleted point was (it reused the neighbours' handles, so healing over a circle cut deep inside it). Corners between straight segments heal straight, as before.
- **Shift-click on a selected point at a corner of the points' transform box takes it out of the selection** (it started a scale); a Shift-drag there still scales in proportion.
- **Outline stroke** turns the stroke's colour style into the outline's fill style, and the outline keeps the layer's opacity, blend, effects, constraints and visibility (they were lost).
- **The stroke panel's Miter angle shows degrees** (it showed the ratio 4 under that name; the default is ~28.96°); `node.set {miterAngle}` sets it and `get_node` reports a non-default join or angle.
- **Individual corners only for rectangles and frames**: polygons, stars and vectors have one radius (the toggle showed for them and set corners that weren't drawn).
- **Option/Alt+Shift+F flattens** (Cmd/Ctrl+E still does).
- **MCP `key` in vector edit mode reaches the edit tools first**, as the canvas does: tool letters, arrows, Delete / Shift+Delete, Cmd/Ctrl+J, Enter (Delete removed the whole layer; `X` was unknown).
**Missing documented behaviour, added**
- **Corner radius and star ratio handles on the canvas**: a handle inside each corner of a selected rectangle or frame, one inside a polygon's or star's top point, a ratio handle on a star's first inner point (`shape_handles.rs`). Dragging along a corner's bisector sets the radius of every corner (Alt: only that corner of a rectangle or frame, documentation silent), stopping where the corners meet; the ratio handle sets the inner radius. A readout shows the value while dragging; the handles hide under 48 px on screen, on locked layers, with several layers selected and in edit mode. One undo step per drag; `inspect_ui` lists them under `tool.shapeHandles`.
- **Count and Ratio fields** in the Design panel for polygons and stars (there was no way to change them in the UI).
- **Cut tool drags**: dragging across paths cuts them where the line crosses; parts that come apart move to layers of their own, each keeping its piece of the fill (`vector.edit.cut {from, to}`). A click still splits at a point or segment.
- **Paint bucket drags**: a drag fills or unfills every area it crosses the way the first one went (`vector.edit.paint {points}`), one undo step.
New strings go through `tr(...)` (the new fields reuse the existing Count / Ratio / Radius / Star / Polygon entries in `en.toml` and `es.toml`); the new fields have accessible names (an icon lead with its label). Each gesture and edit stays one undo step.
|
||
|
|
a2602071d7 |
M3: paint and effects behaviour suite and fixes
**A paint and effects behaviour suite written from the public documentation, and fixes for every divergence it found.** Each documented rule (fill stacking and opacity, paint vs layer opacity, every blend mode per paint and per layer, pass-through and Normal containers, the four gradient kinds and their handles, stops, image scale modes, rotation and adjustments, the colour models and hex entry, the document colour profile, drop and inner shadows, showing shadows behind transparent areas, layer and background blurs, effects on groups, effect order, what exports include) is a table-driven case with a one-line derivation; where the documentation is silent the case says so and pins today's behaviour. Scenes are small and synthetic; pixel cases use tolerances (blurs are checked against the normal distribution with sigma = radius / 2).
**Fixes (13 render cases and 8 command/export cases fail on the old code)**
- **Pass-through and Normal containers**: a group or frame set to Normal now blends its children among themselves first (it never isolated), and a **clipping frame no longer isolates** its children: they were drawn on a fresh layer, so a Multiply child inside a frame (frames clip by default) met neither the frame's fill nor what is behind it. The clipped layer now starts as a copy of what lies under it and goes back in its place through the clip (a memcpy for rectangular clips, so no slower); backdrop effects see the right composite, also for a clipping frame inside a faded group. Same in exports: SVG gives Normal groups `isolation:isolate` and puts a clipping frame's clip on what blends instead of a clipped `<g>` (a clipped group is a stacking context, which isolates); PDF wraps Normal groups in an isolated group (`Node::isolates_children`, shared).
- **Plus darker** (linear burn) is drawn by its formula on the canvas and in pixel exports for paints, layers, shadows and noise (it was multiply); tiny-skia lacks the mode, so `fx::plus_darker` composites it and `paint::composite` routes every layer, shadow and noise composite through it.
- **Shadows hidden behind the layer**: a drop shadow that doesn't show behind transparent areas is now hidden behind what the layer covers, translucent and 0% fills included (it was cut out by the layer's alpha, so it showed through a 50% fill anyway); groups and layers whose pixels a layer blur or texture moves still hide it behind their pixels. One `shadow::Caster::new` serves the canvas, PNG and the PDF's shadow rasters. **SVG** never cut shadows out: its filters now composite them `out` of an image of the layer's outline (or `SourceAlpha` for groups), and the filter region reaches as far as the effects (a fixed 200% box cut off a small layer's long shadow).
- **Fill images cover the box to its edges**: an upscaled Fill (or covering Crop) image faded to transparent over a source pixel at the box's edge.
- **"Normal" meant pass-through** when named: `BlendMode::from_name("normal")` found pass-through first (its CSS keyword is also `normal`), so `node.set {blend: "normal"}` and the blend menu's Normal set pass-through. Labels now win, and the menu sends labels.
**Added**
- A blend mode menu for each paint in the colour picker (no pass-through for paints), sharing `right::blend_menu` with the Design panel's layer menu; strings already translated.
- Inspect (`node.get`, MCP) reports a solid paint's `blend` when it isn't normal.
|
||
|
|
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. |
||
|
|
f665765128 |
M4: text behaviour suite: metrics, line height, CJK (#134)
## What **A text behaviour suite written from the public documentation, and fixes for every divergence it found.** Each documented rule (Auto / % / px line height and how the leading is split, letter spacing in % and px, paragraph spacing and indent, auto width / auto height / fixed / truncate with max lines, vertical trim, decorations from the font's metrics) is a table-driven case with the numbers the rule implies and a one-line derivation, read from the face actually used so it holds on any machine with a font. CJK line breaking follows UAX #14 and the CJK line-start / line-end rules; caret steps follow UAX #29 grapheme clusters. Where the documentation is silent the case says so and pins today's behaviour. **Fixes (6 of the 13 text cases fail on the old layout; the break and caret rules are new functions)** - **CJK line breaking**: lines break between ideographs, kana and Hangul and before or after a Latin word next to them (before: no break before an ideograph that follows Latin, none after `。`, full-width forms, CJK punctuation or Extension B ideographs), never inside a Latin word, with kinsoku: closing brackets and quotes, `、。,.!?`, small kana, the prolonged sound mark and iteration marks never start a line (the character before moves down with them), opening brackets never end one. The ideographic space hangs at the line's end like a space. One small break-class table in `crates/text/src/breaks.rs`, no new crate. - **Fallback fonts no longer move the baseline**: a style's ascent, descent, line height and cap height come from its own font, not the fallback face that drew some of its characters, so a line with CJK or emoji keeps the height and baseline of a Latin line (with a fixed line height the mixed line's baseline used to shift by the CJK face's taller metrics; with Auto the line grew). Decorations likewise use the style's font and size, so an underline runs at one height and weight through mixed scripts (and isn't thinned by synthesised small caps). - **Paragraph indent applies to left-aligned text only** (documented); right-to-left paragraphs keep it when right-aligned (their start). The Type settings field is disabled with a hint otherwise. - **Carets by grapheme cluster**: Left / Right, Delete and Backspace step over a whole emoji ZWJ sequence, flag, skin-tone sequence or letter with combining marks (they moved one code point at a time, leaving the caret inside an emoji and deleting half of it). A cluster is also shaped with one font, so a joiner or variation selector no longer splits an emoji sequence across faces. - **Words by script**: double-click and word moves (Alt on macOS, Ctrl on Windows and Linux) take one script's run in text without spaces (`日本語` | `の` | `テキスト`), Hangul words end at spaces, and a Latin word keeps its combining marks (before: a whole CJK sentence was one word, and `cafe` + combining acute split at the accent). **Missing documented setting, added** - **Vertical trim** (cap height to baseline): `leadingTrim: none | capHeight` (`CAP_HEIGHT` from REST JSON) through `node.set`, a Standard / cap-height segmented control in Type settings, `inspect_document` and Dev Mode properties, CSS `text-box: trim-both cap alphabetic` in generated code. The box runs from the first line's cap height to the last baseline (auto height shrinks, auto layout parents follow); truncation counts the trimmed bottom; exports reach past the box so descenders below the last baseline aren't clipped. Canvas, PNG, SVG and PDF all draw from the same layout. New strings go through `tr(...)` with `en.toml` / `es.toml` entries; the new segmented control reuses the Type settings one (accessible toggle names). Each edit is one undo step. `unicode-segmentation` (already in `Cargo.lock` through egui and winit) became a direct dependency of `figcraft-text`. ## Verified - **Desktop app** over the control channel (own target dir, separate `APPDATA`, stopped by PID): a 200 px auto-height box of Japanese wraps with no line starting with `、。」` and `「` never ending a line (`…テスト` / `を折り返す、テスト` / `です。「括弧」は行` / `末に来ません。漢` / `字とかなとカタカ` / `ナ。`, the last `ナ` carried down with `。`); a Latin / CJK / Latin paragraph at 36 px line height is 108 px with evenly spaced baselines; the same text with vertical trim is 89.45 px (cap height 17.45 + 2 × 36), its box running from the cap height to the last baseline, and the Type settings control turns it off (108 px) and undo brings it back in one step; one Right arrow from after `a` in `a👨👩👧b` lands after the whole family emoji and Shift+Right then selects `b`; a double click selects the katakana run テキスト. - **MCP**: `figcraft-cli mcp` (headless) built the same three texts; `inspect_document` reports 108 px for the 36 px paragraph and 89.42 px with `leadingTrim: capHeight` for the trimmed one (cap height of the headless font), the PNG export wraps the Japanese by the same rules and keeps the trimmed text's descenders, and `inspect.code` emits `text-box: trim-both cap alphabetic`. - **Web**: `cargo xtask wasm` passes (part of the gate). - **Performance**: text layout of a 2,250-char Latin and a 2,000-char Japanese paragraph wrapped at 300 px (release, median of 7 × 30 runs) before and after on the same machine: Latin 0.70 → 0.42–0.50 ms, Japanese 0.46 → 0.37–0.47 ms (within noise or faster: line metrics are now computed once per style change instead of per glyph). All-ASCII text skips grapheme segmentation. - `cargo +1.99.0 xtask ci` passes (fmt, clippy, tests, layers, assets, wasm), after merging #131 and #132. ## Tests - `text` (`behaviour_tests`, 13 tests): line height Auto / % / px; leading split above and below every line (positive and negative); letter spacing % / px / negative; paragraph spacing and indent by alignment; resizing and truncation with max lines; vertical trim (heights, first baseline, glyph spacing kept, truncation); decorations from the font's metrics through mixed scripts; fallback fonts keep line height and baseline (Auto, px, %); 22 break-opportunity cases; CJK wrapping with kinsoku, Latin words in CJK and Korean; the ideographic space hanging; grapheme steps over ZWJ families, flags, combining marks, skin tones and emoji presentation; one face per cluster. Font-dependent cases skip on a machine without fonts. - `engine` (`text_behaviour_tests`, 5): arrows, Shift+arrows, Delete and Backspace by cluster; double-click and word moves by script (and through `keymap::text_motion` for Ctrl and Alt); IME composition with CJK wrapping and growing the box before commit; vertical trim through `node.set` (box size, PNG ink at the top, export bounds below the box, SVG and PDF, CSS, inspect, undo, junk refused, REST spelling); paragraph indent only on left-aligned text. - `format` REST import test covers `leadingTrim: CAP_HEIGHT`. ## Not in this PR - Documented but not modelled yet: list spacing, hanging quotes and hanging lists, underline style / thickness / offset / skip ink. - Documentation silent, behaviour pinned: Korean wraps by syllable (no word mode); Han compounds next to each other are one word (no dictionary segmentation); letter spacing is added after the last character too; a fallback font's taller glyphs may overflow the line rather than grow it. - `.fig` import doesn't read vertical trim yet (the field wasn't checked against real files). - Spell check (on hold, #113). - Progress docs: target-app-parity area 10 85%/64% → 87%/68%, 7–13 h → 5–10 h with the suite listed under text editing depth; feature breadth and ready for real work unchanged at this rounding; full parity ~323–621 h → ~321–618 h (target-app-parity, ROADMAP); ui-parity text row. |
||
|
|
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. |
||
|
|
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). |
||
|
|
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. |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
0638c98564 |
M3: component slots and nested override edge cases (#56)
Slots: a frame of a main component becomes a slot property (component.makeSlot / removeSlot, preferred components, type "slot" in component.properties). Instances show the main's default content until the slot is edited; from then on its layers are the instance's own (override group "slot"): free to add, remove and reorder, kept across main updates, variant switches, copy/paste and detach, and instances placed there stand alone. instance.slotInsert fills a slot, instance.resetSlot brings the default content back. Structure protection allows edits inside slots only; drops from the Assets panel, canvas drags, drawing and paste land in slots. UI: slot icon in the layers panel, dashed outlines and labels on the canvas, slot property rows with a fill menu in the properties panel, Make/Remove slot on frames inside a main. Nested overrides: overrides inside a nested instance survive the main moving it into another frame (layers matched by source alone when unique); a nested instance copied out of an instance is re-linked to its own main with its look kept (values set by component properties aren't overrides); plain layers copied out of instances stop mirroring them; canvas drags can no longer pull layers out of an instance or reorder its auto layout children. Tests for swaps, variant switches, main restructuring, detach and copy/paste. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
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>
|
||
|
|
bf3a45d938 |
M1: sections and frames: resize to fit, section colours, ready-for-dev status (#23)
- Object ▸ Resize to fit (Alt+Shift+Cmd+R, `object.resizeToFit`): frames, sections and components shrink or grow to their visible contents without moving them on the page; auto layout frames switch to hug instead. - Sections get colour swatches (light tints) in the Design panel. - Frames and sections get a handoff status (`devStatus`: ready for dev / completed) set from the panel and shown as a badge next to their name on the canvas; `node.get` reports it. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
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>
|
||
|
|
b7910c81f8 |
M4: text depth: inline IME composition, variable font axes, range-aware typography polish (#8)
- Inline IME composition: egui Preedit events become a composition held in the text edit
session (`TextEdit.preedit`), shown inline at the caret with an underline and the IME cursor,
reflowing the text; the document only gets the committed text (`text.edit.insert`). Composing
over a selection replaces it; moving the caret or ending the edit drops an uncommitted
composition. The IME candidate window follows the cursor inside the composition.
New command `text.edit.preedit {text, cursor?}`; `text.edit.selection` reports it.
- Variable fonts: faces read their fvar axes (tag, name, range, default, hidden) and named
instances. Named instances are offered as styles and resolve in `find`; a style name with no
instance implies weight/italic/slant on the axes and optical size follows the font size.
Per-run custom axis values (`TextStyle.variations`, serde default, omitted when empty) are applied
in HarfRust shaping (ShaperInstance) and skrifa outlines/advances (cached per coordinates).
PDF export outlines non-default instances instead of embedding the default one.
- Commands/props: `fontVariations` / `variationSettings` props (whole layer or start..end),
`text.setVariations {variations, replace?}` (merges; edited range while editing),
`text.fonts {family, style?, axes: true}`; `node.get` and copy/paste properties carry the axes.
Picking a named style (Bold/Italic toggles, weight steps, fontStyle/fontWeight) drops custom
wght/ital/slnt values and keeps the others.
- Type settings ▸ Variable tab for variable fonts: a slider (default tick, double-click to reset)
and a field per visible axis, "Mixed" across differing styles, live drag as one undo step,
"Reset to style". Long style/family names stay on one line with an ellipsis and a tooltip.
- Tests: variable font shaping/outlines/clamping/instances (skipped without a variable font),
serde backward compatibility, preedit lifecycle and undo, variation commands/props/undo/junk;
junk sweeps extended with variation and preedit params.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
4286a3b74b |
M2: auto layout depth: min/max UI, ignore auto layout toggle, text baseline alignment, grid tracks and cells (#3)
- Min/max width/height: the W/H sizing menus add min/max (at the current size) and remove them;
limit rows edit or add the missing bound. The layout clamps Fixed, Hug and Fill children and
the frame itself; a hugging text wider than its max width wraps at the max. min > max is kept
ordered by node.set (the other bound follows); in the layout min wins.
- Ignore auto layout (absolute position): a toggle in the Position section; the child stays
where it is, can't Fill, shows constraints and follows them when the auto layout frame resizes
(by hugging, filling or a direct resize).
- Text baseline alignment for horizontal layouts ("Align text baselines"; crossAlign baseline):
first-line baselines line up, other layers align to the top, nested auto layout frames use
their first child's baseline. Measure gains text_baseline.
- Grid auto layout: column/row tracks (fixed px, hug, fr fill) via columnTracks / rowTracks or
layout.setTrack, cells anchored by gridColumn / gridRow with gridColumnSpan / gridRowSpan,
auto placement in reading order, rows grow to fit, hugging grids size to their tracks.
Panel: column/row gap, track size rows, a child's cell and spans. Old files load unchanged
(serde defaults; missing tracks are 1fr).
- Canvas handles: wrapped-row gaps and grid column/row gaps are draggable; a selected grid's
tracks are outlined.
- The layout section moved from right.rs to layout_panel.rs; inspect reports min/max and a
grid child's resolved gridCell; docs list the new props.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |