A page that embeds the web build can hand it a file and get every save back instead of a browser download, and hears about unsaved changes: ?host=parent for a same-origin iframe (postMessage, the protocol the GridCraft, PdfCraft, WordCraft and CADCraft bridges use) and data-host="page" on the canvas for a page that loads the module into its own document (non-bubbling CustomEvents on the canvas). The bridge holds a Transport (Parent or Page) with one send; the pure rules are unit-tested; the hosting guide documents both modes.
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>
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.
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.
Minimize (#1835) puts an Expand button where the navigation bar was. In a mirrored window the rail is on the right, so the button goes to the top right there; the test pins both sides.
In a small or narrow window the Design panel is drawn narrower than its
saved width, so the mirrored style picker read the saved width and could
open over the panel. It now places itself from the panel's drawn rect,
as the colour picker does; the left-to-right placement is unchanged.
With the UI in Arabic (or another right-to-left language), the icon rail
and the layers panel go on the right and the Design panel on the left.
The panel edges size the panels dragged the other way, with the same
width preferences. The Design panel's edge line, the colour picker, the
style picker and the interaction editor sit on its canvas side, and
egui's own chrome mirrors through Style::right_to_left. Rows inside the
panels, the toolbar and the tab bar keep their order for now.
A queue past what a server takes at once would get the connection refused before
the end of it arrived. The desktop keeps sending at the pace for up to 10 s on its
own thread, and the browser sends what may go now. `collab.reconnect` also respects
a refusal of network access while it waits to reconnect.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The pace was set when a message was queued, so messages held up by a stalled
connection left together afterwards and could trip the server's rate limit. One
queue now counts it as they go, for the desktop and the browser alike.
Leaving while changes are unconfirmed now says so: an undo waits for its edit, so
what was still on its way may not all reach the others.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A client taking in a large welcome holds one queue slot for the whole transfer,
while everyone's presence fills the rest. Five people moving their cursors fill
1024 slots in about 7 s, and the joiner is dropped with the same result on every
try. Presence now stays out of a queue that is half full: it is latest-wins, and
the other half stays free for edits, which can't be left out.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Queued sync messages wait in a list the session loop drains, so Leave, a lost
connection, presence and pings are never held up behind them. A text frame from
the host stays final. A session that ended stays on its tab after a Save as:
ending it there could turn a tab into a plain local file with no word about it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Joining again after a session couldn't go on opened your offline changes in a
copy beside the file. They now go on top as one undoable edit, through the
three-way merge branches use. The rejoin asks to resume as the same client, so
layers made offline are yours to send. What can't go in (the same thing changed
both ways, or layers a new client can't make) leaves the server's version, and
your whole version opens beside it so nothing is lost. A server that no longer
knows you merges the same way instead of discarding unconfirmed work.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A session you joined could end for reasons that pass, or after about 85 s of
retries, and its tab then looked like an unsaved local file. Work done there
reached no one, with only a 3 s toast to say so.
A live connection now reconnects until you leave, pausing a minute after each
12 attempts. Only refusals that are for good stop it, so "too many messages",
"busy" and the like retry. Messages queued while offline go out paced so a long
queue can't trip the server's rate limit. A session that ended stays on its tab:
the tab shows a red mark and a strip across the canvas offers Retry now or Try
again, Save a copy and Dismiss. Joining again uses the same tab, and what changed
offline opens beside it as an unsaved copy. A session that gets out of step
joins again by itself.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The pages list ignored whether the rename field was still open, so a rename
cancelled with Escape or committed unchanged stayed set. Its idle field sat
over the row's name, caught the next click and started editing, and the page
didn't open. The row now ends the rename when the field lets go, as layer rows
do. Rename page from the row's menu now takes the keyboard, as a double click
does: it had relied on the idle field.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Text wrap style, auto layout resize shortcuts, Hide UI and Minimize the
interface, interface size commands, library analytics periods and
counts, Replace image, Extend collection and upload from computer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
Cause: `visual_gaps` mapped a gap at a run boundary to the gap before the later run's first item,
which for a run nested inside another (a Chinese word in a Hebrew phrase in Chinese text, made
with embedding marks) is a gap inside the embedding: one boundary took it, another got none, and
the line came up a gap short of the box's edge.
Fix: each gap is reordered with the line as a glyph of no width at the lower of its two
neighbours' levels (rule N1 for a space between them), through the same `visual_order`: items and
gaps keep alternating and each gap lands once at the boundary it belongs to. Test row with explicit
embedding marks (red before: "char 8 moved 2 (want 1)").
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Cause: deleting a point with Delete (not Shift+Delete) opens the path and leaves its region's
loop with a gap, filled closed by a straight line across it; joining the two ends again (the pen
onto the other end, Connect, Join) added the segment to the network but not to the loop, which
still spanned the gap. Unrounded, the straight line and the segment coincide; once the points are
rounded, the fill and the selection outline run straight across while the stroke, walked from the
network itself, follows the segment and its arcs (#1506's picture).
Fix: `ops::connect` splices the new segment into the loop whose segments are the chain between
the two ends, so the loop closes through it; a chain no loop fills still becomes a region of its
own. Tests: the chamfer made by Shift+Delete, by Delete then Connect, and drawn with the pen, each
rounded, with the fill tracing the stroke (engine); every loop order and segment direction of a
chamfered rectangle (doc).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Review: a combining mark shaped as a glyph of its own (U+20DD on an ideograph, say) got a gap
between it and its base; a gap after the last logical glyph of a right-to-left run opened after
that glyph's visual place, inside the word, instead of between the run and the ideograph after it.
A gap now needs the next item to start a grapheme cluster, and `visual_gaps` opens each logical
gap at its visual boundary: between logical neighbours where they are, between two runs where they
meet. Two more rows in the test.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Cause: justified lines only widened their spaces, so a line of ideographs, written without any,
kept its natural width and its right edge never reached the box's.
Fix: the room on a line (but a paragraph's last) also goes into the gaps next to an ideograph,
kana, Hangul or full-width form wherever a line could break there (`breaks::justify_between`,
beside `break_between`): each is a word of its own, as for line breaking, and no gap opens before a
closing mark or after an opening one. Spaces widen as before; mixed text widens both.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Review: the first and last runs were checked for lying at right angles to their sides, not for
going out of the object, so a path whose first run went up through the start's box (or whose last
arrived from inside the end's) was kept and drawn through it. Both runs now have to go the way the
side faces; one along the side or back into the object leaves the connector to the router. Two more
cases in the routed-here test.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The `.jam` importer routed every elbowed connector itself, so one drawn around a layer by hand in
FigJam came in routed through it. FigJam saves the path as `connectorControlPoints`: one per run
between the first and the last, each a point on its run (in the connector's parent's coordinates)
with the run's axis; the first run leaves the start's side and the last arrives at the end's. They
become the `ConnectorPath::Runs` a board connector reshaped by hand has, walked by
`figcraft_doc::route_along`, so the path is kept exactly. Points that don't make such a path (off
both axes, two runs the same way in a row, a first or last run sideways to its side, more runs than
a hand could make) leave the connector to the router as before, and only connectors routed here
count in the "own routing" note. `figcraft_doc::attach_points` and `MAX_RUNS` are exported for it.
Docs: file-format-parity.md, boards-parity.md.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Review asks on #1773: `css_hue_wraps_a_whole_turn_and_calc_is_not_a_number` checks that 360, 1turn,
400grad, 2 pi rad and -1turn all read as hue 0, that -90deg, -0.25turn, -100grad and 630 all read as
270, the same in oklch() and lch(), and that `calc(NaN)`, `calc(infinity)`, `calc(1)` and
`calc(1turn)` are refused as non-numbers rather than read as 0 or clamped.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* perf(engine): replace linear scans that made document-sized inputs quadratic
enum_of re-classified the whole string for every character; widget names,
comment mentions and devcompare's common siblings each searched a growing
Vec; data.fill's export names and codegen's class names retried every
suffix from 2 against a list, cubic in the number of copies of one name;
and a batch rename template had no length cap although it is expanded
again for every match.
* address review: report the pattern in its own length error