With points selected in more than one edited layer the transform box now
wraps them all, in the page's axes since the layers can be turned
differently. Its corners and edges scale them, outside a corner rotates
them and inside drags them; each layer's points are mapped in its own
coordinates and the whole drag is one undo step. A click inside clears
every layer's points.
Review: after a Save as, closing called request_close, which asked the
question again while another file still had changes. It now carries on
with save_then_close. Save is the default answer: Enter picks it when no
button has the keyboard.
Closing a window with unsaved changes asked "Close the window and lose
them?" with only Close without saving and Cancel. It now asks
Don't Save / Cancel / Save, as PhotoCraft and VectorCraft do. Save saves
each file of the window with unsaved changes and closes; a file without
a path (or saved as .fig) gets Save as first, and closing goes on once
that saved it. A failed save or a cancelled Save as keeps the window
open with its changes. The two changed strings are updated in every
catalog.
With the keyboard in a Design panel field (X, W, a colour…), the
shortcut handler returned before looking at any key, so Cmd/Ctrl+S and
Shift+Cmd/Ctrl+S did nothing: no save, no message, the tab kept its
unsaved dot. Those two now work from a field: the field lets go of the
keyboard, which commits what was typed as clicking away does, and the
save runs the next frame.
Review: " . .name. . " kept inner dots at the edges; trim spaces and dots
in one pass. docs/cli.md and safe_file_name's comment give the 100 and
200 character limits.
Signs such as U+2316 and U+2912 are in no text face. The fallback list
had the macOS and Windows symbol fonts (Apple Symbols, Segoe UI Symbol)
but nothing a Linux install has, so they drew as empty boxes there even
with Noto Sans Symbols 2 or Noto Sans Math installed. Adds Noto Sans
Symbols, Noto Sans Symbols 2 (and the Symbols2 spelling some packages
use) and Noto Sans Math after them.
export into a folder replaced every character outside letters, digits,
'-', '_', space and '.' with '_', so a frame called
"Homepage - desktop (live + verbeteringen)" became
"Homepage - desktop _live _ verbeteringen_.png". The app's
file.exportLayers keeps those names and only replaces what file systems
refuse. The CLI now uses the engine's safe_file_name, and docs/cli.md
says which characters are replaced.
Also adds my own entry to contributors/people.toml.
Like an unset axis, a cleared maximum was written as 0 and could
collapse the layer (CodeRabbit review on #1152).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Checked by importing synthetic files into the incumbent (Drafts > Import,
black-box) and against a "Save local copy" of a synthetic document.
- Images are named and referenced by the SHA-1 of their bytes. With
SHA-256 names, any file with an image failed to import ("Internal error
during import").
- Component sets hold their property definitions, variant properties
included (stateGroupPropertyValueOrders); each variant has its own copy
of each definition (parentPropDefId) and its variantPropSpecs, and its
layers and instances name that copy. Definitions and values also travel
as varValue; text values carry their lines; layers get componentPropRefs
next to the parameter consumption map. Before, sets came in without
variant properties and with their other properties unbound.
- An axis without a maximum size is written as infinite: 0 collapsed
auto layout frames with only a maximum width.
- Compound weights are spelt the way font families name their faces
("SemiBold"; "Semi Bold" in Inter): text in Libre Franklin Semi Bold was
a missing font there and wasn't drawn.
- The kiwi encoder writes infinite floats.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found while testing crates/text/src/layout.rs:1070 (shape_segment calls TextCase::apply_after one char at a time; crates/doc/src/text.rs:124 apply_after)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
node.create, node.set and the other prop appliers map fill/stroke
"none" (either case) and null to an empty fills/strokes list, as
fills: [] already did (undocumented), so an outline-only shape is one
call - agents write "none" for no paint. KEYS and docs/mcp.md
document it; the error names the accepted forms. fill_none_tests
covers create, set, undo, text and the error.
Change to rebuilt the instance from the new variant, and set variable
and set variable mode re-applied bindings, but the player never laid the
result out: no text sizing, no auto layout. A hugging button with a
longer label than its main showed the other variant at the main's width,
its label clipped, and a text bound to a variable kept its old size.
These actions now run the engine's normalize on the player's document
afterwards, the pass that follows every edit on the canvas: text resizes
to what it shows, instances follow and auto layout reflows.
Validated formatting, workspace Clippy, workspace tests excluding the two pre-existing localized startup assertion failures tracked in #1017, layers, assets, WASM and plugins. The startup tests are unchanged in this independent fix.
Found while testing crates/doc/src/text.rs:138 (TextCase::apply_after; is_word_char at :153 counts ' and U+2019 as word characters)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Found while testing crates/svg/src/lib.rs:153 (esc, used for tspan text at :598, :608 and :613)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keep single-quoted attribute delimiters, render text placeholders as text (not quoted strings), skip a > inside an attribute value when placing v-if (and insert before a self-closing slash), give slot content a per-slot class prefix so repeated layer names stay distinct, use a binding name no generated prop can shadow, and add Vue to the five translations that still listed only the older languages.
Vue connected templates keep a string's JS quotes inside bound attributes, so `:title={title}` becomes `:title=""Buy""` rather than a bare identifier; static attributes take plain text. Nested instances no longer lose a boolean `v-if` guard, instance swaps render through `<component :is>`, slot names stay distinct when their idents lowercase alike, slot content CSS reaches the parent stylesheet, and text defaults escape `</script>` in the script block. The five locale catalogs the Vue key rename missed (bn, fi, ka, nl, pl) are updated, and the docs list Leptos/Vue consistently.
A file or library saved before "modes" was an override group has a mode
chosen on a layer inside an instance without its marker, so the next
rebuild or library import still put the main's mode back.
Copies follow their main layer's modes when the main changes them, so a
layer inside an instance on other modes chose them itself.
mark_layer_modes marks those when a file opens and when a published
library is read from a file or a server. Layers on their main's modes
and top-level instances stay unmarked.
A variable mode chosen on a layer inside an instance (the icon inside a
Close button, recoloured through an icon colour collection) wasn't an
override group. A full rebuild of the instance starts each layer over
from its main and carries only override groups, so the mode fell back
to the main's. Library import always rebuilds, as do main edits such as
a new auto layout direction, so a modal's recoloured close icon came
into other files in its default colour.
"modes" is now an override group: setting a mode inside an instance
records it, rebuilds carry it, rebasing a nested instance finds it, the
component panel lists it, and resetting it brings back the main's mode.
A top-level instance's own modes stay its own and record no override,
like its resizing.
`utc_date` (collab) and `date` (ui-egui) took `u64` seconds, which the old
hand-rolled civil-from-days code converted for the whole `u64` range. The
chrono rewrite used `from_timestamp(..).unwrap_or_default()`, so any instant
past chrono's own year limit (262143) silently became the epoch — changing
existing output. `u64::MAX` seconds, for example, used to read
`584554051223-11-09 07:00 UTC` / `Nov 9, 584554051223`, not 1970.
Fix: add a `utc_parts(days, sod, nanos)` helper per crate that moves the
instant into chrono's range by whole 400-year Gregorian cycles (146_097 days),
lets chrono do the month/day/time, and restores the real year afterwards.
chrono stays the only calendar logic; the cycle count just shifts the year, and
every `u64` day count fits in `i64` so the conversion is always exact (no epoch
fallback). Split seconds with `u64` arithmetic first to avoid the `i64` seconds
overflow near `u64::MAX`, matching the old code.
Tests now pin the old output instead of the epoch:
- collab utc_date(u64::MAX) == "584554051223-11-09 07:00 UTC"
(and utc_date(253_402_300_800) == "10000-01-01 00:00 UTC", already pinned)
- ui-egui date(253_402_300_800) == "Jan 1, 10000"
date(u64::MAX) == "Nov 9, 584554051223"
chrono's %Y writes a '+' before years past 9999, whereas the old hand-rolled
{y:04} did not. utc_date now writes the year with {:04} to keep the exact old
output. from_timestamp already reads a clock past chrono's range as the epoch
rather than panicking. Tests cover the 5-digit-year and out-of-range cases.
Replaces the two hand-rolled Howard Hinnant days-from-civil copies with chrono
as a workspace dependency:
- crates/collab/src/message.rs: utc_date (YYYY-MM-DD HH:MM UTC)
- crates/ui-egui/src/home_recents.rs: date (Mon D, YYYY, UTC; keeps the
translated month names and trf formatting)
Output is unchanged. Neither function had a test, so each gains one pinning the
exact strings the old code produced (leap days included). chrono uses only std,
so wasm stays safe. AGENTS.md/CLAUDE.md now say time goes through chrono.
Follow-up to storytold/cadcraft#1785
A Button main's override on its icon instance's glyph (a red fill) was
lost when an instance swapped the icon: the outer rebuild re-copies the
glyph from the main's layer with no overrides of its own, so the nested
rebuild matched it by name and carried nothing. Before that rebuild, each
old layer of the swapped instance now takes the overrides held along the
source chain it had before this rebuild, through layers inside instances
in the mains that place the swapped instance. Geometry, slot content and
groups the layer overrides itself are left out. Matched by name, the
values carry onto the new main's layer as the instance's own: inherited
at the swap, frozen after, and back to the icon main's value on reset.
An icon main's own overrides stay with it.
stroke_area used rect_side_strokes only for square corners; a rounded
rect-like layer with individual weights fell through to a uniform ring at
stroke.weight (canvas, SVG, PDF, shadows, shape builder, outline stroke).
pathops::rounded_side_strokes now draws them as CSS does: the outer edge is
the rounded rect (the fill outline for inside strokes), the inner edge is
inset by each side's weight with elliptical corners max(0, R - weight), so
border-l-4 rounded-r-lg is a straight bar and a weight that differs across
a rounded corner tapers through it. Center and outside grow the outer box
and each non-zero radius by half or all of the weight (CSS has only
inside). The inner edge runs against the outer one, so the ring is a ring
under non-zero too; where it could leave the outer edge (radii shrunk to
fit a narrow inner box, smoothed outer corners) it is cut out instead.
Found while testing crates/doc/src/proto_expr.rs:172 (display writes `not {x}` with a bare var_ref) with crates/doc/src/proto_expr_parse.rs:262 (not() checks variable names before the `not` keyword)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>