* security: linear vector import, stroking and rounding, validation and a cycle check for dev snapshots
Closing a subpath, rounding corners and chaining stroke runs each rescanned the whole
network per step, and Document::validate searched a parent's children per child. They now
update only the current subpath's last segment, rebuild each region loop in one pass, walk
per-vertex adjacency lists, and check edges against a once-built index. A dev snapshot whose
layers contain a cycle under the root is refused instead of reconstructed.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* fix(doc): address review findings for snapshot children and 2-segment rounded loops
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Found while testing crates/doc/src/colors.rs:30 (TextStyle::for_each_color_mut, used by TextData, Style and Document::map_colors / set_color_profile)
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
* M6: Dev Mode measurements kept on the canvas, a read-only layers panel
Measurements placed between two layers, along a side or to the layer
around one (devMode.addMeasurement / removeMeasurement / measurements),
stored on the layer they start from (saved, synced, undoable; old files
open without them), drawn in Dev Mode as labelled redlines that follow
the layers and listed, placed and removed in the Inspect tab. In Dev Mode
the layers panel only selects and expands.
* M6: Dev Mode measurements: explicit imports, a measurement row of its own (Sonar)
* chore: resolve SonarCloud maintainability issues
- Define SHA256_FMT constant in packaging script to remove duplicated string literal (shelldre:S1192)
- Remove redundant 'static lifetime annotations from const items (rust:S8863)
- Replace closures with method and function pointers (rust:S1612)
- Extract helper functions in synthetic document generation, bezier polyline flattening, and component restoration to reduce cognitive complexity (rust:S3776)
* fix(packaging): use verify_sha256 helper function to avoid shellcheck SC2059
* fix(engine): remove duplicate closing brace in large_document
* fix(engine): take &mut Document in restore_id_map; keep 'static on associated consts
restore_id_map allocates ids, so it needs a mutable document. Elided
lifetimes in associated consts trip elided_lifetimes_in_associated_constant,
so those annotations are restored.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Assign positional parameters to locals in sha256 verification
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Dylan Taylor <dylan@craftmaker.dev>
Board files (File > New board) with sticky notes, shapes with text, connectors that follow what they join (straight or elbow, arrowheads, labels), marker, highlighter and eraser, and stamps; a board toolbar and a contextual bar; the Design panel hidden on boards, Prototype and Dev Mode refused there. Boards are ordinary FigCraft files built from ordinary layers, so saving, undo, export, MCP and live sessions work unchanged. File tabs, recent files and Open files show a pen nib for design files and a note for boards (original icons drawn in code).
Found while testing crates/doc/src/geometry.rs:75 (hits_node_itself, the `_` arm, using path.winding(lp) != 0); compare VectorNetwork::fill_contains at crates/doc/src/vector.rs:321
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Found while testing crates/doc/src/proto_expr.rs:172 (display writes `not {x}` with a bare var_ref) with crates/doc/src/proto_expr_parse.rs:262 (not() checks variable names before the `not` keyword)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found while testing crates/doc/src/proto_expr_parse.rs:462 (variable lookup before the true/false literals at :466) with crates/doc/src/proto_expr.rs:151/186 (display writes the literal as bare `true` and var_ref writes such a variable bare)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An instance copied its main's variable modes when it was made and kept
the copy as modes of its own. Switching it to another variant kept the
old variant's modes, and a mode set on the main later never reached it.
Instances now take the modes their main sets through the link
(`Document::inherited_modes`), closer than any frame around them. Modes
set on the instance itself still win and stay through variant switches.
Bindings are re-applied for the instances of a main that chose another
mode and for an instance that switched mains. Detaching keeps the modes
the instance took from its main, and the Variable modes panel shows them
as the instance's Auto value.
Instances made before this change keep their copy as modes of their
own; setting one back to Auto lets it follow its main.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A 1 px Line or stroke-only path only counted as hit within 2 screen
pixels, so a double-click slightly off it cleared the selection and
never entered vector edit mode, and Cmd/Ctrl could not bend it.
Strokes with nothing filled now reach 5 screen pixels; filled shapes
keep the plain tolerance.
The .fig export now writes a numbered list's start on each of its lines,
so a reader that takes it per line or per list counts the same. Bullets
no longer keep a start. The field is named start_offset (it holds the
offset: 1 shows "2."). get_node reports listStartOffsets. The parity doc
lists what's still not read from .fig lists.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Figma stores where a numbered list starts counting on each text line
(listStartOffset; a list shown as "2." has offset 1). The import dropped
it, so every list counted from 1.
A paragraph's list settings now carry a start number (written only when
set, so older files open and save unchanged). Numbering counts on from
it where a list starts; items that go on counting ignore it. The .fig
import reads it, the .fig export writes it on a list's first line, and
plain paragraphs drop it. Toggling a list off ignores differing starts.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Node lookups in the document's B-tree were still a third of a render on a
900 000-layer file: a small subtree was walked for its bounds, again to
see whether it could be splatted, and again to splat it, and each child
of a drawn layer was looked up by paint_runs, by the mask check and by
draw_node.
- The render's table also keeps the layers it looked up, keyed with a
multiply hash instead of SipHash (ids are counters), and so do the
bounds it keeps.
- draw_children looks each child up once, works out the paint runs from
those (Document::paint_runs_of) and draws the child it has.
- Whether a layer can be splatted is kept like its bounds: layers with
children only, answers the nesting cap decided aren't kept, and kept
answers are read only where their levels fit under the cap.
Ctx gets a second lifetime, for the document the kept layers come from.
Single-threaded cycles per 1440x900 render fitted to the page: 0.39 ->
0.34 G on a page of 246 204 layers, 0.26 -> 0.22 G on one of 118 358.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YQxjkNSUJhTkTfY74aNeki
Culling a layer asked for its paint bounds, which walk its whole subtree,
and then did the same for each child: every layer's subtree was walked
again at each level above it, and once more in each parallel band. The
bounds worked out during a render are now kept for it (layers with
children only; bounds the 64-level cap cut short are not kept, and kept
ones are read only where their levels still fit under the cap, so culling
is exactly as before).
Every render also looked through every layer of the document for the
largest background blur, to size the overlap of its bands: 900 000 layers
on one large file, on the main thread before the bands start. The
document now works it out when first asked after a change, like whether
it has masks.
A 1440x900 render on a 900 000-layer file, page of 118 358 layers: at
100% 13.0 -> 0.6 ms, at 25% 16.5 -> 3.4 ms, fitted to the page 56 -> 31 ms;
page of 246 204 layers: 12.6 -> 2.5 ms, 12.1 -> 2.0 ms, 69 -> 62 ms.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YQxjkNSUJhTkTfY74aNeki
page.duplicate copies every layer with duplicate_all_into (navigation between
copied frames points at the copies) and builds the old-to-new list for flows
from that copy, instead of copying each layer separately.
StyleRun start/end are usize taken straight from a .figcraft document.json
(nothing clamps them on load; only the REST and SVG importers normalize),
so TextData::replace_range's p - removed + inserted panics in a debug or
test build and wraps to zero in release, where normalize then drops the
run and the layer silently loses its formatting.
Shift the runs with saturating arithmetic; normalize drops whatever is
left. Regression test: text_runs_past_the_text_never_overflow — panics
at crates/doc/src/text.rs:605 on main, passes with the clamp.