Commit Graph
1169 Commits
Author SHA1 Message Date
bflatastic a632d63602 Merge main into #2049 2026-11-07 09:10:44 +07:00
BFlat 11b2ddfaf4 Merge pull request #1958 from MortenHusted/fix/import-variable-id-collisions
M3: importing a file keeps its own variables and styles when their ids are already taken
2026-11-07 10:03:52 +08:00
BFlat 61e03c4dba Merge pull request #1991 from TrevorBrown-dev/fix/vector-rounded-fill
M8: joining the ends of a path broken open closes its fill loop through the new segment (#1951)
2026-11-07 10:01:58 +08:00
BFlat 47b5979ef0 Merge pull request #2012 from TrevorBrown-dev/feat/text-direction-1830
M4: a paragraph's base direction can be forced: Text ▸ Text direction ▸ Left to right / Right to left (#1830)
2026-11-07 09:58:59 +08:00
bflatastic 88f6986bda Merge main into #2049 2026-11-07 08:58:17 +07:00
BFlat 3a1b698ed4 Merge pull request #2055 from aleksei-suholita/fix/effect-binding-checks
M3: a layer binds an effect's field only when its effect has it, and a bound glass light angle turns round
2026-11-07 09:53:26 +08:00
BFlat 7522c39ba5 Merge pull request #1760 from Silvertree2010/fix/1745-pen-tool-undo-cmd-z-bug
Make undo while drawing with the pen remove only the last anchor
2026-11-07 09:50:33 +08:00
BFlat 5762212ced Merge pull request #2006 from JonRC/fix/jpeg-quality-srgb-icc
M6: JPG exports take a quality and embed the sRGB profile (#2004)
2026-11-07 09:47:15 +08:00
BFlat 6bbaa27f97 Merge pull request #2007 from JonRC/fix/stroke-cap-error-values
fix(engine): an unknown stroke cap lists the caps node.set takes (#2005)
2026-11-07 09:42:05 +08:00
bflatastic 468cab6b2b Merge main into #2055 2026-11-07 08:40:31 +07:00
Trevor BrownandClaude Fable 5.1 171fcd2d8b Merge upstream/main into fix/vector-rounded-fill
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-06 20:40:23 -05:00
Trevor BrownandClaude Fable 5.1 5a63c08a78 Merge upstream/main into feat/text-direction-1830
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-06 20:40:21 -05:00
bflatastic 593a689280 Merge main into #2049 2026-11-07 08:39:43 +07:00
BFlat 401db4beb0 Merge pull request #2046 from StrajnarFilip/claude/fix-2026-merge-layout-conflicts
M9: one auto layout edit on each side of a branch is one conflict (#2026)
2026-11-07 09:39:12 +08:00
BFlat 83054f771d Merge pull request #1971 from TrevorBrown-dev/fix/connector-end-gap
Boards: connectors attached to a side start 4 pt outside the border, as FigJam's do (#1916)
2026-11-07 09:37:24 +08:00
BFlat a3460e4711 Merge pull request #1777 from Silvertree2010/fix/h4857-node-reorder-silently-turns-a-ne
Reject negative and fractional indexes in node.reorder
2026-11-07 09:34:48 +08:00
BFlat 97b16702d5 Merge pull request #1900 from Hamza0101/fix/text-layer-name-follows-characters
M4, #1747: a text layer's name follows the characters typed into it
2026-11-07 09:34:20 +08:00
bflatastic 75847329df Merge main into #2049 2026-11-07 08:29:16 +07:00
bflatastic 642d847329 Merge main into #2055 2026-11-07 08:28:47 +07:00
BFlat 6864ffff4f Merge pull request #1778 from Silvertree2010/fix/h4878-node-create-vector-create-silent
Reject invalid entries in vector.create points instead of dropping them
2026-11-07 09:27:01 +08:00
bflatastic 7214854ed5 Merge main into #2046 2026-11-07 08:26:08 +07:00
BFlat 57496ae937 Merge pull request #1783 from Silvertree2010/fix/h5009-page-move-silently-turns-a-negat
Reject negative or fractional index in page.move
2026-11-07 09:21:59 +08:00
bflatastic 4e5f0f8c04 Merge main into #1900 2026-11-07 08:18:31 +07:00
BFlat 8f2f194cf4 Merge pull request #1787 from Silvertree2010/fix/h5159-variable-movemode-silently-turns
Reject negative and fractional index in variable.moveMode
2026-11-07 09:15:57 +08:00
BFlat c78f6b7080 Merge pull request #2031 from Silvertree2010/fix/2029-bug-snap-to-object-stops-working
Keep object snapping when Snap to Pixel Grid is on
2026-11-07 09:12:11 +08:00
bflatastic 30f149b939 Merge main into #2049 2026-11-07 08:11:36 +07:00
bflatastic 7c3a3b2703 Merge main into #2055 2026-11-07 08:07:48 +07:00
BFlat 09e04e7176 Merge pull request #2037 from denisg0mes/go-to-library-main
M3: Go to main component opens the library file of a library component
2026-11-07 09:06:17 +08:00
bflatastic 9ffe6b07e4 Merge main into #2046 2026-11-07 08:02:46 +07:00
bflatastic ae4d0c912e Merge main into #2055 2026-11-07 07:55:49 +07:00
Oleksii Sukholita d23cebb12f M3: an effects key naming no effect's field binds nothing (#2055)
Review follow-up: `effects/x/radius`, `effects/0` and `effects` name no
effect's field, but `list_binding` doesn't parse them, so the layer's
check took them for some other field and kept an inert binding. A key
under `effects` that names no effect's field is now refused as well.
2026-11-06 20:18:10 +01:00
Oleksii Sukholita 5bdedc3930 M3: a layer binds an effect's field only when its effect has it
`variable.bind` on a layer checked only the variable's type against the
field's scope, so `effects/0/refraction` on a drop shadow, or a binding on
an effect the layer doesn't have (`effects/9/radius`), was kept and did
nothing. An effect style already refuses both. The layer now uses the same
check: effect `i` must exist and have the field, and the field's type
comes from that effect. A binding an older file has on such a field still
does nothing, and still unbinds.
2026-11-06 19:38:52 +01:00
Filip StrajnarandClaude Opus 5.5 b779e46d47 M9: a local view-only share's url is the view URL, not the edit URL
The previous fix made a local share's `link` follow the code shown, but its
top-level `url`, the session's WebSocket URL, still carried the edit token:
`collab.join {link: url}` from a view-only share joined with edit access and
the edit reached the host. The link layer now hands over the view way in as a
link and its URL (`SessionInfo::view`, a `WayIn`, in place of `view_link`), and
`collab.peers` gives a local share's `link` and `url` from the way in for the
code shown (`Live::invite`). Online shares are unchanged.

Regression test: `code_tests::a_local_view_share_s_link_and_url_only_let_people_look`
joins through the `link` and then the `url` over loopback; both are welcomed as
viewers whose edits never reach the host.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LvG6E2VLhC9d5v8VSsf4ng
2026-11-06 19:12:40 +01:00
Filip StrajnarandClaude Opus 5.5 a05201e21f M9: a local view-only share's link is the view link, not the edit link
`collab.share {via: "local", access: "view"}` showed the view-only code but
gave the session's own invite link as `link`, and that link edits: whoever
joined through it could change the host's file. A local share's `link`, top
level and in `sessions`, is now the one that goes with the code shown, and it
follows `collab.share {access}`. Online shares keep the owner's own link there,
as before.

Regression test: `code_tests::a_local_view_share_s_link_only_lets_people_look`
joins through that link over loopback and is welcomed as a viewer whose edit
never reaches the host.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LvG6E2VLhC9d5v8VSsf4ng
2026-11-06 18:45:44 +01:00
Filip Strajnar a2be6632eb M9: share a file on the local network read-only, with a view-only code
The session the app hosts for a local network share now has two secrets:
the edit code and a view-only code (same address and port, a 44-bit secret
of its own). Whoever joins with the view code is welcomed as a viewer: they
see every change and presence, the app makes the file read-only for them,
and the session refuses their batches, as a server does for view links.

Share > Local network offers Can edit / Can view before sharing starts (the
edit code then never shows) and while sharing. collab.share {via: "local",
access: "view"} does the same; share in collab.peers lists both codes and
links. There is still no comment code on the local network.

Next::Stop boxes its event: SessionInfo grew past clippy's
large_enum_variant threshold, and it only ever carries a status or an end.

Tests: loopback_tests::a_view_token_lets_people_look_and_not_edit,
code_tests::a_local_view_code_lets_people_look_and_not_edit,
live_share_tests::a_local_share_can_start_for_looking_only, and the local
share tests that asserted edit-only codes. Docs: collab-protocol.md,
collaboration-parity.md.
2026-11-06 17:03:21 +01:00
Filip StrajnarandClaude Opus 5.5 7f62fc1416 M5: proto.settings refuses an unknown device and answers the settings (#2027)
`proto.settings` took any string as the device ("Pixel Nonexistent", "iphone 16") and saved it in
the file: the presenter then drew no device frame while its picker showed the name as if it were
one. An unknown colour was already refused with the device's colours, but a typo in the device
went unnoticed, and an agent couldn't read the settings back (`proto.settings {}` answered
`{ok: true}`, and neither `inspect_document` nor `get_node` shows a page's prototype settings).

An unknown name, or a value that isn't a string, is now refused with every name the picker offers
("None", "Custom size" and the devices), and nothing changes. A known name is taken in any case and
with spaces around it, and stored as the picker spells it (`known_device`). Every call answers the
page's settings as they are after it (`settings_json`): device, background, rotation, the colour
shown, the screen as presented (null without a device), the custom size and responsive, beside the
`ok: true` it answered before; `proto.settings {}` reads them.

Test: `proto_present_tests::unknown_devices_are_refused_and_settings_read_back`. Docs: mcp.md,
prototyping-parity.md, ROADMAP.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LvG6E2LxvJ6MuFmiLA6Zqm
2026-11-06 12:44:01 +01:00
Filip StrajnarandClaude Opus 5.5 fb55872450 M9: one auto layout edit on each side of a branch is one conflict (#2026)
A padding changed on both sides of a branch gave a conflict for every size and place auto
layout worked out from it: the component's, each instance's and their hugging parents' width
and height, each instance's layout, and every child's position (10 in a file with one instance,
138 with 15). The merge was refused until each had a side, yet the merged file laid itself out
again from the padding whatever they got. A `layout` conflict also took all auto layout fields
together, so a gap changed only on the branch was lost when main won the padding.

`merge::plan` now leaves out properties both sides changed to values both files work out for
themselves (`Plan::derived`): a hugging auto layout frame's or a filling child's size, text
sizing to its characters, an auto layout child's place (when both sides turn and scale it the
same way), and a value a layer in an instance has as its main's layer has it. Merging keeps the
file merged into's own value and normalising lays it out from the merged inputs.

`layout` and `childLayout` merge field by field: a field changed on one side (or the same way on
both) merges, and each field changed both ways is a conflict of its own (`Key::Field`,
`<id>/layout.padding`), written whole from the merged fields and each picked side's value. The
review shows it as "auto layout padding" with both sides' values ("8, 8, 8, 8").

Tests: `branch_tests::one_conflicting_auto_layout_edit_is_one_conflict` (the issue's file: one
conflict, either side laid out as if set directly), `auto_layout_fields_changed_one_way_survive_a_conflict`
(merge and update from main), `sizes_auto_layout_works_out_never_conflict`; the merge fuzz test
leaves worked-out values out of what each side must show. Docs: collaboration-parity.md, mcp.md,
ROADMAP.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LvG6E2LxvJ6MuFmiLA6Zqm
2026-11-06 12:17:11 +01:00
Denis GomesandClaude Opus 5.5 6a31ea7360 M3: Go to main component names a library copy with no link
A file imported from another app can hold library copies in its library
store with no record of the library they came from. On an instance of
one, the command took the local path and failed in selecting it with
"isn't on a page (it is a library component)", which says nothing about
what to do. It now says the copy has no link to its library and that
enabling the library reaches it.

Found on a real imported file, where copies of the same library set were
cached more than once and only one of them could be linked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 02:53:58 -05:00
Denis GomesandClaude Opus 5.5 76af4e3f43 M3: Go to main component opens the library file of a library component
On an instance of a library component the command refused with "open
that file to edit it". The help center describes it opening the file that
holds the main ("Edit main components"), so it now does: the linked
library file opens in a tab, or its open tab comes to the front, on the
main's page with the main selected and zoomed to.

The library keeps the file's layer ids, so the copy's id map
(ImportedComponent::ids) leads from the local copy back to the exact
layer: a nested instance reaches its own main and a variant that variant.

The new tab takes the canvas size of the tab it came from. A tab still at
the default 1200x800 is refitted to the whole page on its first frame,
which would replace the zoom to the main.

A main deleted from the library file since it was published leaves the
file open with a note. A moved library file, a team library on a server
and a path off this computer are refused before anything opens; the last
uses the same check that guards library reads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 02:21:20 -05:00
NetterSilverandClaude Opus 5.5 39c5833818 Keep object snapping when Snap to Pixel Grid is on
Fixes #2029

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 04:10:50 +01:00
Trevor BrownandClaude Fable 5.1 d5d8f1e8ed Merge upstream/main into fix/vector-rounded-fill
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-05 16:41:35 -05:00
Trevor BrownandClaude Fable 5.1 7c6bc49e9f Merge upstream/main into feat/text-direction-1830
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-05 16:41:34 -05:00
8da11395e9 M3: glass effects take variables, and their frost stays within 0–100 (#1886)
* M3: glass effects take variables, and their frost stays within 0–100 (#1885)

Glass's light angle, intensity, refraction, depth, dispersion, frost and
splay bind to number variables, on a layer (`variable.bind`,
`effects/<i>/<field>`, `radius` being the frost) and in an effect style
(`style.bind`), and the Design panel's glass fields take a variable like
the shadow and blur fields. A variable reads as the field shows it:
intensity, refraction, dispersion and splay in percent.

A glass's frost stays within 0–100, the reference tool's range: a larger
frost from an older file draws at 100, and the field, a bound variable and
scaling stop there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M3: a frost from an older file scales and shows as it draws (#1885)

Review follow-up: a frost saved past 100 draws at 100, so scaling a layer
starts from that (150 halved draws at 50, not 75) and the Design panel's
Frost field shows it. The revision row's binding path escapes its pipes, so
the table keeps three columns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M3: Dev Mode and .fig export show a frost from an older file as it draws (#1885)

A frost saved before the 0–100 range draws at 100, and the canvas, CSS and
scaling already read it that way, but Dev Mode's Glass row and the .fig
export still read the stored number, so they said 150 where the canvas
shows 100. Both now read the sanitized frost.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Oleksii Sukholita <oleksii.sukholita@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-05 22:08:15 +01:00
Trevor BrownandClaude Fable 5.1 c16c3676c2 M4: text direction: CSS keeps Left beside a forced rtl, paragraph arrays read to the paragraph count, the empty last paragraph keeps its direction (#1830)
From review: `text-align: left` beside `direction: rtl` (Left is absolute on the canvas); the REST import reads the four parallel paragraph arrays up to the paragraph count, not the 256-entry list limit; `rtl_para` gets an entry for the position after the last char so an empty last paragraph keeps its forced direction; the localization status line.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-05 03:46:28 -05:00
Trevor BrownandClaude Fable 5.1 cd5c9aeb09 M4: a paragraph's base direction can be forced: Text ▸ Text direction (#1830)
Direction came from the letters only; Add right-to-left text says controls override it per paragraph (Text > Text direction, Left to right or Right to left). `TextDirection` on `ParaList`, `Bidi::of` with a base level per paragraph, `text.direction` and the three menu commands (the edited paragraphs while editing), `node.set textDirection`, the Type settings control once a right-to-left script is in the text, inspect `textDirection` / `lineDirections`, the CSS `direction`, and the `.fig` lines' `sourceDirectionality` in and out.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-05 03:23:07 -05:00
JonRC 19af9c9c3b M6: test that a bad JPG quality is refused for every format (#2004)
As vdCompat, bleed and the SVG flags are: PNG, WebP, SVG and a format taken
from the path refuse quality 0 and write nothing, and a quality kept on a PNG
setting is used once the setting becomes a JPG.
2026-11-05 00:41:46 -03:00
JonRC a55a728c4c M6: JPG exports take a quality and embed the sRGB profile (#2004)
file.export always encoded JPG at quality 90, and an sRGB document's JPGs
carried no colour profile, so viewers could only assume sRGB. PNG exports
always get an sRGB chunk, and Display P3 JPGs already embedded their ICC
profile.

- ExportSetting gets a quality (1 to 100, default 90, saved only when it
  isn't 90), parsed with the other export options: file.export, export.add,
  export.update, file.exportLayers and MCP export take it.
- encode_export_with takes the quality; encode_export keeps quality 90.
  Every JPG now embeds the document's profile, sRGB included.
2026-11-05 00:09:19 -03:00
JonRC 5244b0075d fix(engine): an unknown stroke cap lists the caps node.set takes (#2005)
strokeCap, startCap and endCap answered only "unknown stroke cap". They now
name the key and every cap, as strokeAlign, strokeJoin and dashCap do.
2026-11-04 23:53:43 -03:00
Trevor BrownandClaude Fable 5.1 cacf73a094 M8: a reconnected loop is closed only where the segment makes it one continuous chain (#1951)
A loop whose segments are the chain between the two ends but are listed out of walking order has
no place for the new segment that makes it continuous; inserting it at the first gap would leave
the other gaps. Such a loop is left as it was (the walkers' bridging still draws it), as a loop
broken in two places is. Test: the chain A-B-C-D-E with its loop listed out of order, joined A to E.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-04 06:45:00 -05:00
Trevor BrownandClaude Fable 5.1 9ff2105d9b M8: a reconnected loop keeps its direction, so a hole stays a hole whichever end was clicked first (#1951)
Cause: `ops::connect` rebuilt the broken loop along the path from the second end to the first, so
connecting the ends in the other order reversed the loop's walk; a hole wound against its outline
then wound with it and the non-zero rule filled it.

Fix: the new segment goes into the loop at its gap, in the loop's own order. Test: a ring whose
hole loses a corner with Delete and is connected again, either end first (red before for one
order).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-04 06:03:00 -05:00