Edit ▸ Set default properties put the selected layer's properties into the Paste properties
clipboard and did nothing else: a rectangle drawn afterwards was still the default grey, and what
Copy properties had copied was replaced.
The defaults are now kept apart (`Session::default_props`: one set for shapes, set from any layer
but text, and one for text). Rectangles, ellipses, polygons and stars drawn or clicked with their
tools take the shape defaults: fills, strokes and their settings, effects, opacity, blend and
corners (only where the shape has corners), through `apply_copied_props`, the rules Paste
properties already used (now shared). New text from the Text tool takes a text layer's fills and
font and is laid out at that size. Frames, sections, slices, lines, arrows, vectors and layers made
by commands keep their own look; the help center is silent on those. `edit.setDefaultProperties`
answers `{for: shapes|text, properties}`.
Tests: `default_props_tests::new_shapes_drawn_with_the_tools_take_the_default_properties`,
`setting_defaults_leaves_the_copied_properties_alone`,
`new_text_takes_a_text_layers_defaults_and_shapes_keep_theirs`. Docs: ui-parity.md, ROADMAP.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LvG6E2LxvJ6MuFmiLA6Zqm
* 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>
* 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>
* 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>
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.
* M3: library analytics list how each style and variable is used
A file reports how many of its layers use each of a team library's styles and
variables, and Show usage lists them by a Type choice with their layers and
files. Layers in instances count for their component. Also ignores plugins/target.
* M3: the style analytics test waits for the instance report too
A widget description can have a line, 100 long and black and 1 thick unless told
otherwise, and an SVG that comes in as locked layers, scaled to fit a size and
within the widget's 256 layers. The Counter example uses both.
Both reports were checked by running them and are already fixed on main. Pins a rounded rectangle or frame stroking only the side asked for in every alignment, and SVG font families and ids leaving out characters XML forbids.
The desktop shell swallows a plain Cmd/Ctrl+V, press and release, and sends an
image paste event; FigCraft ignored it and waited for a release that no longer
comes. The browser's paste event gives eframe only text, so the page now reads
the image from it, under the same limits.
Bound variables with an iOS or Android code syntax are named in generated SwiftUI and Compose in place of their value: paints, stroke weight, sizes, corner radius, gaps, padding, opacity and text. A variable with none for the platform keeps the value.
Choosing Image shows a checkered placeholder and Upload from computer instead of a dialog at once; an image paint shows its asset preview. A file dropped on the open picker replaces the image, keeping the fill mode, crop, position and adjustments (image.replace, one undo step; paths capped as image.place's). The browser's file picker reaches the tab too. Also fixes a race in library_detach_tests.
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.
A request line can carry timeoutMs beside id and method, capped at 600,000 ms; without it the wait stays 60 s and older apps ignore the field. figcraft-cli mcp --attach PORT --timeout SECONDS sends it with every request and reads the answers that much longer. A timeout's message says how long the request waited.
* M11: a widget opens its plugin's panel (plugin API 5)
Gap 6: a widget callback's ui_show opens the plugin's panel and keeps the module loaded; the panel's callbacks get the widget's id and state and redraw it in the same undo step. Older plugins' ui_show from a widget still opens nothing. The Counter example sets its count from a panel.
* M11: open_panel takes its opener as one value
Gap 14: variable.extendCollection and Extend collection on a collection's row in the Variables editor. Extensions follow edits of the collection they extend at once (normalising keeps them in step whenever variables change), extensions extend again, and a loop of extensions from a hostile file comes apart into plain collections.
* M4: the eyedropper with Cmd/Ctrl+Shift makes a colour style or variable of what it picks
* M4: the Create from color window's collection picker is its own function
The UI starts a request through its gate (a new Running state) just before running it and never runs one the transport already cancelled. At the timeout the reply says it was dropped and not applied, or that it is still running and will take effect; a reply that just made it is delivered. Only screenshots and input blame the window.
* 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
* 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.
* 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)
Main merged #1652 (the Bosnian catalog) and added more strings; ROADMAP keeps main's rows with the count updated, and bs.toml translates the two new strings (a pasted table's size limit, connector labels without a snapshot), so it covers all 1,906 UI strings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Tayw8nmxoLLmKv13jK93z
proto.linkNotice from the Actions palette, the control channel or MCP changed the preference in memory only until the app next saved its preferences. It now sets Session::prefs_unsaved, which the app saves at the end of the frame like installed plugins and live-session tokens; the notice's own Don't show this again goes the same way. Gap 15.
The bar over a selected connector adds start and end point menus with every line end, a line style menu (solid or dashed, weights 1 to 6) and a colour wheel opening the colour picker, beside the board colours and line type; every control names itself on hover. UI only, through board.setStyle and the stroke's dashes and weight.
Four strings main added since the last merge (pasted design layers and
their missing images, image fills and shared styles not in the file) get
Bosnian translations, so the catalog covers all 1,905 UI strings again;
the counts in ROADMAP.md and the localization parity doc follow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Tayw8nmxoLLmKv13jK93z