Commit Graph
181 Commits
Author SHA1 Message Date
bflatastic b6b9b2995f Merge main into #2224 2026-11-12 12:56:12 +07:00
bflatastic dad8d2363c Merge main into #2224 2026-11-12 11:47:53 +07:00
bflatastic 05c8d6b2e4 Merge main into #2121 2026-11-12 11:47:19 +07:00
bflatastic ae6e6231c6 Merge main into #2121 2026-11-12 11:33:43 +07:00
bflatastic dff6e8feb3 Merge main into #2245 2026-11-12 11:19:38 +07:00
bflatastic ebca682595 Merge main into #2224 2026-11-12 11:12:04 +07:00
BFlat 6c7532e8b0 Merge pull request #2240 from TrevorBrown-dev/fix/outline-after-pen-bridge-1951
M4: a rounded vector's fill and outline follow every corner when a segment closes its open loop (#1951)
2026-11-12 11:09:33 +07:00
bflatastic 13635a24ea Merge main into #2121 2026-11-12 11:08:15 +07:00
bflatastic 0c5bbc07a7 Merge main into #2224 2026-11-12 11:03:16 +07:00
bflatastic cf28715c18 Merge main into #2221 2026-11-12 10:47:13 +07:00
bflatastic a360818231 Merge main into #2245 2026-11-12 10:27:59 +07:00
bflatastic a905f50211 Merge main into #2224 2026-11-12 10:11:23 +07:00
bflatastic ed4f4defc8 Merge main into #2240 2026-11-12 10:03:15 +07:00
bflatastic 34b2bba9d3 Merge main into #2121 2026-11-12 09:30:04 +07:00
Trevor BrownandClaude Sonnet 5.5 b8f05d1948 M4: a typed number and full stop start a numbered list at that number, as measured in Figma
Typing `5. ` at the start of a plain paragraph starts a numbered list whose first item reads 5 (Figma has no start-number field); the next item goes on with 6. Digits only, 1 to 9999; `0. `, `12a. ` and `2)` stay text (not measured), `1)` works as before. The measured restart rows (plain paragraph, bullet item) are no longer marked silent.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove; Opus 5.5 briefed; Sonnet 5.5 wrote at medium
2026-11-11 09:58:57 -05:00
Trevor BrownandClaude Sonnet 5.5 a28c03d9f1 M2.1: grid Fill tracks share the space and let a fixed child overflow, as measured in Figma
On a fixed-size axis a Fill (1fr) track now splits the free space by its fraction, after gaps and padding, and a fixed-size or hug-size child alone in it asks nothing of the track, so a child bigger than its share overflows into the next track. Measured in Figma web: frame 300 x 200, rows 1fr / 1fr, children 100 x 150 and 100 x 20 give rows 100 / 100 (second child at y 100); with a row gap of 10, rows 95 / 95 (y 105); with the frame height on Hug the rows fit their content, 150 / 20 (frame 170). This replaces the 2026-10-11 rule from the documentation (a track never gets less than its content). Unchanged: a Fill child's own minimum, spanning children, Hug and Fixed tracks, the Hug-axis path.

Three behaviour cases flipped to the measured rule and three added with the measured numbers; the two parity docs are updated.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove; Opus 5.5 briefed; Sonnet 5.5 wrote at medium
2026-11-11 08:26:17 -05:00
Trevor BrownandClaude Sonnet 5.5 a75d8bf04f M4: Delete closes a region's loop through the segment that joins its ends (#1951)
A Pen bridge between two degree-2 points leaves the region alone, and Delete on the point between them then rotated the loop open with the bridge in no loop, so even an unrounded fill ran straight across a curved bridge. reopen_loops now pushes the one segment in no loop that joins an open chain's ends onto the loop's end (winding kept, loop by loop), as connect does. This also covers removing segments and the eraser.

closing_segment is public, derives its own start point, walks strictly (a loop out of walking order is left alone) and gives up when two segments qualify. The render fix stays as the fallback for files already saved in that state.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove; Fable 5.1 reviewed; Sonnet 5.5 wrote at medium
2026-11-11 02:16:29 -05:00
Trevor BrownandClaude Sonnet 5.5 8e6c342f13 M4: a rounded vector's fill and outline follow every corner after a Pen bridge (#1951)
The reporter's file has a corner cut off by a segment drawn first, with the corner point then deleted with Delete. The region's loop is left as the open chain of the other four segments, and the cutting segment, which joins the chain's two ends, is in no loop. The stroke follows the network, so it rounds both ends of that segment; the fill and the selection outline follow the loop.

Cause: VectorNetwork::rounded_smoothed rebuilds each loop by inserting a corner's arcs where the loop arrives at the point, so the point the loop starts at is never cut (in a closed loop it is arrived at last), and the open loop's closing edge ran straight to it. closing_segment now finds the segment joining an open loop's two ends when no loop holds it, and rounding walks it as the loop's last segment, so every corner of the fill and outline matches the stroke. Closed loops and the cases #1991 fixed are untouched.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove; Opus 5.5 briefed; Sonnet 5.5 wrote at medium
2026-11-11 02:01:32 -05:00
Trevor BrownandClaude Sonnet 5.5 529134b850 M4: a boolean group's corner radius rounds its result (#2222)
A boolean group takes a corner radius (and corner smoothing): the Design panel shows the field for it, `node.set cornerRadius` writes it, and the renderer rounds every sharp corner of the group's result after the fold, convex and concave alike. Layers inside keep their own radius and do not stack with it, a radius larger than the edges allow shrinks to fit while the field keeps the typed value, Flatten bakes the arcs, and a mixed selection reads Mixed. Measured in Figma's web app on 2026-11-10.

The rounded result is cached apart from the fold (keyed by the folded path, radius and smoothing), so scrubbing the radius never redoes the boolean steps. The .fig and REST importers and the .fig exporter now carry the radius and smoothing of a boolean group, and paste properties and radius variables reach it. Docs: ui-parity, target-app-parity, gaps, file-format-parity, ROADMAP, mcp.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove; Fable 5.1 designed; Sonnet 5.5 wrote at medium
2026-11-10 13:36:10 -05:00
Trevor BrownandClaude Sonnet 5.5 020388cf1f M10: Export: a background blur or glass exported on its own keeps the content behind it (#2213)
An export of a layer draws only that layer, so a background blur or glass in it read an empty
backdrop. The export now renders the page's layers below the first exported layer, with the margin
the effects reach, as the backdrop (render_area_over), clipped to the export area; only the exported
layers are drawn on top. Layers without those effects, and every other export path, are unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove; Opus 5.5 briefed; Sonnet 5.5 wrote at medium
2026-11-10 10:31:10 -05:00
Trevor BrownandClaude Opus 5.5 62e7e3a41a M8: the parity docs scope the points' X and Y to the layer being edited
CodeRabbit on #2121: with points selected in several edited layers, the X and Y fields read and set only the active layer's points (`selected_parent_points` and `vector.edit.position` use the edited layer), so the status lines, the vector edit row and the revision entries now say so, and the row lists X and Y over several edited layers under Not yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove and wrote
2026-11-08 09:32:09 -05:00
Trevor BrownandClaude Sonnet 5.5 6ac20c7d53 M8: points' X and Y in vector edit mode show Mixed and set every selected point, as in Figma
Cause: with several points selected, the Point section's X and Y showed the selection's top-left and `vector.edit.position` moved the group so that its top-left landed on the typed value. Figma shows the value the selected points share (Mixed when they differ) and sets every selected point's own coordinate.

Fix: `vector.edit.position {x?, y?}` takes, per axis, a number (every selected point's own coordinate becomes it, in the parent's coordinates) or `{add}`, `{mul}`, `{div}` applied to each point's own coordinate; each point moves by its own delta with its handles, in one undo step; a zero divisor, a non-finite result, unknown keys and other junk are refused. A new `selected_parent_points` gives the panel the points in the parent's coordinates. The panel's X and Y are mixed-value fields: the shared value (compared within 1e-6) or Mixed, a typed value sets every point, and a relative edit typed into a Mixed field sends the object form. A scrub only starts from a shared value and sets it absolutely each frame inside one interaction, as the other point fields do.

Verified: engine tests for two points at different x, one point, add, mul and div, curve handles, one undo, the parent's coordinates and refused junk; panel tests for Mixed, typing -200 and a relative edit on a Mixed field, and a single point's value; the engine lib tests, the vector and field panel tests, clippy and fmt. Measured in Figma on 2026-11-08.

Not changed: the W and H row, other edited layers' points, and scrubbing semantics (the Mixed field cannot be scrubbed, as no start value exists).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove, measured and briefed; Sonnet 5.5 wrote at medium
2026-11-08 07:53:14 -05:00
bflatastic f4f16f1ba1 Merge main into #1913 2026-11-07 10:57:11 +07:00
BFlat e9fc975aa6 Merge pull request #1756 from dotCooCoo/feat/vector-network-corner-smoothing
M4: closed vectors take corner smoothing
2026-11-07 10:27:00 +08:00
BFlat 51b5b700b3 Merge pull request #1989 from TrevorBrown-dev/fix/justify-cjk
M4: justified Chinese, Japanese and Korean text spreads the room between its characters (#1970)
2026-11-07 10:24:44 +08:00
BFlat 8ea82be66b Merge pull request #2047 from StrajnarFilip/claude/fix-1906-minimised-control
M8: a minimised window under Wayland answers control requests (#1906)
2026-11-07 10:09:09 +08:00
bflatastic a9e4b13680 Merge main into #1989 2026-11-07 09:03:20 +07:00
bflatastic a170ca7e3e Merge main into #1913 2026-11-07 08:45:02 +07:00
bflatastic 7e9a853da6 Merge main into #2047 2026-11-07 08:43:16 +07:00
Trevor BrownandClaude Fable 5.1 0df97a91bb Merge upstream/main into fix/justify-cjk
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-06 20:40:26 -05: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
bflatastic 1b25d31964 Merge main into #1913 2026-11-07 08:33:53 +07:00
bflatastic 4e5f0f8c04 Merge main into #1900 2026-11-07 08:18:31 +07:00
bflatastic 7e316c2cf1 Merge main into #2047 2026-11-07 08:01:58 +07:00
Oleksii Sukholita 3d41394bfc M3: glass's docs say which centre splay bends away from and where a shape gets no core
Splay bends away from the centre of the layer's bounds, as the reference
tool's does: on a ring or an L that centre lies outside the shape, so an
inner edge bends towards the hole. Checked side by side on a ring and an
L at Splay 0% and 100%. The module doc still said splay 0 bends along the
edge's normal; it bends towards the core since #1889.

`MAX_SHAPE_PX` now says it is 4M px on the desktop and 1M on the web, and
that a bigger shape gets no core, so a large glass text or vector can bend
differently once zoomed into, in a 2× export, or on the web.

Docs only.
2026-11-06 20:29:14 +01:00
Filip StrajnarandClaude Opus 5.5 853e8c6cc4 M8: a minimised window under Wayland answers control requests (#1906)
Under Wayland an app can't tell that its window is minimised (no `minimized`, no `occluded`),
and the compositor stops asking it to draw. eframe keeps running the app's logic for a window
whose redraw is overdue, but `control::background` still left every request for "the window's
frame", which never came: with the window minimised on GNOME or KDE Plasma each control-channel
or attached-MCP request waited and timed out after 60 s ("it was not applied").

`control::background` now treats a window that drew no frame for `NOT_DRAWING_AFTER` (0.2 s)
while a request waited for it as hidden: requests that act on the app's state are answered from
the logic passes, as they are for a window the platform reports minimised. `FigApp::control_waits`
keeps, per window, its pass count and the time the wait began; a frame drawn since starts the
wait again, and a pass is asked for 0.2 s later while a request waits. Screenshots and `ui.input`
still wait for a drawn frame. Measured on GNOME Wayland with the window minimised by its own
button: 0.54 s a request (before: 13.7 s for one, then 60 s timeouts).

Test: `idle_tests::a_window_that_stopped_drawing_answers_from_its_logic_passes`. Docs:
control-protocol.md, mcp.md, hardware-parity.md, target-app-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:27:59 +01:00
SHXJ 69d4de1c0e Merge remote-tracking branch 'origin/main' into feat/traditional-chinese-taiwan
# Conflicts:
#	ROADMAP.md
#	docs/target-app-parity.md
2026-11-06 08:23:55 +08:00
Trevor BrownandClaude Fable 5.1 e3ce3e46bb Merge upstream/main into fix/justify-cjk
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-05 17:38:31 -05:00
Trevor BrownandClaude Fable 5.1 023762b7c6 Merge upstream/main into fix/vector-rounded-fill
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-05 17:38:30 -05:00
8eb0b87646 M3: glass bends towards its core, and rounded rectangles work it out in closed form (#1889)
* 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 light is a line on its bevel, and splay turns the bend towards the centre (#1885)

The light is a line on the 1-unit bevel added to the backdrop, as the
reference tool's: fully where the edge faces the light, none below 0.35 of
facing it and 0.6 as much on the opposite side, with a faint gradient across
the curve, 5% darker facing the light and lighter opposite. It was a glow
mixed towards white, as deep as `splay` made it, so `light_reach` goes.

`splay` turns the bend from along the edge's normal towards the way away
from the layer's centre, and no longer widens the light: files with a splay
above 0 look different. The direction to the centre is worked out in the
shape's own frame (`own_frame`, shared with the field cache), so cut renders
match the whole one byte for byte.

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

* M3: glass bends towards its core, and rounded rectangles work it out in closed form (#1885)

The backdrop bends towards the glass's core, the shape shrunk by `depth`
and the bevel, instead of along the smoothed edge normal: round a sharp
corner the bend turns smoothly instead of meeting along the diagonal, and a
part thinner than twice that (a capsule, a letter's stroke) bends across
itself towards its own middle, pixels about as deep as any within 2 px. The
fields keep a bend direction beside the normal (an exact feature transform
from the core, box-blurred once), their key includes the core's depth, and
their margin covers the core.

Rectangles, frames, components, instances and sections whose corners aren't
smoothed, under a rotation, flip and even scale, work their glass out in
closed form (`AnalyticRect`): signed distance, the light's normal (corners
rounded to at least the bevel) and the bend direction (corners rounded to
the core) per pixel, in the layer's own frame, with nothing to rasterise,
transform or keep. A zoom frame draws in 40 ms instead of 54 and a resize
frame in 46 instead of 67 (`glass_render_timings`, release, 2×).

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

* M3: cut renders of glass match with splay turning a dispersed bend (#1885)

Review follow-up: the cut-versus-whole test had splay only on looks without
refraction, so no turned bend reached the dispersion. A look with splay 1 and
the default bend and dispersion now renders the same however it is cut. The
parity assessment records the light on the bevel and splay's new meaning.

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

* M8: a glass too big to keep bends along its normals, and the core's fields take less memory (#1885)

Review follow-up: a shape that doesn't use the closed form and whose whole
window at the zoom, with the core's margin, would pass `MAX_SHAPE_PX`
(a large vector, text or smoothed rectangle, or a deep glass far zoomed in)
bends along its edge normals instead of working out a core, with the plain
margin. The choice depends on the shape and the zoom alone, so every band and
patch of a render agrees, and no render works out a core over more pixels
than a kept shape holds. The core's feature transform keeps `u32` indices
(16 bytes a pixel) and drops its maxima and core before making the
directions.

The edge normals are box-blurred twice again and the bend directions once,
as their docs say: the two were swapped.

The parity assessment records the bend towards the core and the closed form.

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>

* M3: glass's light shows over a transparent backdrop, and its flat inside skips the light (#1885)

Review follow-up: the light was added to the backdrop's colour and then
clamped to the backdrop's alpha, so with nothing under the glass (an
export of the pane alone) the line on the bevel vanished, where the old
glow raised the alpha and showed. Now the alpha rises as far as the lit
colour needs (`lit`), and stays the backdrop's wherever the light fits
under it, so renders over an opaque backdrop don't change.

Past `depth` + the bevel the rim light, the gradient and the bend are all
0, so those pixels take one sample of the backdrop and skip the light's
direction, facing and splay work, as the old glow's `reach` early-out did.

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

* M8: a kept glass shape works out only its bend again when the depth changes, and a core gets a wider margin (#1885)

Review follow-up. The field cache was keyed on the core's exact depth, so
every step of the Depth slider missed it and worked the whole shape out
again over a window twice as wide: coverage, distances, normals, the
feature transform and the blur, every frame, for every shape that isn't a
plain rounded rectangle. Only the bend directions depend on the depth. The
kept fields (`EdgeFields`: coverage, distances, normals) are now keyed
without it, and hold the bend directions for the last two depths asked
for beside them, so a new depth works out only those (`EdgeFields::bends`).
Two glass layers of one shape at different depths each keep theirs.

The margin a core takes was `reach` + 2 px past the plain one, but the
nearest core pixel can be √2 × `reach` away round a sharp corner, past
what that covers once `reach` is over about 91 px. It is now
1.5 × `reach` + 2 px past the plain margin. The cut-through-the-pane test
gets a 60-unit-deep look (a core 122 px in at 2×).

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

* Glass: splay's doc says splay 0 bends towards the core

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:59:27 +01:00
Trevor BrownandClaude Fable 5.1 cf2dd3bcf8 Merge upstream/main into fix/justify-cjk
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-05 16:50:38 -05:00
Trevor BrownandClaude Fable 5.1 a695f15e4b Merge upstream/main into fix/vector-rounded-fill
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-05 16:50:37 -05:00
857d7089be M3: glass's light is a line on its bevel, and splay turns the bend towards the centre (#1888)
* 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 light is a line on its bevel, and splay turns the bend towards the centre (#1885)

The light is a line on the 1-unit bevel added to the backdrop, as the
reference tool's: fully where the edge faces the light, none below 0.35 of
facing it and 0.6 as much on the opposite side, with a faint gradient across
the curve, 5% darker facing the light and lighter opposite. It was a glow
mixed towards white, as deep as `splay` made it, so `light_reach` goes.

`splay` turns the bend from along the edge's normal towards the way away
from the layer's centre, and no longer widens the light: files with a splay
above 0 look different. The direction to the centre is worked out in the
shape's own frame (`own_frame`, shared with the field cache), so cut renders
match the whole one byte for byte.

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

* M3: cut renders of glass match with splay turning a dispersed bend (#1885)

Review follow-up: the cut-versus-whole test had splay only on looks without
refraction, so no turned bend reached the dispersion. A look with splay 1 and
the default bend and dispersion now renders the same however it is cut. The
parity assessment records the light on the bevel and splay's new meaning.

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>

* M3: glass's light shows over a transparent backdrop, and its flat inside skips the light (#1885)

Review follow-up: the light was added to the backdrop's colour and then
clamped to the backdrop's alpha, so with nothing under the glass (an
export of the pane alone) the line on the bevel vanished, where the old
glow raised the alpha and showed. Now the alpha rises as far as the lit
colour needs (`lit`), and stays the backdrop's wherever the light fits
under it, so renders over an opaque backdrop don't change.

Past `depth` + the bevel the rim light, the gradient and the bend are all
0, so those pixels take one sample of the backdrop and skip the light's
direction, facing and splay work, as the old glow's `reach` early-out did.

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:44:38 +01:00
Trevor BrownandClaude Fable 5.1 b7a7f2891a Merge upstream/main into fix/justify-cjk
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-05 16:41:36 -05: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
fe3567a56b M3: glass bends with the reference tool's thick-lens profile and splits its colours over 8 steps (#1887)
* 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>
2026-11-05 22:27:51 +01: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
Trevor BrownandClaude Fable 5.1 4cd01f0a04 M4: a justify gap is reordered with the line's runs, so a nested embedding gets its gaps at its edges (#1970)
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>
2026-11-04 06:01:39 -05:00
Trevor BrownandClaude Fable 5.1 de881846a8 M8: joining the ends of a path broken open closes its fill loop through the new segment (#1951)
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>
2026-11-04 04:35:37 -05:00