Commit Graph
158 Commits
Author SHA1 Message Date
BFlat 4a0bd79797 Merge pull request #1488 from ChristopherLinnett/issue/1086-image-validation
fix(collab): image entries must match their hash and sane bounds (#1086)
2026-10-29 09:49:20 +07:00
BFlat baaafc381b Merge pull request #1485 from StrajnarFilip/claude/fix-1476-guides-reopen
M1: page ruler guides survive save and reopen (#1476)
2026-10-29 09:29:31 +07:00
Christopher Linnett 949d74f7ca fix(collab): image checks keep documents shareable and history replayable (#1086)
- The entry size cap is figcraft_doc::ImageData::MAX_BYTES (256 MiB), the
  cap the .fig, .sketch and REST importers already read images with (now
  one constant), instead of 32 MiB: an imported image larger than 32 MiB
  made the document unjoinable and its server snapshot unrestorable.
- History written before the checks (a restored or joined snapshot, a
  replayed server log, a file published to a server) applies without the
  image entries they refuse (docprops::without_refused_images), instead of
  the file not opening or a restart dropping every edit after the image.
- A key that already holds an image can't be given other bytes: producers
  key images by their bytes (the REST importer's placeholder keys always
  hold the same picture), and merge treats a key's image as never
  changing.
- REST import: an unresolved imageRef shaped like a content hash shares
  the rest-image-missing placeholder (the placeholder's bytes don't hash to
  it); ImageData::claims_content_hash is shared with collab.
- Tests: collab image_tests (4 new, size test uses a small cap), format
  image_tests (1 new).
- Docs: collab-protocol.md, collaboration-parity.md, file-format-parity.md.
2026-10-29 08:55:03 +08:00
Christopher Linnett f1c2ee81e4 fix(collab): image entries must match their hash and sane bounds (#1086)
Image entries applied bytes to any key with no checks (audit COL-9): a
collaborator could poison the shared content-addressed image store, claim
absurd dimensions, balloon memory, or name an image `../../x`, which a
saved file then wrote as the archive member `images/../../x`.

- Every key must be 1–128 ASCII letters, digits, `-` or `_`:
  `figcraft_doc::ImageData::is_valid_key`, promoted from the format
  crate's private `is_valid_hash` so the file reader and collab share one
  rule. The reader now also takes `-` and `_`, so the REST importer's
  `rest-image-missing` placeholder survives a save and load.
- The REST importer's side of the same attack: an unresolved `imageRef`
  that can't legitimately be an archive member name (path text, an
  over-long ref) shares the `rest-image-missing` placeholder instead of
  being keyed by its raw name
  (`image_tests::a_path_like_rest_image_ref_never_names_an_archive_member`).
- A key with the shape of a content hash (64 hex chars) must be exactly
  the lowercase sha256 of the bytes, the form `image_hash` writes; an
  uppercase look-alike is refused. Other keys (svg FNV-128, REST
  placeholders) claim no content address and only meet the rules above.
- A side is at most 65 535 px; decoded bytes at most 32 MiB, with the
  base64 text capped before decoding. 0×0 stays valid: the format crate
  stores it for sizes it can't probe.
- Refusals are Error::Invalid and reject their batch, so every replica's
  store stays clean; catch-up snapshots apply through the same check.
- Docs: collab-protocol.md, collaboration-parity.md and
  file-format-parity.md (rows, status lines, histories).
- Tests: collab image_tests (8), format image_tests: round-trip and the
  REST placeholder gate.
2026-10-29 08:11:13 +08:00
Filip StrajnarandClaude Opus 5.5 652bf35b19 M1: page ruler guides survive save and reopen (#1476)
A page kept its ruler guides in `NodeKind::Page`, and since frame guides
(#70) every layer also has its own `guides` field. Both are written as
`guides`, and on reading the layer's own field takes the key, so:

- a reopened page's guides landed in the layer field and were not drawn;
- saving that page wrote `guides` twice (the page kind's list, always
  written, then the layer's), and the file no longer opened ("duplicate
  field `guides`").

Every layer, pages included, now keeps its guides in the one layer field;
the key in the file is unchanged, so existing files open with their guides.
The field reads every `guides` list a layer has, so files already saved
with the key twice open again with both lists, in file order. This also
covers collab `Create` payloads from older peers.

Regressions: JSON round trips of page and frame guides, a page with two
lists, a bad list still refused (doc); save, reopen, save, reopen through
`.figcraft` and a document.json with the key twice (format). Checked in the
app over the control channel: guides drawn after two save/reopen cycles, and
a file with the key twice opens with all its guides.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LvG6Dyzof21BCrUsByhx2w
2026-10-28 22:47:24 +01:00
Filip StrajnarandClaude Opus 5.5 d5ce10f0be M8: repair and library manifests check membership by set lookups (#1078)
Repair re-listed unlisted pages with a scan of the page list per page, and
reading a published library kept manifest styles, collections and variables
with a scan of the snapshot's lists per entry: a file with many pages or a
long manifest made both quadratic. Both now look ids up in a set built once.
validate() already indexed its parent/child edges and page list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LvG6E2qDnjtMzfPVuEP7iH
2026-10-28 22:10:28 +01:00
Dylan M. Taylor 8591d44909 refactor(doc): declare node property dependencies 2026-10-28 16:00:36 +00:00
BFlat cac54fd552 Merge pull request #840 from StrajnarFilip/feat/vector-drawable-export
M6: export as Android Vector Drawable (XML)
2026-10-27 01:06:21 +07:00
BFlat 29c89f7d5b Merge pull request #650 from JimKnoxx/overlay-anchor-trigger
M5: overlay positions relative to the layer that opens them
2026-10-27 00:49:52 +07:00
bflatastic 2aae395310 Merge main into #840 2026-10-27 00:31:32 +07:00
BFlat adea3a5550 Merge pull request #1053 from Silvertree2010/fix/h1274-title-case-leaves-a-word-lowerca
Capitalise title-case word after an opening apostrophe
2026-10-27 00:13:01 +07:00
bflatastic 632f2cef4b Merge main into #840
# Conflicts:
#	docs/roadmap.md
2026-10-27 00:12:27 +07:00
bflatastic bf9783f30f #650: mark relative overlay positions as an extra beyond the reference tool 2026-10-26 23:24:36 +07:00
bflatastic de826e14cf Merge main into #650
# Conflicts:
#	crates/ui-egui/locales/es.toml
#	crates/ui-egui/locales/pl.toml
#	docs/prototyping-parity.md
2026-10-26 23:24:22 +07:00
bflatastic e469cc8135 Merge main into #840
# Conflicts:
#	apps/figcraft-cli/src/files.rs
#	apps/figcraft-cli/src/main.rs
#	crates/engine/src/cmd/file.rs
#	crates/engine/src/lib.rs
#	crates/ui-egui/locales/en.toml
#	crates/ui-egui/locales/es.toml
#	crates/ui-egui/src/export_panel.rs
#	crates/ui-egui/src/export_preview_tests.rs
#	docs/file-format-parity.md
#	docs/mcp.md
#	docs/roadmap.md
#	docs/ui-parity.md
2026-10-26 23:23:07 +07:00
bflatastic e6e4e9ab8a Merge main into #1053 2026-10-26 22:57:25 +07:00
Muamer MustaficandCursor b6d3e002df M4: truncated text fills its last line, keeps truncating when resized, and auto height takes max lines (#1375, #1376, #1377)
- The last line shown runs on into the word wrapped after it, so the ellipsis
  fills the line ("Lorem consec…") instead of following the last whole word.
- An explicit height (node.set, transform.resize, a resize handle, fixed
  sizing) on truncating text keeps it truncating (TextData::fix_size).
- Max lines on auto-height text hugs up to that many lines and ends in an
  ellipsis; text.maxLines keeps auto height, Type settings show it as
  truncating, .fig export writes textTruncation and import keeps auto height.
- node.get reports maxLines; text.fit measures every line.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-26 13:14:51 +01:00
BFlat d3782fdc43 M12: board templates and tables (#1363)
* M12: board templates and tables

Five built-in board templates (retrospective, brainstorm, weekly planner,
flow chart, kanban board) placed as ordinary board objects in one undo step
(board.template, board.templates) from a toolbar picker with pictures, and
board tables made of auto layout frames (board.table, board.tableEdit; up to
50 x 20), typed into with a double-click and edited by row and column from
the bar over the selection. Docs: boards-parity, gap 24, ROADMAP, mcp.

* M12: board templates picker on egui 0.37

Id::new and Ui::id are deprecated in egui 0.37: the templates picker's area uses Id::unique, its buttons' interaction ids come from ui.scope_id(), and its test reads the board bar's area by Id::unique.
2026-10-26 12:52:39 +07:00
BFlat 4fad29a66e M12: board timer and voting sessions (#1361)
* M12: board timer and voting

A countdown everyone on a board sees (board.timer, board.timerState), synced as its end time or the
time left while paused, and voting sessions (board.vote, board.voteResults): votes per person, on
everything, stickies or the selection; clicks vote, your badge takes a vote back; your own votes
show while it runs, everyone's with a ranked list once it ends. Timer and vote sync as document
properties and ballots as one entry per voter; neither is an undo step. A pill at the top right
of the board, new Timer, Pause and Vote icons, labels and strings in every catalog, docs.

* M12: board timer and voting on egui 0.37

Id::new is deprecated in egui 0.37: the timer and voting area and its memory ids use Id::unique.

* M12: board.vote's start and ballot changes in functions of their own (Sonar)
2026-10-26 12:28:59 +07:00
BFlat 26291aff2c M12: boards dot grid, re-attaching connector ends, curved connectors (#1249)
* M12: boards dot grid, re-attaching connector ends, curved connectors

Board pages draw a dot grid in the canvas render, after the page colour
and under the layers (figcraft_render::Dots), thinned out when zoomed out
and coloured from the theme's ink against the page. A selected connector
shows its ends as handles; dragging one onto an object re-attaches it,
onto empty canvas leaves it free (board.reattach, one undo step, also
over MCP). Connectors can be curved, and elbows go around the objects
they join when the short way would cross them.

* M12: boards dot grid: blending a dot in a function of its own (Sonar)
2026-10-26 11:52:16 +07:00
BFlat 8b470eceaa Merge pull request #1275 from Ridzzz0Alam/fix/1062-render-bounds-depth
fix: Issue (#1062) render bounds stop at the depth the renderer draws.
2026-10-26 10:25:45 +07:00
bflatastic a35dbf184f Merge main into #1217 2026-10-26 09:19:49 +07:00
bflatastic 2fd59da54c Merge main into #1275
# Conflicts:
#	crates/doc/src/tests.rs
2026-10-26 09:15:49 +07:00
Brandon Thomas 8cbdff697f Merge remote-tracking branch 'origin/main' into fix-1295
# Conflicts:
#	crates/doc/src/tests.rs
2026-10-25 11:56:28 -04:00
aiongg d31a43419d Merge #1298
# Conflicts:
#	docs/ui-parity.md
2026-10-25 02:21:19 +00:00
aiongg c104757f59 Merge #1286 2026-10-25 02:21:08 +00:00
Dylan M. TaylorandClaude Sonnet 5.5 9f78875338 security: linear vector import, stroking and rounding, validation, and cyclic dev snapshots (#1306)
* 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>
2026-10-24 21:17:36 -04:00
dotCooCoo d8900e1c16 M4: the Text on a path tool
Text went onto a path only with the Design panel's Put text on path
button (a text layer and a path selected together) or with text.onPath.
The Text on a path tool, in the Text flyout of the toolbar
(tool.textOnPath), shows the I-beam over a shape's or vector's path, and
a click there puts new, empty text on the path, starting at the click,
and starts typing. The text takes the path layer's place, transform,
box, constraints and effects, and its fills when one of them shows; the
path layer is removed. Ending the edit with nothing typed puts the path
back with no undo step.

The tool measures the distance on the page, so a layer scaled more one
way than the other, or shown smaller, is reached where it shows. Shapes
in front whose outline is away from the click do not hide a path behind
them, and paths that a mask or a clipping frame hides at the click are
skipped, as are locked layers and layers inside instances.

A double-click into a text layer that was just made no longer makes it
count as an existing layer, so leaving it empty records no undo step,
with this tool and with the Text tool.
2026-10-24 11:37:47 -07:00
NetterSilverandClaude Haiku 5.5 544a29c93b Keep style runs disjoint when inserting text at a run boundary
Found while testing crates/doc/src/text.rs:592 (TextData::replace_range, with normalize at :573)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 17:17:22 +00:00
dotCooCoo 811d599960 M4: corner smoothing on polygons and stars
Polygon and star outlines did not use their corner smoothing: the value
was stored, imported and exported, but `shape_path` drew their corners
as plain circular arcs, and the Design panel showed the Corner
smoothing field only for rectangles and frames. Rounded polygon and
star corners now take smoothing the way rectangle corners do. Each
corner keeps a circular arc over the unsmoothed part of its turn and
eases into both edges, and it starts (1 + smoothing) times the plain
corner's length from the vertex. When a smoothed corner does not fit
in half of its shorter edge, smoothing is reduced first, then the
radius, as for rectangles. The Individual corners button opens the
panel for polygons and stars too, with the Corner smoothing row and no
field per corner.
2026-10-24 07:22:36 -07:00
Ridzzz0Alam 4da6a11bad M1: render bounds stop at the depth the renderer draws (#1062)
world_render_bounds and children_render_bounds recursed once per level
with no limit, so a crafted chain of ~20,000 nested groups overflowed the
stack and aborted the app on PNG/JPG/WebP export, rasterize, copy as PNG
and thumbnails. A stack overflow is an abort, not a panic, so the guard
can't catch it.

The walk now carries a depth and stops descending at 256, the depth
draw_layer draws to (MAX_EXPORT_DEPTH): bounds match what is drawn and
shallower trees are unchanged.

Test: render_bounds_stop_where_drawing_stops_on_very_deep_chains (aborts
on the old code).
2026-10-24 11:26:27 +02:00
NetterSilverandClaude Haiku 5.5 3a1ddf339c Convert text decoration colour in colour profile conversion (#1247)
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>
2026-10-24 05:22:48 -04:00
Dylan M. TaylorandClaude Sonnet 5.5 fdc4f74034 chore: drop redundant 'static lifetimes in consts (Sonar S8863) (#1254)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 04:28:06 -04:00
JimKnoxxandClaude Opus 5.5 76804506dc M5: an open overlay follows the layer that opened it when the view changes
Review follow-up. The overlay kept the screen rect its trigger had when it
opened, so after a resize, a size option change or a pan a trigger-relative
(or manual) overlay stayed where the trigger used to be. It now keeps that
rect in page units of the presented frame and places it again on every
layout. The dropdown test switches to Fill while the menu is open. The
anchor's is_screen gets a doc comment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 09:19:33 +02:00
JimKnoxxandClaude Opus 5.5 545cd630fd M5: overlay positions relative to the layer that opens them
From the Figma forum request "Set overlay position relative to element,
not to page": an open overlay action takes anchor: screen (default) |
triggerInside | triggerOutside. With a trigger anchor the position preset
is measured on the layer that opens the overlay instead of the screen.
Outside puts top presets above it and bottom presets below it with the
edges lined up (a dropdown is Bottom left + Outside); Center centres it
on that layer. Manual is unchanged.

The interaction editor gets Relative to (Whole screen / Trigger layer)
and Inside / Outside. The player, the command (and so MCP and the
control channel) carry it; the command's params doc lists it. Figma has
no such setting, so .fig export writes it as a manual offset. Old files
and JSON without anchor read as screen and write unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 09:19:33 +02:00
BFlat e3e39bbc4d M6: Dev Mode measurements kept on the canvas, a read-only layers panel (#1248)
* 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)
2026-10-24 13:33:26 +07:00
bflatastic 565e0e8266 Merge main into #1217 2026-10-24 12:54:49 +07:00
BFlat 3476f4b1e7 Merge pull request #1244 from storytold/batch/r2-1
Community batch r2-1: 63 reviewed PRs
2026-10-24 11:15:53 +07:00
NetterSilverandClaude Sonnet 5.5 8b94da604a fix: use apostrophe-aware title case on the canvas text path
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 05:52:05 +02:00
bflatastic 46b02d44e6 Batch r2-1 review: paste_nodes refuses two nodes mapped to one new id 2026-10-24 10:31:17 +07:00
a1a842af9a chore: resolve SonarCloud maintainability issues (#1241)
* 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>
2026-10-23 23:28:13 -04:00
bflatastic 771a0060b6 Merge #1038: Keep literal true/false distinct from variables named true/false (@Silvertree2010)
# Conflicts:
#	crates/doc/src/proto_expr_parse_tests.rs
2026-10-24 09:23:15 +07:00
bflatastic 60e1ef9976 Merge #1041: Keep not <var> from reading back as a variable named 'not …' (@Silvertree2010) 2026-10-24 09:23:02 +07:00
bflatastic ef6a6c49b2 Merge #1185: Make is_ancestor fail closed on deep or looping parent chains (@Silvertree2010)
# Conflicts:
#	crates/doc/src/tests.rs
2026-10-24 09:21:37 +07:00
bflatastic 82004c6e9c Merge #1187: Cap the issue list returned by Document::validate (@Silvertree2010) 2026-10-24 09:21:24 +07:00
bflatastic b5a8dcb059 Merge #1230: Honour even-odd fill rule when hit testing vectors (@Silvertree2010)
# Conflicts:
#	crates/doc/src/tests.rs
2026-10-24 09:19:18 +07:00
bflatastic 00074d0041 Merge #1186: Stop stale next_id from making pastes overwrite existing nodes (@Silvertree2010) 2026-10-24 09:18:16 +07:00
BFlat 8167f13a6a M12: Boards: a collaborative whiteboard file kind (#1229)
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).
2026-10-24 04:18:55 +07:00
NetterSilverandClaude Haiku 5.5 068634b586 Honour even-odd fill rule when hit testing vectors
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>
2026-10-23 21:53:25 +02:00
Salah1StandClaude Opus 5.5 c4be9c1a34 M7: say what a default font family in files does and doesn't promise
Review: a saved family renders only where that family is available. The
docs and the test's comment now say the variable changes only what new
text starts in, and that a file keeps its fonts in a build with another
default, falling back like any missing family elsewhere.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 17:34:43 +07:00