207 Commits
Author SHA1 Message Date
BFlat 12e9b409ed M12: a connector end on any part of an object with Cmd/Ctrl (#1733) 2026-10-31 10:27:56 +07:00
BFlat 6b28ef57dc M4: Dash cap setting for Dash and Custom strokes (#1728) 2026-10-31 09:41:39 +07:00
BFlat 57a51af8bb M5: the Controller menu names each button as PlayStation and Switch controllers do (#1726) 2026-10-31 08:41:40 +07:00
BFlat b462a48346 M12: a connector's path can be reshaped by dragging its path handles (#1710) (#1717) 2026-10-31 04:53:36 +07:00
BFlat 4bcfd5247f M12: boards have 16 more shapes, in grid pickers (#1705)
* M12: boards have 16 more shapes, in grid pickers

* M12: a board shape's id and name sit in one table
2026-10-31 04:27:12 +07:00
BFlat 2d784c363d M3: library analytics count insertions and show them over time (#1704)
Gap 10: a file counts the library instances it places, creates, pastes or duplicates and reports them like its detaches. The server counts what a file's totals grow past their highest in the current week and keeps a year of weeks (actions.json). library.usage {days} and Show usage give insertions and detachments over the past 30 days, 90 days or year. New variants are appended; an older server refuses them and Show usage says so.
2026-10-31 04:13:14 +07:00
BFlat 760aece238 M5: an Open link action can open in place of the prototype, with Open in new tab off (#1685) 2026-10-31 02:37:54 +07:00
BFlat 9f4d1ce75b M12: board votes say who voted for what, and earlier votes' results are kept (#1666)
* M12: an ended vote's results say who voted for each object

* M12: a board keeps earlier votes' results, listed in Results and deleted one at a time

* M12: the vote Results popover is drawn by small helpers, and its stepper, rows and Clear are tested
2026-10-31 01:19:03 +07:00
BFlat e97961ba66 M12: stamps stick to the objects they land on, which are highlighted first (#1671)
* M12: stamps stick to the objects they land on

A stamp sticks to the topmost object under its centre (BoardItem::Stamp gains an optional on, left out when empty so older files open with stamps on nothing) and the board's normalize pass carries it when that object moves or turns, once when both move together. A stamp dragged onto another object sticks to that one, off everything to nothing; one whose object is deleted stays where it is. Gap 24.

* M12: the object a stamp will stick to is highlighted

While the stamp tool is over an object, or a stamp is dragged over one, the object it will stick to is outlined with the canvas's hover outline (Overlay::stamp_target, found by the same stamp_target as board.stamp and settle_stamps). A drag no longer drops what it moves into the auto layout of a sticky, shape with text or table: board objects take no drops; a stamp already inside its sticky moves with it once. Builds on #1671. Gap 24.
2026-10-31 00:25:38 +07:00
BFlat be6a7af25f M3: team library analytics count detached instances (#1668)
* M3: team library analytics count detached instances

Detaching an instance of a library component counts it in the file (libraries.detached, saved with it, capped). The background library check reports the counts after its fetch (ReportDetached); the server keeps them per file beside the file's usage report in usage.json (same caps) and answers Detached with them. Show usage and library.usage list detaches per component and per file (detached, detachesKnown). The new request and reply variants are appended, so every older library message keeps its bytes; an older server refuses the new requests, which the app ignores. Gap 10.

* M3: the detach test waits for the instance count too (the two arrive in separate reports)
2026-10-30 23:29:18 +07:00
BFlat e4a7e89f39 M3: typed and stored variable, collection and mode ids find exactly that object (#1613) (#1635)
Builds on #1618. Variables::get and Variables::collection take an exact id before a name; stored mode ids go through Collection::mode_by_id (expressions, the prototype's mode switch); font family variables and the colour picker, prototype editor and panels read stored ids exactly. Ported from fix/stored-ids-exact.
2026-10-30 18:00:34 +07:00
BFlat 9099f1d5ae M6: .fig elbowed and curved connectors without a saved path are routed, not drawn straight (#1649)
The board connector router moves, unchanged, from the engine into figcraft-doc (route_points, RouteEnd) so the .fig importer can use it: an elbowed or curved connector saved without its path runs in right angles or one curve from the side of each layer it joins (the file's magnet, else the edge its point is on, else the side facing the other end). Straight connectors are unchanged. Gap 1.
2026-10-30 17:11:22 +07:00
Christopher Linnett 8d5bad2afc M3: CSS export never names a calculation it leaves out either (#1280)
The CSS export left out a colour calculation that resolves to nothing but still wrote an alias to
it as var(--name); where no block defines that property, whatever reads the alias is invalid.
CSS (and Tailwind) and Penpot now share one rule, Variables::flattener: an alias that leads to a
calculation some mode of its collection leaves out goes out as the value it shows in the mode,
or is left out when it shows nothing.
2026-10-30 16:59:19 +08:00
bflatastic 68450a6a0e Merge main into #1641 2026-10-30 15:57:31 +07:00
BFlat c1451fecd5 M4: an imported file finds an installed family under the other names its fonts give it (#1292) (#1628)
Fonts name a family by its typographic name and by a legacy name per weight; fonts without typographic names are installed under the legacy ones only. FontDb::resolve finds either from the other, and matches names across case, spaces, hyphens and underscores; every lookup (import, paste, REST import, rendering, missing fonts, the font report) uses it. A weight-number style goes by that weight, and .fig and REST imports wait for the installed fonts to be indexed.
2026-10-30 15:44:09 +07:00
Christopher Linnett 55e5f31eb5 M3: document the colour-calculation functions this change touches (#1280)
Why type_of gives references no type, what display shows, how resolve_memo bounds one resolve,
and what variable.create and variable.set check before anything changes.
2026-10-30 16:25:26 +08:00
Christopher Linnett bc76665c65 M3: colour variables can be calculated from others, faded or mixed per mode (#1280)
A colour variable's value in a mode can now be {"alpha": {"variable", "opacity"}} - that
variable's colour with its alpha replaced - or {"mix": {"from", "to", "t"}}, a per-channel blend
of two colour variables or literals over the stored values (in the document's colour profile),
alpha included. Calculations resolve lazily like aliases, so bound layers, mode switches,
instances and undo follow their sources; they compose, and a loop, or a calculation that reads
more than 32 variables in its collection's modes (other collections in their default modes), is
refused when it is set. Operands are stored by id and read by exact id, as stored references
are. Files keep them; REST, CSS, Penpot and .fig exports write the colour they show in each mode;
deleting a source leaves its readers on the colour they showed; library import and swap
re-point their operands. The Variables panel shows them with their formula and can swap one for
an alias or detach it to its colour.
2026-10-30 16:25:26 +08:00
bflatastic b157a38e99 Merge main into #1618 2026-10-30 13:05:24 +07:00
samhyunandClaude Opus 5.5 07c936f560 M8: PR #1626 review: tiny scales and pixel-spread layers
local_reach gave up on transforms whose determinant was under 1e-12 and
kept the reach in page units, so a layer scaled to a ten millionth (a
billion units drawn 100 wide) had its shadow cut (245 levels). It now
converts any finite, non-zero scale, capping the result where the sums
would overflow.

Groups and stroked text spread their shadows as pixels, in page units:
their spread no longer also counts in local units.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 14:12:58 +09:00
2f47014917 M3: stored variable and collection references keep their identity (#1613)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
Co-Authored-By: GPT-6 Astra <noreply@openai.com>
Co-Authored-By: GPT-6.1 Terra <noreply@openai.com>
2026-10-30 06:49:06 +03:00
samhyunandClaude Opus 5.5 0df9189750 M8: effects reach as far under a scaled layer
The renderer draws drop shadows, layer blurs and texture at their size in
page units however a layer is transformed, but sized a layer from its local
box grown by the effects' reach in local units, so under a transform that
scales down the effect was cut (245 levels on the canvas, exports holding
half to 60% of the paint).

bounds_within now takes the layer's local-to-page map and turns the reach
into local units per axis. A drop shadow's spread of a filled outline is the
exception: the outline is grown before it is transformed, so that part
follows the layer's scale. Layer bounds and own_paint_bounds take, per
shadow, the larger of the two readings, so an unscaled layer comes out as
before; exports of scaled-up spread shadows, which were cut, now hold them,
and so do the corners of a turned rectangle's spread. The mitre reach is
clamped like the other reaches, so a huge spread no longer makes bounds NaN.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 12:03:28 +09:00
samhyunandClaude Opus 5.5 81f89b314b M8: a group's effects reach past what its children paint
A group or an unclipped frame applies its layer blur, drop shadows and
texture to everything its children paint, including children reaching past
its box. The renderer sized the container's layer from the union of its
children without growing it by the container's own effects, so the blur or
shadow was cut where a child's paint ended; exports of unclipped frames
lost part of the shadow the same way.

bounds_within now grows the children's union by the node's effects reach,
and render_bounds_at grows an unclipped frame's children by its effects.
The retained partial-render test drew this very cut (61 levels in its group
child scene); with the shadow counted the damage passes the partial share
of its 300 x 220 view, so the view is widened to keep it a partial render.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 10:51:57 +09:00
BFlat b2b9e08a46 Merge pull request #1589 from samhyun/fix/export-stacked-blur-bounds
M8: exports hold what stacked layer effects paint together
2026-10-30 07:53:10 +07:00
BFlat d29f544707 Merge pull request #1579 from Silvertree2010/fix/h3968-title-case-capitalises-mid-word
Keep combining marks inside the word in title case
2026-10-30 07:48:32 +07:00
samhyunandClaude Opus 5.5 9426eff1ff M8: exports hold what stacked layer effects paint together
Layer blurs and texture apply one after another and drop shadows are cast from what they leave, but an export grew the node's box by each effect on its own. Stacked blurs came out cut at the image's edges (eight blurs of 10: alpha 10 at the edge), and a sharp shadow under a blurred layer lost the blur around it (alpha 126). Blurs now combine as one whose squared reach is the sum of theirs, texture adds on, shadows grow from that, and paint_reach follows the same rule. Reaches are clamped so adding them stays finite.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 09:10:14 +09:00
NetterSilverandClaude Haiku 5.5 bf97557eb2 Keep combining marks inside the word in title case
Found while testing crates/doc/src/text.rs:150 (TextCase::apply_impl, Title arm) with is_word_char at :156

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-29 22:28:51 +00:00
BFlat 7d76b0c673 M3: component properties keep their order on rename; Make slot asks for the name (#1536, #1537) (#1565)
Properties were listed by name, so a rename moved one. Components now keep prop_order (the names as they were made; a rename keeps the place) and every listing follows it. Make slot opens a name field under the frame's Slot row; Enter makes the slot, Escape cancels, a taken name keeps the field open.
2026-10-30 04:49:00 +07:00
BFlat 3e16119710 Merge pull request #1507 from Silvertree2010/fix/h3859-export-size-label-rounds-the-sca
Keep full precision in export size labels so they parse back
2026-10-30 00:19:59 +07:00
bflatastic 187d091f7b Merge main into #1507 2026-10-29 23:23:12 +07:00
bflatastic 59f000ee59 Merge main into #1542 2026-10-29 23:22:57 +07:00
BFlat 3af5d8a534 M9: comment.list reports the names each message mentions (#1415) (#1534)
Every message (the first and each reply) gets `mentions`: {name, known,
you} for each @mention, by the rule the thread view highlights with
(figcraft_doc::mentions, now shared with mention_ranges). Names resolve
against the file's people and you; unknown names are kept with
known: false; a@b.com mentions nobody.
2026-10-29 22:07:06 +07:00
NetterSilverandClaude Opus 5.5 33ddb44016 Resolve stored variable ids by exact id, not by variable name
Fixes #1513

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 14:11:48 +01:00
BFlat 72b4269383 M8: an AI agent attached over MCP comments under its own name (#1414) (#1529)
Every command an MCP client runs acts as the client's name from initialize (clientInfo.title, else .name) with ' (agent)' after it, or 'AI agent' without one, carried to the app as engine.execute {agent}. Its messages, reactions and read state are its own, and its messages are marked (agent: true in the file and comment.list, an Agent label in the thread). The user's name and comments made in the app are unchanged; in live sessions the name and marker travel with the thread.
2026-10-29 19:15:19 +07:00
BFlat 07681afebe M6: .fig import keeps the file's saved line breaks while the text is unedited (#1236) (#1519)
* M6: .fig import keeps the file's saved line breaks while the text and its box are unedited (#1236)

The line breaker already keeps trailing spaces and the last glyph's letter spacing out of the fit test and doesn't round box widths; imported text wrapped a word early or mid-word because local fonts measure slightly differently from the file's. The import keeps the saved line starts (derivedTextData) on the text with the box width and a stable key of its layout content; layout follows them while both match and no line overflows its box by more than 3% (+1 px).

* M6: local check that imported text wraps at the line breaks its file saved (#1236)

Runs only when FIGCRAFT_FIG_SAMPLES names a folder of .fig files: each fixed-width text with saved breaks wraps there once imported, or falls back by the overflow slack; prints the counts.
2026-10-29 17:04:29 +07:00
NetterSilverandClaude Haiku 5.5 31d7d5ca7e Keep full precision in export size labels so they parse back
Found while testing crates/doc/src/props.rs:1341 (ExportSize::label, via trim_num at crates/doc/src/text.rs:62)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 09:06:47 +01:00
BFlat 8dc3ab5703 Merge pull request #669 from yelintong/feat/flutter-codegen
M7: Flutter code generation
2026-10-29 13:37:49 +07:00
bflatastic 789fa33444 Merge main into #634 2026-10-29 12:25:06 +07:00
bflatastic a81efe5466 Merge main into #669 2026-10-29 11:52:03 +07:00
BFlat 53726b6248 Merge pull request #1444 from Silvertree2010/fix/1067-audit-col-5-id-space-exhaustion
Keep session joins working when a document has out-of-range ids
2026-10-29 11:41:40 +07:00
bflatastic a5e8330aee Merge main into #1417 2026-10-29 10:29:08 +07:00
bflatastic 6b205dd1b5 Merge main into #1444 2026-10-29 10:29:07 +07:00
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
Skyfuller1303 c785e96b1c Merge remote-tracking branch 'origin/main' into feat/fig-vector-region-fills
# Conflicts:
#	crates/format/src/fig/export.rs
#	crates/format/src/fig/map.rs
2026-10-28 10:12:00 +01:00
NetterSilverandClaude Opus 5.5 2d02f289ec Keep session joins working when a document has out-of-range ids
Fixes #1067

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 02:16:00 +01:00