* M3: glass bends with the reference tool's thick-lens profile and splits its colours over 8 steps (#1885)
The backdrop bends most a 1-unit bevel in from the edge, falls off as the
3.5th power of the way out of the curve to nothing `depth` further in, and
moves a fifth as much on the bevel itself (it fell off as the square of the
way in from the very edge). Dispersion samples each channel at 8 steps along
the bend with a triangular window, red moving furthest and blue least (it was
three samples, blue furthest), as far apart as the reference tool's: at 100%
red and blue move 0.16 of the bend apart (`Glass::DISPERSION_SPREAD`).
`Glass::max_bend` bounds red's furthest step, so the backdrop read around a
pane covers every sample, and the field margin covers the bevel.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M3: glass's bend stays within `max_bend`, checked on a single bright pixel too (#1885)
Review follow-up: the bend reaches `depth` × `refraction` a bevel in from
the edge, so `Glass::max_bend` drops its 0.999 and bounds it, as a test now
checks for several refractions. A single white pixel on black pins the
dispersion's triangular window, which a ramp and a flat backdrop can't tell
from one centred sample. The parity assessment records the bend, the
dispersion and how they were measured against the reference tool.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M3: glass's flat inside skips the bend's curve, and full dispersion stays within `max_bend` (#1885)
Review follow-up: `bend_at` returns 0 straight away past `depth` + the
bevel, where the curve and the bevel's easing are both 0, instead of
working out a power of 0 for every pixel of a pane's flat inside. A test
moves one white pixel along a row under every bend of the profile at full
dispersion and checks that no channel ever reads it further out than
`Glass::max_bend`, which sizes the backdrop read around a pane.
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>
* 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>
* M1: dragging a gradient handle on the canvas keeps the colour picker open (#1852)
A press on one of the open gradient's handles or stops closed the picker: the canvas took the press
and began the drag, then the picker, drawn later in the same frame, saw a press outside its card and
closed, committing the drag's interaction at once. The handles vanished with the picker and the drag
went on without an undo step of its own. The handles are the picker's controls: while a gradient
handle or stop drags (`handles::gradient_dragging`, the press included) the picker doesn't close on
an outside press, as it already doesn't while the eyedropper samples. A press elsewhere on the
canvas still closes it. Docs: ui-parity.md.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* M1: a handle drag released off the canvas ends there (#1852)
Review: a gradient handle drag whose button went up over a panel, over the colour picker or outside
the window stayed stashed, its interaction uncommitted, until the pointer was back on the canvas,
and the next press there moved the handle to wherever it landed; with the picker now held open while
a gradient drag is stashed, it would also have stayed open meanwhile. `handles::end_released_drag`
commits and clears a stashed drag when the button is up, called from the canvas input's off-canvas
branch and when the pointer is over something drawn above the canvas. Test:
`a_gradient_drag_released_off_the_canvas_ends_there` (released over the sidebar and over the picker).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* M1: a handle drag ends on a release over a ruler, and when Escape or Undo closes its gesture (#1852)
The release check ran only where the pointer was not on the canvas, after the
comment pins and ruler guides had their turn; both return early, so a gradient
handle released over a ruler stayed dragged, uncommitted, until the next press.
It now runs first, wherever the pointer is, and the frame it ends a drag on is
the handle's, as the release on the handle itself was.
Escape (the picker closes and commits) or Undo can also close the gesture while
the button is still down. The stashed drag then kept setting the fill every
frame with no gesture open, one undo step a frame; with no gesture open the
drag is now over.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
The window keeps a selection of variable ids: a click selects one row, Shift+click the range from the anchor through the rows shown, Cmd/Ctrl+click toggles a row, and Escape or a click on the empty space clears it. Selected rows use the sidebar's selected-row colour.
Right-clicking a row that is part of a selection of several offers Delete, Move to group and Scope for all of them, each sent as one command.batch so it is one undo step. Rename and value edits stay single-row.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The Design panel's Boolean groups menu listed Union, Subtract, Intersect and Exclude as text only. Each entry now leads with its operation's icon, drawn in code from polygons and rectangles: two offset squares with the result filled at reduced alpha. The commands, enabled rules and layout are unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Bands that start at different offsets across the flow (a centred title above a card) are lined up at the smallest offset, which moves some of them, so the result now carries a warning for it. Instances and layers inside them are refused before anything changes, with the same error Convert to group gives; a main component still works.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
layout.suggest used to run layout.add and made one row of everything. It now splits a frame's layers into rows or columns by the gaps between them, wraps each band of several layers in a hugging frame without fill and repeats inside it, so the design keeps its placement where the gaps are even. The frame keeps its size, and uneven gaps and layers that overlap along both axes come back as warnings.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
From review: "the Indic scripts" promised more than the sample list holds. The localization doc, the ROADMAP and the families comment now say Devanagari, Gurmukhi, Gujarati, Tamil, Telugu, Kannada and Malayalam.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(ui): About credits table columns can be dragged wider or narrower
The Contributors and Models tables already open fitted to the dialog, the
name column taking the spare width, and scroll sideways only when there is
not room for every column. Their columns were fixed, though: no edge could
be dragged. They are resizable now, down to 28 points (the flexible column
to its minimum). Widening a column grows the table, as in a spreadsheet,
and only then does it scroll sideways; cells clip instead of overlapping.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* About: refit credits tables when what they measure changes
The columns became resizable, so the table keeps its own widths. A longer
name, a changed cell or a resized window left the old widths in place and
clipped text. The table now goes back to its fit when a column measure or
the clip rect width changes, which scrolling never moves.
Adds tests that drag every column edge, change the fit and scroll a
widened table, as CodeRabbit asked for the drag.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Tests: a fitted credits table fills the window exactly
The table ends on the window edge, which the layout rounds to whole points,
so the check allows that edge.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
From review: crate::cjk picks the Bengali and Thai faces after this list in its own order, so the two scripts come out of SCRIPT_SAMPLES and their families out of SCRIPT_FAMILIES; the Hindi row says the desktop UI takes an installed Devanagari face; the ROADMAP status line and Localization row carry the change.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A String variable's Amharic value drew as boxes in the panels: `bidi::script_fallbacks` sampled Hebrew and Arabic only. It now samples one letter of each of 18 scripts against the system families that draw them on Windows, macOS and Linux; `theme::system_fallbacks` takes the first installed one per script, once. Test: `bidi::tests::other_scripts_take_an_installed_system_face`; localization doc and ROADMAP updated.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
In page host mode a plain Save of a .figcraft the host opened used the document's internal name, so the host got a different file name with saveAs true instead of an update to the file it opened. Services gains host_file(); Save uses it for a .figcraft host file, Save As keeps the document name, and a missing, .json or .fig host file stays a Save As.
The transports, the message types and the origin checks come from the shared crate (storytold/craft-libs#420); FigCraft keeps what is its own: which saved files become the document, the open size limit and the author.
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>
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>