* docs: plan drawing workspace UI implementation
* assets: add drawing icon drafts for owner review
* feat(ui): add drawing workspace scaffold with shared ribbon and docked settings
Expose planned Drawing tools without introducing drawing documents, paper rendering or CAD behavior. Reuse existing source BOM, properties and flat-pattern services; retain placeholders for unapproved icon drafts.
Validation: full UI, kernel, icon, clippy, wasm and layout checks passed. Full cargo xtask ci reports three geometry failures that also reproduce on unchanged drawings base 3b982ca: counterbored chamfered-disc ring cut, robustness seed 36 and seed 71. No geometry source or golden fixtures changed.
* Preserve default icon palette in Drawing scaffold tests
* Preserve Drawing providers and complete scaffold translations
reference_margin_mm becomes reference_shown_percent, 0 to 100. At 0 the
boundary is the model's box, as before, and no reference part is measured.
At 100 it holds the model and all the view's reference parts. Between, each
side moves on its own, so a part on one side grows only that side.
The model's middle now places a view, not the middle of what it shows, so a
view stays lined up with its projected views as its box grows. With nothing
grown the two are the same point and the picture is the same.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing views made by command: drawing.view.base and drawing.view.projected (#4816)
drawing.view.base {sheet, view?, scale?, position_mm?, the view settings?} adds a base view of the
drawing's design to a sheet. Without a scale it takes the largest ISO 5455 preset at which it lies
inside the sheet's drawing space, centred where it goes, its margin included, as the snapshot fits
it; that scale is saved as the view's own (1:1 with nothing to measure).
drawing.view.projected {parent, side, place?} adds a view projected from a saved view, on its
parent's sheet, following its parent's scale and display settings. Without a place it is lined up
with its parent, its boundary 20 mm past the parent's (DEFAULT_GAP_MM, until a drawing style
gives it).
Each is one undo step through edit_drawing, refused with nothing changed when the record's rules
refuse it; the reply gives the view, its sheet, its scale, its boundary and, when it leaves the
sheet's drawing space, itself as outside (it isn't moved back in). Unknown fields are refused.
One rule, one place: whether a view leaves its drawing space is drawing_view::leaves, and the sheet
of an id is drawing_view::sheet, used by outside() and both commands.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing views made by command: a build fix, and each view one undo step, tested (#4816)
From the review of the view create commands:
- drawing.view.base measures the shown bodies (`measure` takes them, not the session), so it builds.
- The nothing-to-measure case is a design with one sketch and no bodies, as the other view tests
make it: drawing.create refuses an empty design.
- The schema test gives drawing.view.base its own key list (it takes no snapshot-only keys).
- Every view a test makes is checked to be one undo step, not only the first: a second step made
under the first (a nested edit) still let one undo take the view away.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Paper scene: one walk onto a writer's number grid, for every writer (#4890, C4)
The SVG writer kept its own number grid: each coordinate whole nanometres, every joint written once,
a closed path ending exactly on its first point, slivers merged into the next piece and a path's
last piece kept. PDF, PNG and the Drawing workspace's canvas need the same rules, so they move into
`geom::paper::grid`, and the SVG writer only formats what the walk gives it.
- `Grid`: `per_mm` steps a millimetre. `Grid::NANOMETRE` (six decimals of a millimetre) is SVG's.
`Grid::new` takes 1 to 1e9; a power of two keeps every number exact as a binary float, which a
writer of 32-bit numbers such as PDF can use.
- `Prepared::on_grid(grid) -> OnGrid`: each group's items as paths of `Piece`s (lines, arcs of at
most a quarter turn, cubics, parabolas), y up as in the scene, a closed path's closing line left
to the writer's close, and a report of widths snapped and pieces merged. Slivers are measured in
the grid's own steps, so a coarser grid merges more.
- The SVG writer flips y on the whole numbers it's given and writes the same bytes as before.
`SvgReport` is the grid's report.
- Ellipses, hyperbolas and Béziers above degree 3 are still refused (A3b), now by the walk.
DXF stays on its own number rule for now: it writes exact entities (ELLIPSE, SPLINE) and is being
reworked under #5318 to #5321; whether its polylines take a grid is decided there.
Tests: every SVG test unchanged, bytes included; the grid's rounding and its bounds; the A3 border
on the grid (y up, closing line left out, fill loops closed); slivers in the grid's own steps, with
the merge boundary both ways (10 and 11 steps, a 6-8 and a 7-8 step); an arc's quarters with their
radius and turn in steps; a width under half a step made one step; an ellipse refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Paper scene: a sheet PNG at a chosen dpi and colour (#4890 A4)
A new writer, io::paper::paper_png, that reads the shared walk on the
nanometre grid, the one the SVG writer uses, so a PNG draws the same joints,
closures and merges. Neither the walk nor the SVG writer changes.
- Curves are flattened within 1/8 px by geom::paper::flatten, which turns
each walked piece back into its Seg2 (an arc's centre from its ends, radius
and direction) and takes Seg2::chords' steps. chords is the bound
Seg2::polyline already used (the chord's bulge for arcs, Wang's bound for
the rest), moved out of polyline unchanged so both read one rule.
polyline itself doesn't fit: it floors an arc's tolerance at a millionth of
its radius and caps a piece at 4096 steps, coarser than asked without a
word (0.1 mm, 9 px, on a 100 m arc at 2400 dpi). Over 2^20 points a path,
or 2^24 a sheet, is refused instead. tiny-skia only gets lines: its own
flattening stops at 64 lines a curve, more than a pixel off on a large arc
at a high dpi.
- 25 to 2400 dpi (300 by default). 8-bit greyscale (the default), 1-bit
black and white (no anti-aliasing, every line at least a pixel wide), 24-bit
colour, or 32-bit colour with transparent paper.
- The file records its dpi (pHYs) and sRGB, so it prints at the sheet's size.
- Each line is stroked once, then the sheet is drawn in bands of 16 MiB of
pixels streamed into the file, so memory doesn't grow with the sheet. Each
band also draws a row above and below that isn't written: tiny-skia starts
an edge a little differently where a clip cuts it (one subsample, 16
levels, in a few pixels), so no written row sits on a cut and the bytes are
the same whatever the band.
- A line under a pixel wide (in greyscale and colour) is drawn as tiny-skia's
hairline, its coverage scaled by its width: its stroked outline would fall
between the rows the rasteriser samples and vanish.
Tests: the pixel size and pHYs of A4 at 300 and 25 dpi; a line's and a fill's
pixels at 254 dpi (butt ends, the fill's hole); each colour's PNG type, ink
and paper; a hairline between two rows' centres that is light grey in
greyscale and stays one pixel in black and white, and a bar a fifth of a pixel
wide that black and white inks by its centre; a closed outline joined at its
start; a 40 mm circle round to the pixel at 600 dpi; the same bytes drawn 1, 3
or 7 rows at a time or all at once, and again; the refusals; the flattener's
bounds and counts, and its point cap at the exact boundary; the scene-JSON
fuzz through the PNG writer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Geom: the pixel grid of a paper rectangle (#5624, W17b S1)
One rule for where a picture's pixels lie on paper, shared by the picture renderer and the writers
that place it: anchored at the rectangle's lower-left corner, rounded outward (W = ceil(w / p - 1e-9)
with p = 25.4 / dpi), so the overhang is at the top and the right; rows run top-down. At most
65,536 px a side; nothing caps the total, as pictures are drawn in tiles and strips.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Paper scene: pictures, and a sheet's pixel grid and windows (#5624, W17b S3; #4890 A9)
A picture on a sheet is `Item::Picture`: the rectangle it sits in, the name of its pixels
(`source`, opaque as a group's object id is) and its opacity. The pixels stay out of the scene: a
writer asks a `Pictures` provider for them on the grid it draws on. A PNG at 1200 dpi then gets a
shaded view drawn at 1200 dpi, not one stretched, and a scene stays small, plain data.
The pixel grid gains the two rules a sheet needs, beside the rectangle's own grid:
- `PixelGrid::sheet`: the whole sheet's grid, anchored at its top-left corner, as an image of
the sheet is drawn.
- `PixelGrid::window`: the columns and rows a rectangle meets; a side on a pixel edge takes no
sliver beyond it. A picture drawn into its window of the sheet grid is placed by copying whole
pixels.
`Picture::grid` is a picture's own grid at a dpi, for SVG and PDF to embed. `Scene::validate`
counts a picture as one record. It refuses a rectangle outside the coordinate bound or without
area, and an opacity that isn't above 0 and at most 1. `Aabb2` is read and written as
`{min, max}`.
On the walk (C4), which S3 follows here: `GridItem::Picture` passes a picture through with its rectangle in
millimetres, since the pixel grid places its pixels, and the SVG and PNG writers refuse it ("not supported yet:
pictures (A9)") until their own slices. `GridItem`, `GridGroup` and `OnGrid` are `PartialEq` only, as a
picture holds numbers that aren't `Eq`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Paper scene: pictures in a sheet PNG (#5624, W17b S5)
The sheet PNG writer draws the scene's pictures (S3's Item::Picture) where
the scene orders them, between its lines and fills, from a Pictures provider.
- The PNG is now the sheet's pixel grid (PixelGrid::sheet, S1): anchored at
the sheet's top-left corner and rounded outward, so nothing is clipped.
210 mm at 300 dpi is 2481 px (it was 2480, rounded to the nearest). A side
over 65,536 px is refused and the refusal names the limit ("a sheet PNG
side of ... px at ... dpi is over the limit of 65536 px"); an A0 sheet
fits up to about 1400 dpi.
- A picture fills exactly its window of that grid, so it registers with the
lines drawn over the same rectangle. Each band asks the provider for just
its own rows, as straight-alpha sRGB RGBA, and the bytes are checked: too
few or too many, or the provider's error, refuse the PNG with the picture's
source named. paper::NoPictures refuses every picture.
- Pictures are asked for at most MAX_PICTURE_DPI (1200): above it, on the
grid k = ceil(dpi / 1200) times coarser, whose pixels each fill exactly
k x k of the sheet's (both grids share the top-left anchor), so a 2400 dpi
export doesn't ask a renderer for four times the pixels.
- Source-over in sRGB, the picture's alpha times its opacity; transparent
paper keeps the alpha. In black and white each picture pixel is dithered
with an 8 x 8 ordered (Bayer) matrix at its place on the sheet, so bands
don't show; lines stay solid. PngReport says whether anything was
dithered.
- In the writer a picture is one more shape a band draws, beside A4's
outlines, fills and hairlines; the walk's picture arm passes pictures
through, and the SVG writer still refuses them (A9).
Tests: a picture's exact window and its register with a fill over the same
rectangle, also on a sheet 100.8 px high (the grid anchored at its top);
pictures keep their place in the paint order; opacity scales the alpha (on
white and transparent paper, and the picture's own half alpha); above 1200
dpi a coarser picture's pixels are each copied 2 x 2; black and white
dithers half of mid grey and keeps a line over it solid, whatever the band;
a provider's short rows, its error and no provider are refused, and a
picture off the sheet asks for nothing; the bands test draws three pictures
1, 3 or 7 rows at a time, the same bytes and each picture row asked for
once; the sheet sizes on the outward grid and the 65,536 px refusal; the PNG
fuzz with a picture.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Paper scene: pictures in a sheet SVG (#5624, W17b S5)
A picture is an <image> holding a PNG as a data URI, on the picture's own
pixel grid (Picture::grid) at the picture resolution the caller gives,
25 to 1200 dpi: paper_svg(scene, pictures, picture_dpi), the same shape as
paper_png. Only its window over the sheet is embedded and asked for, so it
never costs more pixels than the sheet holds; one off the sheet is left out.
Its x, y, width and height are the grid's own pixel edges put on the
nanometre grid every line is on, so a line on its edge has its number.
Opacity below 1 is written, in millionths as lengths are.
The PNG writer gives the embed its encoder settings (sRGB, the dpi
recorded) and its provider check, now one function each, and streams the
picture a band of rows at a time, refused once it passes the room left
under the file's 64 MiB.
Tests: the 1182 x 591 px grid at 300 dpi and its edges, a line on the
edge, paint order and opacity, the crop and the picture off the sheet, the
bands, and each refusal; the SVG fuzz case gains a picture.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Paper scene: one walk onto a writer's number grid, for every writer (#4890, C4)
The SVG writer kept its own number grid: each coordinate whole nanometres, every joint written once,
a closed path ending exactly on its first point, slivers merged into the next piece and a path's
last piece kept. PDF, PNG and the Drawing workspace's canvas need the same rules, so they move into
`geom::paper::grid`, and the SVG writer only formats what the walk gives it.
- `Grid`: `per_mm` steps a millimetre. `Grid::NANOMETRE` (six decimals of a millimetre) is SVG's.
`Grid::new` takes 1 to 1e9; a power of two keeps every number exact as a binary float, which a
writer of 32-bit numbers such as PDF can use.
- `Prepared::on_grid(grid) -> OnGrid`: each group's items as paths of `Piece`s (lines, arcs of at
most a quarter turn, cubics, parabolas), y up as in the scene, a closed path's closing line left
to the writer's close, and a report of widths snapped and pieces merged. Slivers are measured in
the grid's own steps, so a coarser grid merges more.
- The SVG writer flips y on the whole numbers it's given and writes the same bytes as before.
`SvgReport` is the grid's report.
- Ellipses, hyperbolas and Béziers above degree 3 are still refused (A3b), now by the walk.
DXF stays on its own number rule for now: it writes exact entities (ELLIPSE, SPLINE) and is being
reworked under #5318 to #5321; whether its polylines take a grid is decided there.
Tests: every SVG test unchanged, bytes included; the grid's rounding and its bounds; the A3 border
on the grid (y up, closing line left out, fill loops closed); slivers in the grid's own steps, with
the merge boundary both ways (10 and 11 steps, a 6-8 and a 7-8 step); an arc's quarters with their
radius and turn in steps; a width under half a step made one step; an ellipse refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Paper scene: a sheet PNG at a chosen dpi and colour (#4890 A4)
A new writer, io::paper::paper_png, that reads the shared walk on the
nanometre grid, the one the SVG writer uses, so a PNG draws the same joints,
closures and merges. Neither the walk nor the SVG writer changes.
- Curves are flattened within 1/8 px by geom::paper::flatten, which turns
each walked piece back into its Seg2 (an arc's centre from its ends, radius
and direction) and takes Seg2::chords' steps. chords is the bound
Seg2::polyline already used (the chord's bulge for arcs, Wang's bound for
the rest), moved out of polyline unchanged so both read one rule.
polyline itself doesn't fit: it floors an arc's tolerance at a millionth of
its radius and caps a piece at 4096 steps, coarser than asked without a
word (0.1 mm, 9 px, on a 100 m arc at 2400 dpi). Over 2^20 points a path,
or 2^24 a sheet, is refused instead. tiny-skia only gets lines: its own
flattening stops at 64 lines a curve, more than a pixel off on a large arc
at a high dpi.
- 25 to 2400 dpi (300 by default). 8-bit greyscale (the default), 1-bit
black and white (no anti-aliasing, every line at least a pixel wide), 24-bit
colour, or 32-bit colour with transparent paper.
- The file records its dpi (pHYs) and sRGB, so it prints at the sheet's size.
- Each line is stroked once, then the sheet is drawn in bands of 16 MiB of
pixels streamed into the file, so memory doesn't grow with the sheet. Each
band also draws a row above and below that isn't written: tiny-skia starts
an edge a little differently where a clip cuts it (one subsample, 16
levels, in a few pixels), so no written row sits on a cut and the bytes are
the same whatever the band.
- A line under a pixel wide (in greyscale and colour) is drawn as tiny-skia's
hairline, its coverage scaled by its width: its stroked outline would fall
between the rows the rasteriser samples and vanish.
Tests: the pixel size and pHYs of A4 at 300 and 25 dpi; a line's and a fill's
pixels at 254 dpi (butt ends, the fill's hole); each colour's PNG type, ink
and paper; a hairline between two rows' centres that is light grey in
greyscale and stays one pixel in black and white, and a bar a fifth of a pixel
wide that black and white inks by its centre; a closed outline joined at its
start; a 40 mm circle round to the pixel at 600 dpi; the same bytes drawn 1, 3
or 7 rows at a time or all at once, and again; the refusals; the flattener's
bounds and counts, and its point cap at the exact boundary; the scene-JSON
fuzz through the PNG writer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Geom: the pixel grid of a paper rectangle (#5624, W17b S1)
One rule for where a picture's pixels lie on paper, shared by the picture renderer and the writers
that place it: anchored at the rectangle's lower-left corner, rounded outward (W = ceil(w / p - 1e-9)
with p = 25.4 / dpi), so the overhang is at the top and the right; rows run top-down. At most
65,536 px a side; nothing caps the total, as pictures are drawn in tiles and strips.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Paper scene: pictures, and a sheet's pixel grid and windows (#5624, W17b S3; #4890 A9)
A picture on a sheet is `Item::Picture`: the rectangle it sits in, the name of its pixels
(`source`, opaque as a group's object id is) and its opacity. The pixels stay out of the scene: a
writer asks a `Pictures` provider for them on the grid it draws on. A PNG at 1200 dpi then gets a
shaded view drawn at 1200 dpi, not one stretched, and a scene stays small, plain data.
The pixel grid gains the two rules a sheet needs, beside the rectangle's own grid:
- `PixelGrid::sheet`: the whole sheet's grid, anchored at its top-left corner, as an image of
the sheet is drawn.
- `PixelGrid::window`: the columns and rows a rectangle meets; a side on a pixel edge takes no
sliver beyond it. A picture drawn into its window of the sheet grid is placed by copying whole
pixels.
`Picture::grid` is a picture's own grid at a dpi, for SVG and PDF to embed. `Scene::validate`
counts a picture as one record. It refuses a rectangle outside the coordinate bound or without
area, and an opacity that isn't above 0 and at most 1. `Aabb2` is read and written as
`{min, max}`.
On the walk (C4), which S3 follows here: `GridItem::Picture` passes a picture through with its rectangle in
millimetres, since the pixel grid places its pixels, and the SVG and PNG writers refuse it ("not supported yet:
pictures (A9)") until their own slices. `GridItem`, `GridGroup` and `OnGrid` are `PartialEq` only, as a
picture holds numbers that aren't `Eq`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Paper scene: pictures in a sheet PNG (#5624, W17b S5)
The sheet PNG writer draws the scene's pictures (S3's Item::Picture) where
the scene orders them, between its lines and fills, from a Pictures provider.
- The PNG is now the sheet's pixel grid (PixelGrid::sheet, S1): anchored at
the sheet's top-left corner and rounded outward, so nothing is clipped.
210 mm at 300 dpi is 2481 px (it was 2480, rounded to the nearest). A side
over 65,536 px is refused and the refusal names the limit ("a sheet PNG
side of ... px at ... dpi is over the limit of 65536 px"); an A0 sheet
fits up to about 1400 dpi.
- A picture fills exactly its window of that grid, so it registers with the
lines drawn over the same rectangle. Each band asks the provider for just
its own rows, as straight-alpha sRGB RGBA, and the bytes are checked: too
few or too many, or the provider's error, refuse the PNG with the picture's
source named. paper::NoPictures refuses every picture.
- Pictures are asked for at most MAX_PICTURE_DPI (1200): above it, on the
grid k = ceil(dpi / 1200) times coarser, whose pixels each fill exactly
k x k of the sheet's (both grids share the top-left anchor), so a 2400 dpi
export doesn't ask a renderer for four times the pixels.
- Source-over in sRGB, the picture's alpha times its opacity; transparent
paper keeps the alpha. In black and white each picture pixel is dithered
with an 8 x 8 ordered (Bayer) matrix at its place on the sheet, so bands
don't show; lines stay solid. PngReport says whether anything was
dithered.
- In the writer a picture is one more shape a band draws, beside A4's
outlines, fills and hairlines; the walk's picture arm passes pictures
through, and the SVG writer still refuses them (A9).
Tests: a picture's exact window and its register with a fill over the same
rectangle, also on a sheet 100.8 px high (the grid anchored at its top);
pictures keep their place in the paint order; opacity scales the alpha (on
white and transparent paper, and the picture's own half alpha); above 1200
dpi a coarser picture's pixels are each copied 2 x 2; black and white
dithers half of mid grey and keeps a line over it solid, whatever the band;
a provider's short rows, its error and no provider are refused, and a
picture off the sheet asks for nothing; the bands test draws three pictures
1, 3 or 7 rows at a time, the same bytes and each picture row asked for
once; the sheet sizes on the outward grid and the 65,536 px refusal; the PNG
fuzz with a picture.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Paper scene: pictures in a sheet SVG (#5624, W17b S5)
A picture is an <image> holding a PNG as a data URI, on the picture's own
pixel grid (Picture::grid) at the picture resolution the caller gives,
25 to 1200 dpi: paper_svg(scene, pictures, picture_dpi), the same shape as
paper_png. Only its window over the sheet is embedded and asked for, so it
never costs more pixels than the sheet holds; one off the sheet is left out.
Its x, y, width and height are the grid's own pixel edges put on the
nanometre grid every line is on, so a line on its edge has its number.
Opacity below 1 is written, in millionths as lengths are.
The PNG writer gives the embed its encoder settings (sRGB, the dpi
recorded) and its provider check, now one function each, and streams the
picture a band of rows at a time, refused once it passes the room left
under the file's 64 MiB.
Tests: the 1182 x 591 px grid at 300 dpi and its edges, a line on the
edge, paint order and opacity, the crop and the picture off the sheet, the
bands, and each refusal; the SVG fuzz case gains a picture.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing views: shaded snapshots and their pictures' parts (#5624, W17b S4)
drawing.view.snapshot takes `shaded` as a display or reference style and,
when a picture is drawn, says which: picture {pixels, dpi, rect_mm}, the
boundary's pixel grid at the sheet's dpi or 1200, the lower, since lines
may export finer. It renders nothing. A shaded part's own lines are left
out of the reply, as its picture draws it; it still hides what lies behind
it. shaded_visible and shaded_visible_hidden stay refused until the view's
exact lines (W06), and shaded with `scene` (and on a sheet) until the
paper scene draws pictures.
- engine::drawing_picture: the pictures a view draws, bottom first, and how
each body takes part in each (painted, depth only, left out). The model
always hides reference parts behind it; reference parts hide the model
only when reference_hides_model is on; shaded reference parts that don't
get a picture of their own under the model's, so they never cover it.
- product_scene::extract_where: the scene for the bodies a caller picks;
extract keeps the browser's eye, a drawing draws every body.
Tests: every style and hide case, reference parts through a subassembly
and a part, the picture's size at 300, 1200 and 2400 dpi, a view with
nothing shaded, the hidden body in the filtered scene, and which lines a
shaded model or shaded reference parts leave.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
`Item::Text` is a run of placed glyphs in one font, with its logical text and the clusters that
tie the two together, and `Scene.fonts` the table of the fonts the runs use (as posted on #4890,
with each glyph's `hmtx` advance). `Scene::validate` holds them to the posted rules. Writers never
parse a font.
- A run names its font by the font's id (a hash of its face's bytes and index), so groups laid
out apart name a font alike whichever machine's font it is; `Scene::add_fonts` joins their
tables at assembly, kept in order of id, so the same input gives the same bytes.
- A font's bytes never go through serde: a scene read from JSON is refused until they come from
the font of the id it names.
- `TextRun::outlines` places the glyph outlines on the sheet, for the writers to fill nonzero.
The SVG writer refuses text by name until B2. `Scene::new` builds a scene without text, so the
next field doesn't touch every caller again.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Paper scene: text runs and their fonts, as records with their rules (#4890 B1a)
`Item::Text` is a run of placed glyphs in one font, with its logical text and the clusters that
tie the two together, and `Scene.fonts` the table of the fonts the runs use (as posted on #4890,
with each glyph's `hmtx` advance). `Scene::validate` holds them to the posted rules. Writers never
parse a font.
- A run names its font by the font's id (a hash of its face's bytes and index), so groups laid
out apart name a font alike whichever machine's font it is; `Scene::add_fonts` joins their
tables at assembly, kept in order of id, so the same input gives the same bytes.
- A font's bytes never go through serde: a scene read from JSON is refused until they come from
the font of the id it names.
- `TextRun::outlines` places the glyph outlines on the sheet, for the writers to fill nonzero.
The SVG writer refuses text by name until B2. `Scene::new` builds a scene without text, so the
next field doesn't touch every caller again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Paper text: a text laid out by sketch's rule, as the scene's runs and their drawing font (#4890 B1b)
`paper_text` lays a text out with `TextMetrics`, the rule sketch text draws and measures with, and
returns the paper scene's runs of placed glyphs (B1a) with the font they are set in, holding the
glyphs they place: each one's `hmtx` advance and its outline in font units.
- Drawings are set in bundled fonts only (`DRAWING_FONTS`: Liberation Sans for now). A font is
admitted once: a static face with a PostScript name, a cap height and a `post` table, its stem
width measured across its `l`, and its id hashed then. A run names its font by that id.
- A character the font has no glyph for is refused by name; bold is refused; an italic leans as
sketch text's made-up italic does; text on paper follows no path.
- Lines break a pitch apart and align as sketch text's do. A line with right-to-left characters
is a run per direction, shaped, with clusters from the shaper; any other is one run, a
character to a glyph.
`TextMetrics` keeps one layout: it now reports each glyph's cluster and the line's runs, and the
glyph outline walk gives loops of `Seg2`, which sketch text turns into the geometry it drew before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Paper text: the fonts a text uses, each once (#4890 F0)
`paper_text` returns its runs with the list of fonts they use, each once in order of id (so the
same text gives the same list) and each holding exactly the glyphs its runs place, where it
returned one font. Still one drawing font today; the list is the shape a text set in several
fonts (a Latin, Arabic or CJK line) needs, and `Scene::add_fonts` already takes it as it is.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Reference parts in a drawing view's lines (V5a, #5866)
A body placed through an occurrence that counts as reference is a reference
part: one rule, Document::reference_occurrences. Its lines say so in replies.
With reference_hides_model off (the default) the model's lines skip reference
parts at the one occlusion test, and reference lines test against everything.
The two roles merge apart, so a reference line never takes a model line's
place on one stretch. Extents come from the model parts when there are any. A
sketch curve is the model's: a reference part hides it only as it hides the model.
One thing the plan didn't name: reference lines stop at the view's boundary
(extents and reference margin) when model parts are there too, which is what
the margin is for; a part much bigger than the model would otherwise run off
the sheet.
Hidden reference lines in a scene are refused until dashed lines (V7b), as the
model's are.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Reference parts on paper: thin, under the model's lines (V5b, #5866)
Saved views and the snapshot's scene alike, 0.25 mm until the drawing styles
slice sets widths by kind of line.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Reference parts: tests the planted faults asked for (#5866 V5)
A reference line against a model line on one stretch, and against another reference part; one part
placed through a reference assembly and on its own; a dimension on a reference part's edge; undoing
and redoing a change of type, set on the occurrence or its component; and a saved view's setting.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* A test reads any other paper item as no stroke (#5866 V5)
Naming Fill alone would stop compiling when #7590 adds a kind of item, with
no conflict for git to show.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A drawing's fixed unit may be any length unit of the expression unit
table (expr::UNITS: um, inch, yd, mil and the rest besides a design's
five), but the display functions knew only the five and showed the
others' measurements as millimetres. unit_scale now reads the same
table, so the two can't drift apart again; the five keep exactly the
scales they had (1 over 10, 1000, 25.4 and 304.8). The inch rule (a
fourth digit when it means something) follows an inch's size, so
`inch` reads as `in` does.
A design's own units are only the five, so nothing it shows changes.
The test that pinned `inch` to millimetres becomes one that checks
every length unit of the table shows its own numbers; angle names
(`deg`, `rad`) and other text keep the millimetre fallback.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing dimensions: a hole's or an arc's centre as a point to measure from (#4896, W20c)
A linear dimension's point can be the centre of a circle or an arc, saved as its one edge's name
with `centre`. It is found again as construction geometry finds a round edge's centre
(`construct::as_circle`), in 3D, so any view can measure it; a split edge is ambiguous, and an edge
that is no longer round can't be measured. `drawing.dimension.add` takes `{centre: edge}` wherever
a corner goes, and refuses a straight edge.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing dimensions: a chain of distances along one line (#4896, W20c)
A chain dimension runs through 3 to 64 points in order (corners or hole centres) and reads the
distance from each to the next along one line as the view shows it: horizontal, vertical, along a
direction, or aligned from the first point to the last. The line runs the way the first point to
the last goes, and a point out of order is refused, never moved, so the members add up to the
overall from the first point to the last (`drawing_measure::chain`). It reads projected only (true
lengths of points off one line don't add up) and shows its members' numbers, never a text instead.
`drawing.dimension.add` takes `{chain: {points, direction?}}` and counts the points before looking
any up; `drawing.dimension.edit` changes its direction; inspect lists its members, its value being
the overall.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing dimensions: a whole cylinder's diameter between its silhouettes (#4896, W20b)
A cylinder dimension saves a face by name (ModelFace, as ModelEdge names an edge) and reads Ø
between the cylinder's two silhouettes, which any view but one along its axis shows 2r apart
(`drawing_measure::silhouette_side`). The face, or the pieces it was cut into, must lie on one
cylinder (the kernel's own condition for faces on a common cylinder); every face of the body on
it counts, since the kernel builds a whole cylinder as two half faces; and both silhouettes must
lie on those faces, so a fillet's quarter round is refused. `Document::face_reference` picks a
face by a point on it or by its index; `drawing.dimension.add` takes `{cylinder: {face}}`, edit
refuses its settings, and inspect can list face candidates.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing dimensions: between two cylinders' silhouettes (#4896, W20b)
A dimension between a silhouette of one cylinder and a silhouette of another, as the view shows
them. Each is named by a face of its cylinder and by its side: the one nearer the other cylinder
on the view, or the one farther (SilhouetteSide). So it keeps to them whichever way round the
view shows the cylinders.
- geom: drawing_measure::between_silhouettes takes the two cylinders (Cylinder, now geom's: the
type doc kept privately), their sides and a direction as a linear dimension takes it (aligned:
square to the silhouettes). The silhouettes must be parallel on the view and the axes apart
there. Foreshortened when the axes aren't parallel in the model, or the silhouettes lie at
different depths.
- doc: DimensionKind::Silhouettes { a, b: Silhouette { face, side }, direction }, read as the
view shows it. Each silhouette must lie on its cylinder's faces (Lookup::on_cylinder, now shared
with the cylinder kind). Document::silhouettes_kind makes two silhouettes of one cylinder of
one body that cylinder's diameter (which takes no direction), else the distance between them.
- engine: drawing.dimension.add takes {silhouettes: {a, b: {face, side}, direction?}};
drawing.dimension.edit changes its direction only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Recovery: a change to a drawing alone is autosaved and recovered (#3890, K9')
A design's drawings are part of its document (K3b), so the autosave already sees a drawing's
change as unsaved work and writes it with the design, and recovery reads it back. This test
holds that: with the model saved, a change to a drawing's properties alone is autosaved, and the
recovered design has the drawing as changed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Recovery: a drawing that breaks a rule is left out of the autosave and the crash copy, named (#3890)
Reading a design refuses it whole for one drawing that breaks a rule of the record, so an autosave
or a crash report's design holding one never opened again: all the unsaved work was lost. Both
copies now keep the rest of the design (drawing::openable) and name each drawing left out and the
rule: the autosave as its error, the crash report in its notes. A design's save error names the
drawing it couldn't save too (Drawing::label).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Recovery: a copy that must open keeps a design's drawing rules too (#3890)
drawing::openable now leaves out what a read refuses at the design's level as well: the later of
two drawings with one id, and the drawings past MAX_DRAWINGS, each named with its rule. So an
autosave never keeps a stale copy over a drawing problem. The id rule is take_id, which
check_in_design uses too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Recovery: a drawing too large to save is left out too, and the status line names it (#3890)
The autosave and the crash copy already leave out a drawing that breaks a rule of the record.
A drawing too large to save failed the whole autosave instead; `openable` now leaves it out the
same way, before its id, so the rest of the design is kept. Nothing is skipped silently: the
autosave's error names each drawing left out, and the status line shows it.
Tests: a drawing left out takes no id from a later one; an autosave leaves out one too large to
save; an autosave keeps what is hidden; the status line names a drawing the autosave left out.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Dimension::last_good saves the value the session last read, written only when
there is one, and a save takes it from the session's reads as it takes what is
hidden: no read touches the document, makes an undo step or asks to be saved.
drawing.dimension.inspect answers this session's read first, else the file's.
resolve_dimensions never sees it, so a lost dimension stays lost. A value past
the drawing format's limit is left out of the save, not written into a file that
then can't open.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
An edge given as a point looked along the edge's mesh lines, within a point's tolerance, so a point on a
circle or an arc that fell between two chords found no edge: a radial dimension on most points of a rim
said "no edge at". A point off every chord now takes the edge whose chords are within what its body was
meshed to, and only when no edge passes through the point itself, so a point on a straight edge beside
a curve is still that edge.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Dimension layout: sizes for paper, and text above the line (#4896, W19c)
A dimension's sizes become a parameter, so a drawing can lay its
dimensions out in paper millimetres with the same code as a sketch:
DimSizes { arrow, gap, overshoot, offset, above } and
dim_layout_sized. dim_layout and dim_layout_reaching keep their
signatures and use DimSizes::screen(px), today's 9, 3, 4 and 22 px;
the other default text places stay the same fractions of the offset
(18, 48 and 20 to 22), and default_text takes its 22 from the same
place. A size that isn't a finite number above 0 (or is beyond MAX_COORD,
the largest length a sketch takes) falls back to its 1 px value, as a
bad px does today.
`above` puts a linear dimension's text above an unbroken dimension
line (ISO 129-1), running along the line and reading from the bottom
or the right; DimLayout::text_dir gives its direction (level for text
in the line). The arrows go inside or out by the same test, with the
text's length along the line; a leader to text outside runs on under
it.
On main, #7056 moved the four default places into
default_text_offset(frame, px). This leaves those lines as they are,
so when main next comes into drawings the layout's arms call it with
this px (the offset's 22nd), and lib.rs's export line takes both lists.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Format the dimension layout's re-exports (#4896, W19c)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
`paper::annotate` places the marks that point at things on a sheet as plain
strokes and fills in sheet millimetres: open, closed and filled arrowheads,
the oblique stroke and the dot, and leaders (points, a landing line in the
reading direction, a terminator at the first point). Every size is the
caller's; ISO 128-2's dot and landing ratios and ISO 129-1's 15° to 90°
arrowhead angle are constants beside them. A closed or filled arrowhead stops
its line at the base. Dimension layout stays W19c's (#4896) and the datum
triangle the datum symbol's (#7257).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Brings the 1,270 commits on main since the drawings branch last met it,
including the first drawings landing (#8249), into drawings.
Merged with the drawings snapshot that #8249 landed (ebafe0748) as the
merge base, so only the drawings work after that landing is replayed on
top of main. Main's structure wins; drawings' additions are kept and
adapted to main's APIs (metadata Owner/KEYS table, checked! without a
contract argument, Snapshot visibility, crash journal outcome, the
part-properties normalisation, model_body_components, io helpers).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Ribbon drop-downs as a grid of large icons, as an option (#4739)
Preferences > Interface gets two settings, both off by default:
- Menus: List | Large icons. With Large icons a panel's drop-down shows its
commands as rows of up to five icons at 32 px (twice the list's), keeping
the list's group headings and order. The name, shortcut and help are in
the tooltip, as on a ribbon button. Arrow keys move across and between
rows; a click starts the command; right-click and drag pin it as before.
Resting on an icon for two seconds arms a pin in its corner; a quicker
click anywhere starts the command, so nothing is pinned by accident. A
pinned command shows its pin.
- "Leave pinned commands out of the drop-down menus": a drop-down (list or
grid) leaves out the commands the ribbon shows as buttons; a panel too
narrow for its buttons lists them again, and a menu is never left empty.
No icon artwork or colours change. Sketch > Create keeps its family list.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Sketch > Create in the large-icon grid: every variant under its family (#4739)
With Menus: Large icons, Sketch > Create showed its family sub-menus as
plain text rows (Line, Rectangle, Circle, ...), unlike every other menu. It
now shows every variant as a large icon under its family's heading, then the
other tools under "More". The list keeps its family sub-menus.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Large-icon menus: groups side by side, no footer (#4739)
A menu of two or more headed groups (Sketch > Create's families, Model >
Modify's Edges, Faces, Bodies, Specialise) puts the groups side by side, each
heading over a column of its icons, at most two across. Menus without groups,
or one too wide for the window, keep rows of up to five. The pinning note
under the large-icon menus goes: the tooltips and right-click menus say it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Large-icon menus: a separator line between groups side by side (#4739)
As between the ribbon's sections: a 1 px line in the theme's border colour.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Large-icon menus: at most three rows, growing to the right (#4739)
A menu fills five columns, more when that would take over three rows (Model >
Create: 7 x 3); in grouped menus a group too big for three rows gets more
columns. Only a window too narrow for that wraps into more rows. A test checks
every panel's menu of every tab.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Large-icon menus: group headings centred over their icons (#4739)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drop-down group headings: smaller than the drop-down's header, not in capitals (#4739)
Edges, Faces, Bodies (and Sketch > Create's families) were drawn in capitals
at font_small, which reads larger than the drop-down's own header
(font_caption). They now use font_micro, written as named, in the list and the
large-icon grid. Review feedback on #5012.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Large-icon menus: start a command from the grid at its icon, as main's rows do (#4739)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Ribbon icon grid strings: a pending locale file, with ar, he and vi (#4739)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Large-icon menus: commands in no group stand apart, so a left-out group doesn't move them (#4739)
With Convert to Sheet Metal pinned and left out of Model > Modify, its
Specialise group went and Delete, in no group, moved under Bodies.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #9332
* Expressions: hyperbolic functions sinh, cosh, tanh and their inverses (#9332)
`sinh`, `cosh`, `tanh` and `asinh` take a unit-less number and give one,
as `exp` does; `acosh` refuses x < 1 and `atanh` refuses |x| >= 1, worded
as `asin` and `ln` refuse what lies outside their domain. They are in the
expression help (`FUNCTIONS`, the parameter autocomplete), the
hostile-expression fuzz tokens, the parameters and equation-curve guides,
and the checking skill's list of functions. Horn flares (Salmon's hypex
`cosh h + T sinh h`), catenaries and the tractrix can now be written as
they are stated instead of expanded into `exp`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #6896
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013WUFq6QFBqmZjxwaKy2J1M
* Sketch: find profiles island by island and say when the search is cut short (#6896)
Every live drag frame ran the profile search over the whole sketch: every
spline piece of a large imported logo was sampled, checked for crossings
and traced again, about 100 ms per frame at 6,000 degrees of freedom.
Profile curves are now split into islands whose enlarged piece boxes do
not touch (the margin exceeds every tolerance the search uses), each
island's loops are found on their own and cached by a fingerprint of its
geometry, and the loops are nested into profiles over the whole sketch in
the order a single search finds them, so profiles, ids, holes and
whole-boundary certificates stay the same. The end-near-crossing lookup
covers its whole reach, independent of the grid's cell size.
The 50,000-piece limit now holds per island, and a sketch that still
exceeds it gets a warning instead of silently missing profiles.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013WUFq6QFBqmZjxwaKy2J1M
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
`schema::issues(id, params)` looks up the command's typed contract and
lists what is wrong with the parameters (none without a contract or for
an unknown id). The batch preflight, the MCP bridge's pre-send check and
the app's check of a design-switching command each did that lookup
themselves; they now call it. No behaviour change.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
sketch_consumed knew Extrude, Revolve and the sheet metal flanges but not
a Hole drilled at a sketch's points, so that sketch stayed drawn inside the
holes. A Multiple hole now counts as using its sketch; Edit Feature shows
its points while the dialog is open. Single holes keep their sketch shown,
since a hidden sketch offers no points to snap to.
* claim: #9299
* Recover unsaved design?: Discard all, with a confirmation (#9299)
A Discard all button at the left of the window's footer, away from Later.
It asks first ("Discard 7 autosaves? This cannot be undone.", Discard all /
Cancel), then discards every listed autosave the way its own Discard button
does (doc.recovery_discard on each id). Cancel goes back to the list
unchanged. The button publishes the handle recovery:discard_all.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #9265
Co-Authored-By: Claude Code <noreply@anthropic.com>
* Preferences: a setting in a row with no label is found by the words it shows (#9265)
A search hid a row by its label before its controls were drawn. Seven checkbox rows and two segmented rows on the Interface page have no label, so "status bar", "search results" and "reveal on hover" found nothing. A hidden row with no label now keeps the words of its controls, as a dropdown keeps its value, and is drawn when they match. Each such row has a key of its own, so its neighbours are not drawn with it.
Co-Authored-By: Claude Code <noreply@anthropic.com>
---------
Co-authored-by: Claude Code <noreply@anthropic.com>
* claim: #9195
* Viewport: an opened design is framed at the home view, in the middle of the view (#9195)
A fit the viewport cannot frame yet is kept and asked again on the next frame instead
of being consumed and lost, so a design opened from the Home screen or by launching on
a file no longer comes up at the default camera, wherever its own coordinates put it.
Framing then centres the model on the view rather than on the free rectangle between
the floating controls: `Layout` carries `middle`, the view without the panels, computed
before the compass, the navigation bar and the OK/Cancel corner join the obstacles. The
model is still sized to `drawable`, so it stays clear of every control, but only a panel
moves it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Viewport: keep the centred fit clear of the controls, and frame --sample (#9195)
The fit framed the model into a rectangle the size of `drawable` centred on `middle`, which is
not inside `drawable`, so the model could sit under the compass, the navigation bar or the
render product frame. The model is now sized to `drawable` as before, and only the room left
over in it moves the model towards the middle of the view.
`s27_move_triad` turns the Z ring on its far side: the model now sits further right, and the
Move dialog that docks on the right afterwards covered the ring's old grab point.
A launch with `--sample` builds the design straight into the session and never asked for a
fit; it now comes up at the home view, framed, like an opened design.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Viewport tests: the cursor orbit's pivot keeps the pixel it was picked on (#9195)
With the model now centred on the view, one of the test's perspective drag points lands within
the pick tolerance of the box's top edge, and the pick snaps the pivot onto that edge, a few
pixels from the cursor. The orbit keeps that point still, as it should; the test compared it
with the cursor instead of with where it started.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Both commands need a choice, but start_with_variant_memory only special-
cased the parameterless display toggles, so a click on the ribbon (or
command search, a shortcut, ui.start) ran them with {} and showed only
the engine's "style must be ..." / "tool must be ..." error.
They now open a short menu, the way Units does: under the ribbon row
that started it (a second click closes it), or in the middle of the view.
Visual style lists the four styles with the current one checked; the
Navigation tool lists Orbit, Pan and Zoom, and picking the latched one
releases it, as its navigation-bar button does. The four styles come
from one list shared with the navigation bar's menu.
A sketch that failed (a dimension set negative by a parameter) left every
feature built on it with "unknown sketch 1 (it must come earlier in the
timeline)", which points at the timeline order. The evaluation now notes
the features that failed earlier in the pass, and a feature whose input
sketch is one of them fails with "sketch `Sketch1` failed: <its error>".
That result is never cached, since the cache key holds neither the
sketch's name nor its error: a second failing value or a rename shows
up at once.
A page window (Preferences) filled its frame with
dialog_body.gamma_multiply(0.97), which also scales the alpha, so the
ribbon's labels showed through its title bar, search field and heading.
It now uses the opaque dialog_body, as every other window does; the
navigation bar keeps its own translucent resting fill.
* i18n(uk): add pending translations for missing strings
* i18n(uk): add pending translations for new strings from upstream
* i18n(uk): improve pending translations
* i18n(uk): professional translation of all pending strings
* i18n(uk): finish the pending translations for 29 October
Every value in uk-sync-29oct.json is now Ukrainian except the seven symbols
and formulas that must stay unchanged: the modifier keys, HDR and the three
inertia tensors.
Terminology follows uk.json: spot light is "прожектор", directional light is
"спрямоване", body is "тіло", render is "рендеринг". Camera orientation keeps
тангаж, рискання and крен.
Removes history-origin-bodies.json, which was empty after its content moved
into this file.
- worst_status searched the history and the results for each feature of a folded folder, the square of the history's length every frame
- A folder of 16 features or more now finds places and results in tables built once, first of an id winning as feature_index and Model::result do
- Frame on a 4,880-body design: 20.4 ms -> 7.9 ms (M4 Max, release, headless)
- Test a_folders_mark_is_the_worst_of_its_features_own
Co-authored-by: P'tite Tête <asso@ptitetete.com>
A new user guide page, Equation curves, under Sketching: the first curve step by step, Explicit and
Parametric explained with tables of rules to type (a line, a parabola, a wave, a circle, a spiral),
radians, parameters with the involute of a base circle, building a part (a closed flower curve
extruded), editing, and common mistakes. The technical details stay in a short section for scripts
and agents.
Seven pictures, rendered by `cargo xtask book` from new scripts in docs/book/scripts/equation-*.json
(gitignored output, attributed in attribution/pending/docs-equation-curves.md). The Sketching page
keeps a short summary and links to the new page.
* claim: #9184
* Control channel: a request's own param_check level, applied and confirmed by the app (#9184)
`engine.execute`, `engine.batch` and `engine.script` take an optional
`param_check: "off"|"warn"|"strict"` in their params. The app runs the
request at that level through the same `run_checked` path and batch
preflight a headless session uses, puts its own level back, and every
reply says the level it ran at as `param_check` beside `ok`. A command
that switches designs keeps the app's level; any other word is refused.
Readings and exports answered on a copy off the window thread keep the
level too.
`control.capabilities`, answered at once on the control-server thread,
lists what the channel does beyond the methods every version has:
`{"param_check": ["off", "warn", "strict"]}`. An older app answers
"unknown method".
The MCP bridge asks it once per connection. When the app lists the
level, every call and batch goes with it and the app checks against its
own commands; otherwise the bridge checks before sending, as #9073 does.
At strict, a reply that doesn't say the app checked the call is an error,
and the next calls are checked by the bridge. Also: `add_param_warnings`
takes the reply by `&mut`, and `with_param_check` sits beside
`Remote::new`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Control channel: a design-switching command is checked at the request's level
Review fixes for #9184. `component.edit_in_place`, `component.finish_edit`,
`view.next_tab` and `view.previous_tab` ran at the app's own level and
echoed `off`, so a misspelt call went through at strict. They now have
their parameters checked at the request's level before they run (and
still run at the app's own level, as each design keeps its own), and the
reply echoes the requested level. The MCP bridge at strict now requires
the echo to match its level, not just to be there. `param_check` is read
only by `engine.execute`, `engine.batch` and `engine.script`; other
methods ignore it. The off-thread test now shows the design change
computing off the window thread before it is refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Sketch corners: a round that would use up a whole arc is refused
A quarter arc of radius 5 running into a line along it, rounded with
radius 5, put the round's tangent point at the arc's other end and left
an arc of no length. round_corner now skips a round whose tangent point
falls on either curve's other end; with none left it says that radius
does not fit, as for two lines (found by a local trim fuzzer).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Sketch corners: a round where the curves meet smoothly is refused
A line running on from an arc's end along its tangent (off only by rounding)
has no corner, but the round's tangent points landed within 1e-7 mm of it and
the round came out as an arc of no length (found by a local trim fuzzer). The
corner test's line met its arc this way too and was checking that output; it
now meets the arc at a corner.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #9179
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FAWPuFoh9qxn2uS2j53QZ
* sketch.inspect: each profile loop names a curve once, not once per segment (#9179)
A profile records one curve id per piece its loop walks, and a conic or
spline is walked as many short pieces, so sketch.inspect listed a single
Bezier curve 50 times in a profile's `curves` and as often in a hole loop.
The reply now names each curve of a loop once, in the order the loop walks
them (solidcraft_sketch::distinct_curves, as delete and profile references
already do). The solved profiles keep their per-piece lists.
The lists are now a profile pick as they stand: a whole_boundaries pick
refuses a curve named twice, and the regression test extrudes the wall
from the reply's lists.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FAWPuFoh9qxn2uS2j53QZ
---------
Co-authored-by: Claude <noreply@anthropic.com>
An SVG import leaves a sketch with regions and no body. The marquee (box select) took only the bodies, so the imported geometry was unreachable with the selection tools: a marquee over it selected nothing, and a click on a region did not take it either.
The marquee now takes the regions of the shown sketches when the design has no body. A design that already has a body keeps the old exclusive behaviour: the marquee takes the bodies alone, not the regions of the sketches that are shown. A dialog that asks for edges or faces (the Patch dialog) never takes the regions: they are neither.
* claim: #2829 (construct.* contracts)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Typed contracts for the construction axes, points and planes
The 19 construct.* commands get input contracts and FeatureResult as
their output, with shared types for references, planes, a plane or
face base, an axis line and a construction plane's name, each calling
the reader its handler uses (split out of geo, plane_param, the face
capture and named_axis_line).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #9188
* Preference: keep the view when a sketch opens, Look At faces it on request (#9188)
Preferences > Sketch > "Turn to face a sketch when it opens" (on by default,
so nothing changes unless it is turned off). Off: creating or editing a
sketch leaves the view as it is, a sketch started on a face no longer
re-centres on the picked point, and Look At (the palette button, Ctrl+8)
still turns to face the sketch. The control channel sets it as
`ui.prefs {"sketch_open_look_at": false}`.
Changes a default: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #9087
* fix(#9087): ui.home refuses a sample index that names no sample
The control channel passed the sample index straight to open_sample, which drops an index that names no sample and says nothing. The request then replied ok, so an agent could not tell a typo from a sample that had opened.
The index is checked before it reaches open_sample: an index that names no sample is refused with the valid range in the error. A sample that exists still opens, as before.
* claim: #9086 (edge lengths)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Measure: an exact circle's or arc's length agrees with its circumference (#9086)
Measure gave a circle's or an arc's length_mm as the length of the
polyline drawn for it, which runs inside the curve, while the same reply
gave its circumference or arc_length from the exact circle. When the edge
is known exactly (the kernel's circle, or a sketch's own circle or arc),
length_mm is now its radius times its angle. A circle fitted to a mesh
body's polyline keeps the polyline's length.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Drawing sheets on paper: the drawing border, grid and marks (#4890 A6, #3875)
SheetLayout::paper gives what a sheet's drawing border draws as paper
groups of kind drawing_border, in order, each left out when empty:
- the drawing border, one closed stroke drawing_border_line_mm wide
(0.7 mm in the built-in profile, ISO 5457);
- the grid: the grid reference border, closed, and a mark at each edge
between two fields from the drawing border out to it, on all four
sides, GRID_LINE_MM (0.35 mm, ISO 5457's grid line width as the
drawings study's section 11.3 gives it) wide; none without fields or
with the grid reference border on the drawing border;
- the centring marks, as the layout places them, in the drawing
border's width, as the profile says;
- the trimming marks, each rectangle filled.
The grid's labels are text and come once the paper scene has text
(#4813). Only the function, its constant and its tests are added.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing sheets on paper: one sheet's scene, its border then its views by id (#4890 W10, #3890 W15)
`drawing_scene::build` draws one sheet of a drawing as one paper scene, the
one the sheet page and every export will draw, and `drawing.scene {sheet}`
answers it (in_drawing, noundo, doc_only, typed contract and output schema).
In paint order: the sheet's drawing border groups as `SheetLayout::paper`
gives them (object: the sheet), then each view on the sheet by id (object:
the view), its visible lines as `paper_group` makes them.
- A view's lines are worked out centred on the view and then moved to its
centre, exactly (each point plus the centre), so the same lines on the same
centre are the same bytes. They match the snapshot's within rounding.
- The shown set is worked out once per sheet. No model is evaluated.
- A view with nothing to measure is a placeholder, and one that can't be
drawn yet (shaded, or hidden lines) fails with the snapshot's reason. Each
draws nothing, and the sheet's other views are still drawn.
- A view is placed only while the sheet stays within the paper scene's
100,000 records. One that would pass the limit fails and says so, and later
views that fit are still drawn. Each view's lines are placed as they are
drawn, so at most one view's lines are held beside the scene.
- A view above 100:1 takes a coarser chord rather than asking the mesh for
chords below its floor. A saved view has no chord setting to give.
One rule, one place:
- the snapshot's two style refusals are `drawing_view::not_drawable`;
- a group's record count is `paper::Group::records`, which `Scene::validate`
now counts with;
- the stats reply is `From<Stats>`;
- both paper scene schemas come from `paper_scene_schema`.
Tests: the border then the views by id, byte for byte against the layout,
with hand-worked outlines and lengths; the command's reply and its schema; the
SVG's group order; each view against the snapshot, and one of many lines (a
block with eight holes, from above); an empty sheet; a failed and a
placeholder view; no evaluation; refusals; the record limit at exactly 100,000
and one past it; a 200:1 view.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing sheets on paper: the provider the sheet page draws through (#4890 W10, #3890 W15)
The sheet page asks for a sheet every frame, so it draws through a provider
that keeps each view's lines between frames, instead of through `build`.
- `drawing_scene::extract(session, sheet)` returns the last scene at once,
with each view as it was last drawn and the ones out of date. Nothing is
drawn there. When the drawing, the world state and the views drawn are all
as they were, it returns the same answer without looking at the sheet.
- `drawing_scene::step(session)` draws the first out-of-date view of the
sheet last extracted and says whether more wait: a short piece of work for
the UI's tick, one view's projection.
- `generation` comes from one counter for every session. It goes up only
when a sheet's scene is assembled again, so a page never sees one number for
two scenes, even across closing and opening its drawing.
The cache is the session's view state, beside the record: never saved, never
an undo step. `Session::execute` drops it when a command closes the drawing or
opens another. One entry per view, keyed by the world state it was drawn from
(held in the entry and compared with `Arc::ptr_eq`) and the view as it resolves.
A drawing-only edit keeps the world state, so a move, and its undo, project
nothing: the same centred lines are moved to the new centre. The shown set is
worked out once per world state. Views no longer in the drawing are forgotten
when a scene is assembled again.
Tests:
- a new cache gives the border with both views out of date;
- each step draws one view, in id order, until the scene is `build`'s, byte for
byte;
- the generation stays put while nothing changes;
- a move and its undo project nothing, and the views move by exactly the move;
- after a model change (a checkpoint restored) the last scene shows, and a view
listed as up to date always draws as `build` does;
- closing drops the cache, and the scene after reopening has a new generation;
- seeded mixes of moves, setting edits, undos, redos, extracts and steps
always end at `build`'s scene.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing export: a sheet of the open drawing as SVG, from its scene (#4890 A7)
drawing.export {sheet, format?, path?} writes one sheet of the open
drawing from drawing_scene::build's scene, the scene the sheet page
draws, so a file shows what the page does: its drawing border, then
each view's visible lines, through the SVG writer. With a path (a .svg
file, checked by vfs::check before and again just before writing, as
the view DXF is) the file is written atomically; without one the reply
holds the SVG. The reply gives its size, what the writer snapped or
merged, and each view's status as drawing.scene reports it.
A sheet with a view that failed isn't written: the refusal names each
such view with why. A placeholder view (nothing to measure) draws
nothing and is exported all the same. PNG and DXF are "not supported
yet": they come with the sheet PNG writer (#4890 A4) and the shared DXF
writer (#5319). No undo step; no model is evaluated.
One rule, one place: the scene's sheet look-up is
drawing_scene::sheet_id, and a view's reply entry is
From<ViewState> for SheetSceneView, both used by drawing.scene and
drawing.export. The scene tests' drawing fixture is open to the crate's
tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`bom_quantity` is a part property of any component: what one placement of it means, an expression
evaluated with the design's parameters (pieces without a unit, else a length). A row's `quantity`
is what its placements add up to, with `quantity_unit`; pieces beside a length, or a quantity that
no longer evaluates, give none and say why. A quantity is a parameter user: a rename follows it, and
a parameter it uses can't be deleted. The CSV gains a `quantity` column at the end.
`virtual` marks a component with no geometry (grease, paint, a label): listed with its quantity,
weighing nothing. It can't be the active component, and one check after each undoable outermost
command refuses any change that puts a body, a sketch or a placed component into one, whichever
command made it. A file a damaged virtual component already fills opens, and other changes go on.
Mass, volume and area quantities wait for the expression units of #4811.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Bill of materials types on one occurrence (#3874)
An occurrence can count in the bill of materials as another type than its
component, for that occurrence only: `bom_type` on the occurrence, set with
occurrence.bom_type (one undo step; it changes no geometry, so nothing is
evaluated). An occurrence without it counts as its component, so older
designs need no step; the format change is one pending file
(occurrence-bom-type.rs).
assembly.bom counts each placement by its own type: a purchased placement of
a kit part is one row without the kit's contents, a phantom placement lists
what it holds where it sits, and one component placed as two types gives a
row for each. Paste New keeps the types; Paste Instance counts as the
component. The compact list and the locales (en, de, ja, ko, zh) carry it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Bill of materials rows: material, and mass when asked (#3874)
Every row of assembly.bom (and every part folded into a row) names the
physical materials of its definitions' own bodies, each once, with "; "
between several. An unassigned body's is Default. A subassembly or an
empty component has none of its own.
With mass: true each row also carries mass_g, what one of its parts
weighs as the row stands for it, so the rows of any list add up to the
assembly once:
- at the top level, all it holds;
- in parts only, its own bodies, because what it holds is listed beside
it; but a folded part's row is the kit (all it holds), a purchased part
is all it holds, and an inseparable one all but its purchased parts;
- a reference placement adds nothing, nor does anything inside it.
It is null when nothing in the part has a body, a body can't be
measured or measures a negative volume (inside out, such as an open mesh
facing the origin), or the definitions sharing the row's part number
weigh differently.
Measuring runs the kernel, so it is asked for: the Bill of Materials
window re-runs assembly.bom after every change and doesn't ask (0.2 ms
without mass, 135 ms with it on a bolted bracket). Placements are rigid,
so each definition is weighed once however often it is placed, and each
body is measured once with measure_mass (the same volume as measure,
without the topology counts' tessellation).
A body's material and its density now follow one rule:
Document::body_material (new, beside Document::density) is the material
its mass is worked out with, named as the library or the design spells
it: the assigned one when it is known and has a usable density, else
DEFAULT_MATERIAL (new, beside DEFAULT_DENSITY). Document::density is
that material's density, with the same values as before. So a damaged
file's unknown or broken material reads as Default, as it weighs, and a
hand-edited "steel" reads as Steel. The bill of materials uses both; the
other places that read a body's material or spell "Default"
(component.properties, inspect and others) move to them in a separate
cleanup PR for #4783, which brings the same two functions to main.
Document::body_owners returns each body's owner in order, so the rows
find their bodies in one pass. It no longer repeats the owner rule:
model_body_component, legacy_body_component and body_owners go through
the rule written once, given how to find a feature's component (the
timeline for one body; body_owners still builds its map once). The CSV
columns are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Physical properties: a reference placement adds nothing (#3874)
component.properties and occurrence.properties now leave out a placement
that counts as a reference in the bill of materials (its occurrence's
bom_type, else its component's: a fixture shown for context), and
everything inside it: no body entry, mass, volume, area, centre of mass
or bounding box, and it isn't in placement_count. That is the bill of
materials' rule, so a reference part is in neither the parts list nor
the assembly's mass, and every bill of materials list still adds up to
the assembly's Properties. The placement asked for counts whatever its
type: a reference fixture's own Properties still measure it and what it
holds, as a component's own bill of materials lists what it holds.
Only the totals change. The placements walk that face picking, sketches
on placed faces and the occurrence path lookup use stays complete, so a
reference fixture's faces can still be picked.
The type an occurrence counts as is now written once:
Document::occurrence_bom_types (new, beside BomType) gives each
occurrence's type in their order, looking each component up once. The
bill of materials, occurrence.bom_type's reply and the physical
properties use it, in place of the two copies in the bill of materials.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Bill of materials: one module of typed rows in doc (#3874)
The bill of materials now lives in solidcraft_doc::bom as the typed rows
of a bill of materials view. assembly.bom's JSON and CSV and the Bill of
Materials window present those rows; parts lists, item numbers and
balloons are to read them too.
- list(tree, request) walks the placements below the assembly once. One
rule (classify) decides for each placement whether it is counted in a
row, how what it holds is walked and what its row stands for when
weighed, from the type it counts as, the view and the component's
shape. "An inseparable part's purchased parts are listed beside it" is
written there once, and the mass walk reads it.
- A row carries its identity (RowKey: the rows it is listed under and
its lowest member, a component and the type it counts as), its item,
count, each, the placements it counts by occurrence path, what it
stands for (Holds), its part properties, materials, mass and children.
- Mass is a Mass: not asked, grams, or unknown with the reason (nothing
to weigh, unmeasurable, the definitions differ). Each definition is
weighed once for each kind of row and kept in the Tree. The assembly's
total is weighed from the design the same way, not added up from rows,
so it is known even when a row's definitions weigh differently.
- Bom::row_of(tree, path) finds the row a placement is counted in (a nut
inside a purchased bolt is in the bolt's), or says why there is none:
no such placement, a reference, or no row of its own.
- rows_deep() lists each row followed by its children (1, 1.1, 1.2, 2);
Request::default() is assembly.bom {}; View and Group carry the
commands' words.
What changes, beside that the output is otherwise the same:
- One placement budget (100,000) and one depth limit (64 levels) cover
the whole list. A folded part's contents used to be walked under a
budget of their own, so a design past the limit was listed with
group_children; it is refused now, as it already was without it.
- Kits are compared by what they hold (part, type and count), not as
text: a part without a part number whose id is 12 no longer reads as a
part numbered "#12", nor a part numbered "P×1\nQ" as a P and a Q. Kits
holding the same parts through different definitions (two bolt
definitions, each with its own nut definition of one part number) are
one row, and its children count every bolt's placements, where they
showed the first bolt's definitions only.
- A folded part's row always stands for all it holds. It used to stand
for its own bodies when nothing inside it was listed, which differs
only for a phantom with bodies of its own inside it.
- An inseparable part's row in parts only names the material of what it
stands for (its steel blocks read Steel) where it was blank, and a
subassembly whose parts share one material names it at the top level.
- A damaged file's component placed inside itself is refused at the first
level too, since naming a row's material walks inside it, as weighing
it already did.
- A row's body count saturates, so a compact assembly with more placed
bodies than a count holds (a block placed 2^64 times) is weighed
instead of overflowing.
Materials follow Cameron's rule for a row that stands for an assembly
(C3, 2026-10-23): a part's row lists its own bodies' materials, as before;
a row that weighs what is placed inside it (a subassembly at the top level,
a purchased or inseparable part with parts inside it, a folded part) names
a material only when everything it stands for shares it, else none. A frame
of steel and aluminium parts reads blank, and a bought bearing of steel
parts reads steel. One walk (adding) decides what is placed inside a row's
part for both its mass and its materials, and each definition's materials
are read once.
Components are looked up by id once per list; they were searched for
each row and each kit. The window reads the rows directly instead of
running assembly.bom after every change, so it no longer adds an entry
to the crash journal each time it refreshes. The engine keeps the
command, its parameters and the files.
Tests check rows. The module's own: an empty assembly, one part, a row
named by its lowest member through part-number edits, one part number
of two types, a phantom in a phantom, a reference at the root and its
own list, the 64-level limit, the budget hit exactly with and without
folding, kits told apart by what they hold and the types they hold it as, a folded part's contents not
folded again, row_of for every type, every placement in one row or
none, a damaged file's missing component and cycle, the mass agreement
rule (moved from the engine with row_mass) and what adds to a row. In
the engine: the material rule for every kind of row, the JSON against the
rows for every option, why a mass is
unknown, the total against component.properties in every view, what a
row stands for and its placements, and the overflow. The assembly.bom
tests from #1888 on pass unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Bill of materials: structured, all levels (#3874)
P4: `assembly.bom {levels: "all"}` lists the assembly's direct contents, each subassembly a row with
its own rows listed under it, to any depth (items 1, 1.1, 1.1.1), each child counted `each` per one
of its parent and `count` in all.
- `doc::bom::View::AllLevels` goes through the one rule (`classify`) the other views use: a
normal subassembly is a row holding what it holds, listed under it as all levels again; a
phantom has no row and what it holds joins the level it sits in, merged with the same parts
there; a purchased part is a row with nothing under it unless `show_purchased_contents`
(Request::purchased_contents) asks for its contents; an inseparable part is a row with nothing
under it; a reference counts nowhere.
- The rows under a row are the folded-part rows already built (kits), so subassemblies with one
part number and different contents are different rows, and no second nested builder is written.
All levels gathers every level as it gathers the first; a folded part's contents are still
gathered by part number in the other views.
- The JSON and CSV already nest to any depth; `show_purchased_contents` is echoed with the other
options. Each row's mass still adds up to the assembly at the top.
Tests, worked by hand: four bolts and a two-bolt subassembly placed twice (8 bolts in parts only;
in all levels the subassembly twice with 2 bolts each, 4 in all, beside the top's 4); three levels
deep (2 x 3 x 2: counts 2, 6, 12, items 1, 1.1, 1.1.1, the CSV in that order); each type (a
phantom's bolt merged into its subassembly's, purchased with and without its contents,
inseparable, reference); two subassemblies of one part number with different contents as two
rows; `levels: "deep"`, a flag that isn't one, and an assembly over 100,000 placements refused.
D's test that a folded part's contents are listed as parts only lists them now names the two views
it holds for, and checks all levels' nested rows (the subassembly inside the folded part a row with
its own rows).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Bill of materials: parts with one part number are one row only when they hold the same to any depth (#3874)
The kit compare looked one level down: two parts with one part number whose contents differed two
levels below merged into one row, and the children that only one of them held came out at each 0.
Each placement now has a kit, a number interned from what it holds (each child's part, type and
own kit), built in one pass from the last entry back: an entry's children come after it in the
walk, so their kits are known first. Rows are told apart by that number.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Bill of materials: a phantom's own bodies are a row of their own (#3874)
A phantom had no row, and its own bodies were weighed in the assembly's total but in no row, so
the rows didn't add up. Its own bodies, when it has any, are now a row of the phantom's type that
stands for them alone, as a subassembly's own bodies are in parts only; what it holds still counts
where it is placed. Inside an inseparable part in parts only, nothing changes: the part's row
weighs them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Bill of materials: the assembly's total mass in assembly.bom (#3874)
`list` weighed the assembly as a whole (from the design, not added up from rows) and the command
dropped it. `assembly.bom` now gives it as `total_mass_g`, and so does a JSON file it writes, only
when `mass: true` (null when it isn't known).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Bill of materials: all levels as JSON lists its rows one after another (#3874)
All levels nested each level's rows in the row above it: two JSON containers a level, so the
64 levels a list may hold came to 129, past the 128 that serde_json (and so the MCP server and
the control channel) reads. In all levels `assembly.bom` now gives the rows one after another,
each followed by the rows under it (items 1, 1.1, 1.1.1), as its CSV does; a row listed under
another still reads its item as text and gives `each`. A folded part's contents, one level, stay
inside its row as `children`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Bill of materials: a row whose definitions weigh something and nothing differs (#3874)
A row gathering a definition with bodies and one with none (one part number on a steel block and
on an empty assembly) said there was nothing to weigh. Their masses differ, so it says so; only a
row where none of them has a body has nothing to weigh.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Bill of materials: definitions agree on a mass within the measurement accuracy (#3874)
A row's definitions had to weigh the same to 1e-9, but the kernel measures a volume from
tessellations, and one part modelled another way is tessellated differently: a pin made as a
cylinder and as a revolved rectangle measured 1.05e-5 apart, so it read as two masses. The bill of
materials now has its own `bom::MEASURE_ACCURACY` (1e-4: ten times that gap, with its doc on what
it follows: the kernel's `measure` gives one solid's volume to about that, relative, however the
solid was modelled), and a row's definitions agree within it. A pin 0.1% wider (0.2% heavier) still
differs. The kernel is unchanged: its work is paused while it is being replaced.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Bill of materials guide: all levels, purchased contents and this round's changes (#3874)
The guide had no `levels: "all"` or `show_purchased_contents`. It now has both, with how all
levels tells subassemblies apart and lists its rows as JSON, a phantom's own bodies as a row, the
assembly's `total_mass_g`, and definitions that agree on a mass within the measurement.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Bill of materials: materials are bodies', as the code says (#3874)
The rule stays as built (a row that stands for an assembly names a material only when everything
in it shares one), but its comment and the guide said a material set on a component is set on
all its bodies. `material.assign` takes bodies only: a component has no material of its own yet.
The module notes now say so, and where component materials will join the rule.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Bill of materials: a body's volume measured once with the evaluated model (#3874)
`assembly.bom` measured each definition's bodies anew for every list, and `component.properties`
measured each placed body again. A body's measure depends on nothing but the body, so the evaluated
model keeps its volume (`ModelBody::volume`, shared by clones, as its outline box is), and both
commands weigh that one measurement: the same grams to the bit. A body offset scales it by the size
of its determinant (`mat_volume_scale`) instead of copying the B-rep to measure it. The physical
properties still measure the placed body for its area, centre, box and counts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Bill of materials: row keys, what rows stand for, members and row_of stay in doc (#3874)
Nothing outside `doc` reads them yet: `RowKey`, `Unlisted`, `Holds`, a row's `key`, `stands_for`
and `members`, and `Bom::row_of` are crate-private until item numbers and balloons need them, so
they can still change without breaking another crate. The engine test that read them is covered
by doc's own (`row_of_finds_the_row_a_placement_is_counted_in`, `every_placement_is_in_one_row_or_none`).
`Unknown` stays public: `Mass::Unknown` carries it, and a public enum can't carry a private type.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Assemblies: one fallible placement walk for totals that cover the whole assembly (#3874)
The physical properties walked the placements twice in their own code, with the 64 levels and
100,000 placements written as literals, and the bill of materials gave the same refusal in words
of its own. `Document::placements_below` is that walk once, from any placement, refusing what is
past `MAX_PLACEMENT_DEPTH` or `MAX_PLACEMENTS` (never walking it in part), and both use its
refusal: "the assembly is deeper than 64 levels or has more than 100000 placements". The physical
properties now allow 100,000 placements below the start, as the bill of materials does (they
stopped at 99,999).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Bill of materials: the same rows for less work (#3874)
- Part numbers are borrowed from their definitions (`Part<'a>::Number(&'a str)`) instead of copied
for every placement grouped and every kit.
- A row's members, the placements' paths, are built only when the list is asked for them
(`list_with`, for item numbers, balloons and `row_of`): the walk then keeps paths and moves each
into its one row instead of copying it. A list made for its rows alone builds no paths at all.
- `row_of` reads an index of member paths, built once with the list, instead of scanning every
row's members for each placement asked about.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* occurrence.bom_type: input and output schemas (#3874, #4035)
`OccurrenceBomTypeParams` takes the occurrence (an id or a name), its path when given and the
type (one of the five, `same_as_component` or null), each read by the handler's own reader: the
path through `cmd::path_ids`, split out of `occurrence_path`, the type through
`cmd::occurrence_bom_type`, split out of the handler. The handler builds its reply from
`OccurrenceBomTypeResult` ({occurrence, bom_type, effective, changed}).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Bill of materials tests: all levels presented at every depth, and its caps (#3874)
- `assembly_bom_presents_the_typed_rows` now covers all levels (with mass, purchased contents and
by definition) and the total mass, on a three-level design with a purchased gearbox holding a
gear, comparing every field of every row down to 1.1.1.1.
- All levels lists 100,000 placements and 64 levels, and refuses 100,001 and 65.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Bill of materials: static materials, typed in for a component (#3874)
A value the bill of materials works out can now be typed in by hand for a component (a static
value, Cameron's choice of word). Material comes first:
`component.metadata.set {component, material}` takes any text up to MAX_TEXT_BYTES, and
`component.metadata` reads it back as `material`. It is shown wherever the component is listed,
at every level and in every view, over the materials worked out from its own bodies or an
assembly's blank, and the row carries `static: ["material"]`. Clearing it (empty text or null)
brings the worked-out material back.
It is kept the way `bom_type` is: a part property in the component's metadata (`material`), so
there is no format change and older releases keep it. `PART_ONLY` names both keys in one place:
Design Properties doesn't list them and `document.metadata.set` refuses them. Another tool's
non-text value is kept and reads as no static material.
The bill of materials says one thing about the part everywhere: a static material also stands for
the part in the one material an assembly around it shares (two parts typed in as S355 make their
frame read S355; S355 beside plain steel reads blank). A phantom's row is its own bodies, so its
static material names those only, and what it holds keeps its own. A row gathering several
definitions takes the first one's that has a static material, as its description does. It only
names: mass still comes from the bodies.
The CSV gets a `material` column at the end, so the other columns keep their places: what the row
shows, the static material or the worked-out ones joined with "; ".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Bill of materials: stable item numbers, saved with the design (#3874, P5a)
A numbered parts list keeps each row's item number under the row's key in
`assembly.item_numbers`, so a row keeps its number when others are added,
removed or reordered. New rows take the next number after the highest at
their level; Renumber restarts a view in list or item order and leaves
locked rows alone; duplicates are marked; numbers change no geometry and
are part properties in the undo history.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #9029
Co-authored-by: Cursor <cursoragent@cursor.com>
* Square sketch axes when looking at a turned plane (#9029)
Look At only yaw-aligned sketches facing world up/down, so a sketch on a rotated component stayed tilted on screen. Roll (or yaw at the poles) so the sketch's x runs right and y is upright.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* claim: #9108
Co-authored-by: Cursor <cursoragent@cursor.com>
* Canvas calibrate: pick Point A and B with viewport clicks (#9108).
The Calibrate card only had typed coordinates, so users could not mark the image. Clicks on the canvas now fill A then B, with on-image markers and a short hint.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Calibrate canvas: ja/zh/ar/he/vi strings for the click hints
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* View DXF: its lines come from one module both view commands share (#5866, W07 V1)
The view DXF (file.export_view_dxf) finds its lines in four files private to the command: the
export meshes, the projection, the hidden-line scene and the progress stages. Drawing views
(W07's drawing.view) need the same lines, so the four move to engine/src/mesh_lines, with
three changes that let a caller other than the DXF use them:
- prepare takes the bodies to draw from its caller (the DXF passes the shown ones), and
names the caller's command in its errors;
- the lines are seen from a View (an origin, a right/up/back basis and, for perspective, the
eye's distance) instead of a viewport Camera; the DXF builds one from its camera, with the
same arithmetic as before;
- a sketch's polyline (Part) belongs to the module, so the sketch code no longer defines what
the scene draws.
The DXF writing stays in cmd/view_dxf.rs, with connect (the dashed-line joining), and the stage
names and cancellation text stay the DXF's. Nothing else changes: the module is the DXF's own
code, moved, and the DXF writes the same bytes as before for every case checked (see the PR).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* View DXF: the sketch runs go once the lines are made, and a box's outline pinned (#5866, review)
From the review of #6482:
- The sketch runs (up to 4,000,000 points) are dropped once the lines are made, as they were
before the move, instead of living through the DXF writing.
- `mesh_lines::prepare`'s doc says which errors name the command: those about the placements.
A body with no exportable surface, the memory budget and cancellation come back as the meshing
stage's own.
- A new test pins the lines themselves by hand: a plain 40 x 30 x 10 box seen from the front is
its 40 x 10 outline. Every line lies along a side, they add up to the 100 mm perimeter, and
nothing is hidden.
- The shared-geometry test hides the definition's first placement: the other two are still
drawn from the one shared mesh.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing views: the bodies a view shows, worked out once per request (#5866, W07 V3)
A view's extents and, next, its lines read one list: each body the design places, with the
occurrences that place it and its definition's body name, in the world state's order. Choosing
occurrences and hiding parts (V4) and the reference split (V5) become filters on it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing views: a view's lines from the mesh projection, each named by what drew it (#5866, W07 V3)
drawing.view.snapshot now returns its lines: every shown body's edges and silhouettes from the
view DXF's mesh projection, seen orthographically on the view's basis, in pieces of one visibility
on the sheet. Each line names its occurrence path, its body and its source (an edge, as model.edges
numbers it, with whether it is a tangent edge, or a silhouette across a face). They are marked
approximate and sampled (Cameron's choice, 2026-10-27): the exact lines (W06) take over per body
after the kernel swap (V8).
- display_style visible or visible_hidden; tangent_edges hide or show; chord_mm, the paper chord
the meshes are asked for (0.001 to 1, 0.01 by default). The model chord is chord_mm over the
scale: the view DXF's 1 mm cap doesn't apply, so a view below 1:100 keeps its chord on paper;
its 0.0001 mm floor does, and a chord under it is refused with the chord_mm that would do.
- max_error_mm is a check, never a setting (the accuracy rule on #5866): refused while any piece
is sampled, with the count of pieces short of it.
- The three view DXF changes moved here from V1: each segment keeps its owner (its placement and
its edge or silhouette face); a stretch two sources share belongs to the nearest visible one,
else the nearest hidden one (level ones in owner order), ends where that changes, and counts as
covered; the triangles keep their faces for that. The view DXF asks for neither, so it merges and
writes as before. (Nearest, not first: a hole seen end on has its bottom rim under its top one,
and by owner order the bottom rim took the stretches it shows through, cutting the top rim up.)
- A mesh side between two faces every edge between which is drawn is no silhouette: it lies on one
of those edges, already drawn from its own curve. (Seen end on, a hole's wall faces neither way,
and the side it shares with the top face drew stray chords just inside the rim.) The triangles
keep their faces for it in both commands (4 bytes each, in the memory budget); without them the
view DXF drew a hole's rim twice, and a view DXF test now finds every line drawn once.
- A triangle seen edge on, its normal at most 1e-3 (cosine) toward the view, faces away whatever
sign the mesh's own error gives it, so no silhouette lies between two of them. (Seen end on, a
bend's surface drew its rows as chords beside its arcs: a 60° bend's outer arc 1.76 times over.
A view DXF test now finds each of its arcs drawn once.)
- Mesh positions within the tolerance weld: a position takes the key of the first one within it,
in its own rounding cell or a neighbouring one. (The faces round a hole's rim are meshed apart,
their copies of a rim vertex up to about 1e-7 mm apart, and copies either side of a cell
boundary never met: each copy's side was a boundary, drawn as a silhouette.) The view DXF takes
these three rules too; they change its lines only where a side lies along a drawn edge, a
triangle is seen edge on or a weld was missed. The sample designs' view DXFs lose lines, and
each line they gain lies within 0.0011 mm of an old one.
- One source's stretches join into runs within twice the tolerance the lines were merged at: the
merge drops spans up to that long, so a stretch can stop that short of a corner on either side.
- The tangent test is a stadium of cubic segments: the kernel marks a join smooth only between
faces swept from cubic segments, and one between two pieces of one surface is a seam, never
drawn; a fillet's round keeps its edges, as the viewport draws them.
compile fix by the personal PC
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing views: a circle facing the view is drawn as its exact arc (#5866, W07 V3)
An edge that is a circle, or part of one, in a plane parallel to the view
is drawn as its exact arc from Body::planar_edges instead of the mesh's
chords. Where the mesh projection says its visibility changes, the arc is
cut there: a piece that runs along all of the edge is analytic, a cut
piece keeps the mesh's sampled accuracy; its ends may stop within the
stretches' join tolerance of the edge's own, as the merge leaves them.
planar_edges is asked once per shared mesh and placement turn, for the
curved edges only; a part placed again the same way takes the arcs
shifted on the view's plane.
compile fix by the personal PC
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing views: a view's visible lines as a paper scene group (#5866, W07 V7a)
The paper scene gains a kind for views (ObjectKind::View, layer VIEW).
drawing_view::paper_group turns a view's lines into its group: each
visible piece one stroke, 0.5 mm wide until D2 sets widths by kind of
line, closed when it ends where it starts; a line with no length is left
out and an arc too short to sweep is its chord. W14 calls it for saved
views.
drawing.view.snapshot takes scene: true and replies with the lines as a
paper scene of its sheet, checked by Scene::validate before it is
returned. Hidden lines need dashed strokes (A5, #7430), so scene with
visible_hidden is refused until V7b.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing views: a view draws the occurrences its source names and hide leaves out (#5866, W07 V4)
drawing.view.snapshot takes source: {occurrences: [[ids]]}, the occurrence
subtrees it draws ([] the design's own bodies; every body when not given),
and hide: [[ids]], subtrees left out. A body is drawn when its path is under
a source path and under no hide path, so a hidden path wins over a source
path above it. The rule is one filter on the shown set (ShownSet::keep), so
the extents, the lines and the paper scene follow it; each body costs one
look-up per head of its path, at most 64.
Each path is checked by Session::occurrence_placement as it is (each id in
its parent, no id twice, 1 to 64 ids), with at most 1000 paths in each list
and 4096 ids in all. [] in hide and a choice that leaves nothing are
refused. M2's occurrence tree will make the check one step per id later,
inside occurrence_placement.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing views: a bend seen end on draws its arcs once at any angle (#5866, W07 V4b)
A triangle whose surface is seen edge on at each of its corners (the mesh's own normals, at most
1e-3 toward the view) now faces away, whatever lean its facet has. A thin triangle's facet leans by
the mesh's chord error over its own width, which can be far past the 1e-3 a facet may lean and
still count as edge on. Seen end on, a 90° bend's surface is meshed with its edges' samples at its
ends and its rows at even angles inside, 1.667 mm in; the triangles that join the two lean 1.1e-3
to 5.8e-3, so each one facing the view among its neighbours facing away drew silhouettes along its
sides: chords beside the arcs, 1.14 times the inner arc and 1.69 times the outer. The view DXF
takes the rule too.
The export meshes keep their normals for it: 24 bytes a vertex, a position's size again, counted
in the export's 512 MB mesh budget, so the largest view that budget takes is about 30%
smaller. A mesh without normals keeps its facets' facing.
Tests: the bend test takes a 90° bend beside the 60° one; each bend circle's lines add up to its
arc, less only what sampling takes off.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* cell: rules both kinds of table share (#3874)
Sheet tables (parts lists first) use the title blocks' cell content, so
the rules both read live in cell.rs:
- Source::Row: the row of a sheet table a cell is on. A title block has
no rows, so its validation refuses a cell that reads one.
- Warning::Width: a word wider than its value box, shown whole, never
cut: the width warning that measured text brings.
- is_snake_case moves here from title_block.rs: table columns are named
by the same rule as cells. check_text is shared the same way.
Tests: a title block cell reading a row is refused, beside other parts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 640cc3e0f5)
* Sheet tables: the parts-list table model and layout (#3874, P7a)
A Table is a sheet table's definition: its columns, the side its heading
is on (top, or at the bottom with the first row just above it, ISO
7573), its row height (7 mm by default), text height, line widths, a
filter and overrides. A column's cells are the title blocks' cell
content read against each row: row fields and text. Rows are input, in
their listed order, with their values worked out and formatted, so a
filter (parts, assemblies, purchased, text per column ignoring case)
hides rows and never changes a count or a quantity.
Table::layout places it with a text measurer, from the right end of the
heading's outer edge:
- a column without a set width is as wide as its widest line, padding and
line insets added; a set width is kept;
- every cell in a set-width column breaks between words to fit, and its
row grows to hold its lines, 10/7 cap heights apart (ISO 3098-0); a
word wider than its column is shown whole with Warning::Width;
- a row field the row doesn't have shows "-"; an override shows its text
and keeps the value it replaces, an empty one is a blank cell, and one
for a row that has gone is kept;
- cells sit inside their lines as a title block's do.
Limits, refused and never truncated: 10,000 rows and overrides, 64
columns, 1 MiB of text in all (each cell's text counted before it is
built), 256-byte keys, a measured width that isn't finite and at least
0, a table off the paper's coordinates.
Nothing is saved or drawn yet: the real measurer, DXF and CSV output and
the command come with P7b.
Tests: hand-worked cell corners and text anchors with a fixed-width
measurer, the heading at the top and at the bottom; wrapping and the
row it grows; a word too wide; overrides, blanks, "-"; each filter
leaving every shown value as given; JSON round trip, defaults and
unknown fields; every limit and one past it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 0c5180267d)
* Revision record and table (#3874, W31, P14a)
A drawing's revisions as data (geom::revision): one RevisionRecord for
the whole drawing, or per sheet with each sheet numbered apart.
- A row holds what people type: revision, description, designer,
checker and date. Adding a row fills in only the next revision;
nothing fills in a date, a checker or an approval.
- Numbers run from 1, or 0; letters run A to Y without I, O, Q, S, X
and Z, then AA, AB. The next revision follows the highest in its scope
among the rows and the issued history, so an issued revision is never
given out again. A typed revision is kept as typed; in letters one
with a skipped letter is accepted with a SkippedLetter warning.
- Rows are added, hidden and deleted; issue() appends a copy of a row to
the issued history, which nothing removes or changes (private, read
through issued()). Row and tag ids come from the caller (the
design's random ids), never from a counter, so two people adding rows
never take the same id; an id in use is refused.
- Triangular tags bind to a row id and show its revision or their own
text; deleting a row reports its tags, which keep the revision they
showed, marked lost.
- The revision table is the sheet table (table()): Revision,
Description, Designer, Checker, Date, heading at the top; hidden rows
are left out. The table's rows and a title block's revision fields
(newest(), hidden rows included) read RevisionRow::field, one path.
This is the drawing's revision only: the model's revision and the
export time are kept apart. Release, approval and frozen snapshots
(G16/G22) are not in this slice. Nothing is placed or saved on a sheet
yet (W12, W15).
Tests: numbering both ways with the skips and past Y and YY, typed
revisions and the warning, the next after issued and deleted rows, per
sheet; add fills in nothing else; history kept through hide and
delete; tags reported and lost; the title block's fields and the table
rows from one function, laid out by the sheet table; JSON round trip,
unknown fields and damaged records refused; the caps; an id in use refused.
- An issued copy keeps the model version it was issued against
(model_version, given to issue(); none until version control gives
versions, #2869), so the release step has its field here. It is read
by the label rule, like the revision.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit fa7849ada7)
* Drawing revisions: rows added, edited, deleted and listed by command (#3874, W31)
A drawing saves one revision record for all its sheets (Drawing::revisions, behind an Arc as the
sheets are, so an undo step shares it; validated with the drawing; its ids among the drawing's ids
in use), in the order its rows were added, the newest last. Pending drawing format: revisions.
- drawing.revision.add {set?}: a row with the next revision, after the highest the drawing has
given, and any texts given: one undo step.
- drawing.revision.edit {row, set: {revision?, description?, designer?, checker?, date?,
hidden?}}: the texts as typed (a revision is one line) and what changed; a revision in letters
with a letter the lettering skips is kept and warned about.
- drawing.revision.delete {row}: the row, and the tags it leaves showing the revision they had.
- drawing.revision.list: every row, the oldest first, and the revision a new row gets.
An id's text form (13 characters of lowercase Crockford base32) moves down to solidcraft_geom::ids,
with an option helper, so the revision record (geom) writes its row and tag ids as every drawing id
is written; solidcraft_doc::ids gives them on, beside IdSource.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing revisions: listed by drawing.inspect, a table row keyed by its id's text, and a record per sheet tested (#3874, W31)
The rows show in drawing.inspect beside the sheets, as `revisions: {rows, next}`, from the same
function the revision commands answer with; drawing.revision.list goes, so a drawing's rows have one
place to be read. A revision table's row is keyed by its id as a file writes it, the one spelling
every drawing id has (a table's cell overrides name a row by its key). A record kept per sheet whose
rows keep its own rules is refused by the drawing, so that rule has a test of its own.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Persistent naming: a proposal from a second implementation
A design proposal for the persistent-naming failures still open (#4341,
#5198, #5308, #5857, #5858, #5860, #5861), from building persistent naming
in Form Studio: fall back by lineage, never by geometry alone; a suppressed
maker is not a lost reference; names saved as structured one-level values;
ordinals from the feature, not the kernel's face order; split picks resolve
to all pieces; body identity apart from the display name. Documentation
only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Persistent naming proposal: body identity follows the #5308 plan
Change 6 now agrees with bluecmd's plan on #5308: an id minted from
next_id and stored per feature slot, better than the derived identity
the first version proposed. It adds that a stored id is only as stable
as the slot assignment, which today follows each lump's least face
name, and gives the lineage signature from the #5308 discussion.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #4515
Co-authored-by: Cursor <cursoragent@cursor.com>
* Inspect: keep analysis legends clear of a left History (#4515).
Draft, Accessibility and Curvature legends were painted at the viewport's left edge under a left History sidebar. Place them in the free workspace content instead.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
offscreen_device() asked wgpu for its default adapter and ignored the
SOLIDCRAFT_GPU policy that headless pictures follow, so with
SOLIDCRAFT_GPU=software the ui-egui offscreen tests still opened the
hardware GPU. Under hardware or software the adapter now comes from
solidcraft_gpu::select (new pick_among, which pick uses too); under cpu
there is none; unset (auto) keeps wgpu's default adapter as before.
Part of #9040.
Claude-Session: https://claude.ai/code/session_013WUFq6QFBqmZjxwaKy2J1M
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #9147
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Sketch: the Sketch Grid box in the Sketch panel turns the sketch's grid off (#9147)
The box sets the open sketch's hide_grid, but the viewport drew the grid
whenever a sketch was open, so hide_grid only reached the ground grid and
the box did nothing. An open sketch now shows its grid whatever the Grid
switch says, unless its own Sketch Grid option is off; the window guides
that renders and the control channel use follow the same rule.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Drawing balloons: the record, its rules and the sheet hooks (#3874)
A balloon points at a part placed in its view's model by an edge of it, saved
by name as a dimension's edge is (ModelEdge), with the occurrence path as the
part it numbers, and sits at a place on paper from its view's centre. The
dimension module's reference checks and strict lookup are shared, not copied.
Balloons go with their sheet, list in on_sheet and take ids from the drawing.
One pending drawing format file; a drawing without balloons saves as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing balloons: add, edit and inspect, with their schemas (#3874)
drawing.balloon.add puts a balloon on a saved view, pointing at a placed part
by an edge picked as a dimension's is and saved by name; edit moves it on
paper; inspect says where it stands with the model now, with the arrow's
point on its edge's middle. Add and edit are one undo step each, and none of
them evaluates the model. The status words come from one function shared with
the dimension commands.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing balloons: a copied view takes its balloons, and views and sheets list them (#3874)
A view's balloons are part of what it shows, so copying a view copies them,
with ids of their own, as its dimensions are copied. drawing.view.inspect
lists a view's balloons, and deleting a sheet names the balloons on it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #7521
* fix: headless pictures return their draw counts with the picture
`the_renderer_counts_what_it_draws` drew with `Capturer::rgba` and then
read `Capturer::last_stats()`. The capturer is shared with the binary's
other tests, which draw on it from parallel threads, so another picture
could land between the two calls and the test read its counts (e.g. 10224
triangles for the empty design).
`Capturer::rgba_with_stats` returns the picture and its counts, read while
the call still holds the device. `last_stats()` is removed; this test was
its only caller (the performance overlay reads the viewport renderer's own
stats handle).
New test `pictures_drawn_at_once_each_count_their_own` draws empty and box
designs from eight threads on the shared capturer. Against the old pair it
failed 25 of 40 full runs of the `capture` binary; it passes with the new
call.
* fix: a headless picture waits for its read-back's answer
A picture maps its read-back buffer, waits for its submission with
`device.poll(PollType::Wait)`, and then checked once with `try_recv`
whether the map callback had run. wgpu calls a finished mapping's
callback from whichever thread next maintains the device, in a poll or
in any `queue.submit`, after it lets go of the device. When another
thread got there first, the wait returned with the mapping done but its
callback not yet called, and the picture failed with "not supported yet:
reading a picture back without waiting for the browser", the error meant
for a browser.
On the shared capturer, the other thread is another picture's device
check (`probe_lost` submits nothing before it takes the capturer's lock),
so the `capture` tests failed now and then: `bad_picture_sizes_are_errors`
(its last 8 x 8 picture), `pictures_show_a_plain_cut_without_the_hatch`,
`pictures_blend_a_line_that_is_not_opaque`. In the app, the window's
frames are submitted on the device its thumbnails and `ui.render`
pictures are drawn on.
A native picture now waits for the answer, up to the picture timeout; a
browser still answers later, on its own.
New test `pictures_read_back_while_another_thread_uses_the_device`: 2,000
small pictures while another thread polls their device. Without the fix
it failed 19 of 20 runs (1 to 7 pictures each); with it, 150 of 150 runs
pass. `bad_picture_sizes_are_errors` now prints the error it gets.
* Release 0.27.1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Render job test: wait for the cancelled job's reservation to be released
The worker drops a job's reservation just after the job turns terminal, so
checking admitted bytes right after wait_timeout raced (10 of 40 local runs
failed). Poll with a deadline, as dielectric_tests does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Drawings saved inside the design's file, in both forms (#3890)
K3b of the drawing storage plan, after #7098: a drawing lives in the file of the design it draws
(bherbruck's proposal, Cameron's choice), so the separate drawing file goes.
- `Document.drawings`: the design's drawings, each its own record of the drawing family
(`solidcraft-drawing/N`), each behind its own `Arc`. Every reader and writer of a design now
carries them: save, Save As, autosave and recovery, crash copies, the CLI.
- A design container keeps each drawing in its own entry, `drawings/<id>.json` (Deflate), beside
`document.json`, which holds nothing of them: its bytes, and the model cache keyed to them, are
the same with drawings or without. A plain JSON design keeps them inline under `drawings`.
- Each drawing is read by its own family's rules, taken out of the design's JSON before the design's
own checks. One a build can't read (a newer number, a change it doesn't know, damage) refuses the
whole design, never dropped. So does an entry under `drawings/` that isn't a drawing's, one whose
record has another id, drawings inline in a container's `document.json`, two drawings with one
id, or more than 1,000 (counted before any is read). A design is never saved with two drawings of
one id.
- Drawings are listed by name, then id: no stored order, nothing to merge when two people add
drawings.
- A drawing has an `id`, unique among its design's drawings (`Drawing::new` takes the ids already
taken). Its source is the design it is in: at most one, an id and no path.
- One pending design change, `drawings`: the last release refuses a design saved with it, so it
never opens one and saves it without its drawings. One pending drawing change, `in-design`, for
the record's new shape.
- Gone: `read_drawing`, `read_drawing_file`, `write_drawing_container`, `file_kind`, `FileKind`,
`DRAWING_EXT` and `is_drawing_path`. `read_document`, `read_design` and the container writer are
as they were before #7098, with the drawings added.
Tests: a design's drawings round-trip through both forms byte for byte, with `document.json` and
`container.json` the same with drawings or without; their order; every refusal above; a drawing's
properties kept as other tools wrote them while a sheet's number out of range is refused; the
drawing's id among its objects', a new drawing's id none of the design's; a design with two
drawings of one id not saved; mutation fuzz of a design with drawings in both forms.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing commands on the design's own drawings, each drawing its own undo (#3890)
K4' of the drawing storage plan, on K3b (a drawing lives in its design's file): make, list, open,
close and delete a design's drawings, and read and change the open one's settings and properties.
It replaces #3891's drawing sessions, which held a drawing as a file of its own.
- `drawing.create {name?, language?, units?}` adds a drawing of the design (one ISO A3 landscape
sheet with the ISO border, the design its one source) and opens it; refused on an empty design.
`drawing.list`, `drawing.open {drawing}`, `drawing.close`, `drawing.delete {drawing}`.
`drawing.inspect`, `drawing.set`, `drawing.properties`, `.properties.set`, `.properties.remove`
work on the open drawing. Labels in en, ja and zh.
- The open drawing is the session's (`Session::open_drawing`, kept in checkpoints), and the part
gate keeps the parts apart: a command says the part it works on, the model unless it says
otherwise (`.in_drawing()`, `.any_part()`). While a drawing is open the model's commands wait
("a drawing is open: close it to change the model"); the drawing's run only while one is.
Files, recovery, undo and redo, checkpoints, selection, diagnostics, the command list and
choosing a drawing work either way. `CommandSpec::allowed` is the one check running a command,
running it inside another and listing it make.
- `Session::drawing` and `Session::edit_drawing` are the open drawing's one way in: an edit is kept
when it changed something and still keeps the record's rules, in a new design whose other
drawings are shared.
- A change that only touches drawings evaluates nothing: `Document::differs_only_in_drawings`
joins the render-only path, as render settings and part properties do. A model edit shares
every drawing, so it pays nothing for the check.
- Undo per part: a step records the part it changed (`Snapshot::part`: a drawing, or the model
and its list of drawings). Undo and redo go through the open part's steps only. A drawing's
step puts back only that drawing; the model's step puts back the design as it was, keeping each
drawing it had as it is now. Making and deleting a drawing are the model's steps. A new step
ends what could be redone in its own part only. `Snapshot::of` is the one place a step is
taken.
- An agent's message that changed several parts keeps their steps apart, rather than merging them
into one step that would undo only one part.
- `cmd::metadata`'s rules for reading, setting and removing properties are written once
(`reading`, `setting`, `removing`) and used by the design's and the drawing's commands.
- `drawing::sort` is the one place the drawings' order (by name, then id) is kept;
`Document::put_drawing` adds or replaces one in that order.
Tests: making, listing, opening, closing and deleting; what making one refuses, at the cap too; the
gate both ways, inside another command and in the command list; drawing edits as undo steps that
evaluate nothing over 100 rounds; undo and redo per part with model and drawing steps interleaved;
redo ended per part; the order by name, then id, after renames; delete and its undo; refused
changes; checkpoints; save and open in both forms; properties; hostile values for every command
that runs with a drawing open, from the registry; a fuzz of the steps in any order.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Unknown-command tests: the closest id first (#3890)
drawing.open now joins doc.open in the suggestion for `file.open`, as the rule says: both end in
the same word. The engine's and the CLI's tests check that doc.open comes first and let other
close ids follow, as the `solid.extrud` line beside them already does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Model bodies: their extents seen on a plane, for a drawing view's size (#5866)
ModelBody::mesh_bounds_in(plane) is the box of a body's mesh in a plane's own coordinates
(Plane::to_local), beside mesh_bounds and in the same way: a body meshed already is measured from
its own mesh, and a placement of a part from its definition's shared mesh, with the placement
folded into the plane's two axes once (two dot products a point), so no moved copy is made. W07's drawing.view measures a view's extents with it.
Tests: a 100 x 50 x 20 block seen on the three principal pairs of axes and from an isometric
corner (150/sqrt(2) by 190/sqrt(6), worked by hand), a plane away from the origin, and every
placement in the shared-mesh test against its moved mesh, still without a moved copy, and the
same again from its own mesh once it is meshed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing views: projected views in first or third angle, lined up with their parent (#4816, W17a)
W17a's second slice: the principal views derived from a base view.
- `ProjectedSide` (left, right, above, below): the side of its parent a
projected view sits on.
- `projected_basis(parent, side, projection)`: in third angle a view shows
the side of the model it sits on (one to the right looks from the right), in
first angle the side away from it (ISO 5456-2). It's the parent's basis
turned a quarter turn about one of its paper axes, through its origin, with
no -0. From the front view it gives exactly the named views: right, left, top
and bottom in third angle; left, right, bottom and top in first.
- `ViewFrame::aligned(side, projection, centre, position_mm)`: a projected
view at its parent's scale, lined up with it, so it moves only along the free
axis.
- The projection is S2's `cell::Projection`, the value the title block's
symbol reads, so a drawing has one first-or-third setting.
Tests: the front view's projections are the named views in both angles, with
no -0; from every named view and a leaning one, a projected view looks from
its side and keeps the parent's other axis and origin, four turns or a turn
and its opposite bring the parent back; a box's corners line up across
aligned views in both angles; hostile parents, centres and positions refused.
Corner (isometric) projected views wait for one convention check on first
angle. The derivation graph (projected from, aligned to, cycles refused) moves
to the view record in the drawing file, where the view ids live.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing dimensions: measurement on view frames (#4896, W19a)
`geom::drawing_measure` is what a dimension on a view reads, in model mm or radians, from model
points and edges and the view's basis: one rule for every dimension. The view's scale and place
on the sheet never enter.
- `linear`: horizontal and vertical along the view's paper x and y, aligned as the view shows
the segment, or along a direction picked on the view; a true reading is the segment's own
length (aligned only).
- `angle`: from the view's horizontal or vertical, a model axis as the view shows it, or
another edge, from 0 to π as each line is oriented; a true reading is between model lines.
- `foreshortened` says a measured line isn't parallel to the view's plane.
- Refused with a reason: points that aren't finite or are past 1e12 mm, an invalid basis, a
zero-length segment, a line seen end-on, a zero direction, a zero reading, parallel lines,
and a true reading where there is none. `drawing_view`'s `PARALLEL` and `require` are shared.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* drawing.view.snapshot: a view's scale, place and boundary on its sheet (#5866, W07 V2)
The first slice of W07: one orthographic view of the design, worked out and returned as data,
before any line is drawn, so scripts can lay views out by their boundaries.
- `drawing.view.snapshot` takes the view's settings, the sheet it goes on (A3 landscape at
300 dpi when not given) and where its centre goes (the sheet's centre when not given). With no
scale it takes the largest ISO 5455 scale at which the view, centred there, lies inside the
sheet's drawing border. It returns the resolved view, the scale (and whether it was fitted),
the extents on the view's plane, the boundary on the sheet and what it was drawn from.
Read-only and not undoable; it opens nothing and saves nothing.
- Every body the design places is in the view, wherever its occurrences place it; the browser's
eye counts for nothing (`engine::drawing_view::extents`, from P2's `mesh_bounds_in`). A design
whose assembly walk stops at its limits (deeper than 64 levels, or past 100,000 placements) is
refused rather than drawn with parts left out: `Document::placements_complete`, beside the walk,
says whether every placement it holds had its occurrences walked.
- `doc::drawing_view::ViewSettings` is the view's one parameter set: the view (named, or a
direction and an up), the scale, the two visual styles, the reference settings and tangent
edges. A saved view (W12) keeps the same fields; no scale is written as none, not null, so
what it writes reads back as the snapshot's parameters.
- `render::VisualStyle` names the five styles: visible, visible_hidden, shaded, shaded_visible
and shaded_visible_hidden. The shaded ones are refused as not supported yet until the drawing
renderer (#5624).
- The command has a typed contract (input and output schemas). Its handler reads the contract's
own type through `schema::read`, in the contract check's own words, so the check's levels give
one message for one mistake and an unknown field is refused by name. The reply includes the
sheet, its defaults filled in.
Tests: the 100 x 50 x 20 block's boundary in the front, top, right and isometric views at 1:2,
worked by hand; the fitted scale on A3 and A4 and near the border; scale, position and margin;
the browser's eye ignored; the design untouched; each hostile setting refused; the walk's
depth limit one level either side; the contract's keys the same as the settings' own.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing views in the drawing record: base and projected views, and the projection (#4816)
E4 of the view-boundary plan, with #4816's derivation graph (plan on #4816).
A drawing now saves its views, as one pending drawing-format change
(`drawing/format/pending/sheet-views.rs`):
- `Drawing.views`: each view an object of the drawing, on one of its sheets.
A base view draws a source from a direction (`ViewDirection`, as
`drawing.view.snapshot` takes it); a projected view sits on one side of its
parent. Its place is a point of the sheet, or, lined up with its parent, only
how far along the free axis, as the other comes from the parent. A projected
view's source and basis come from its chain, so neither is saved.
- `ViewOverrides` beside V2's settings: what a view sets itself, each setting
on its own (scale, display style, reference style, reference parts hide the
model, tangent edges); none is written when left out. Each view has its own
reference margin.
- `Drawing.projection`: first or third angle (S2's `cell::Projection`). Third
angle is the type's own default (Cameron, 2026-10-26), for a drawing that
doesn't say and for a new one until a template or a preference chooses.
- The rules, checked on reading and after every edit: a view is on one of the
drawing's sheets; a base view draws one of its sources, has a scale and a
direction that makes a view; every parent is a view, no chain of parents
loops, and a chain is at most 32 views; a lined-up view is on its parent's
sheet; a place is on paper; at most 4,096 views. A view's id is the
drawing's, never another object's.
- `Drawing::resolve_view`: one rule for every reader. It turns the base view's
basis down the chain (`projected_basis`, in the drawing's projection) and
takes each setting from the nearest view that sets it, then the default,
saying who gave it.
- geom's `is_paper_coordinate`: the one rule for a view's place on paper, now
used by `ViewFrame::new` and the record.
The record tests' seed drawing has views, so its round trip and mutation tests
cover them; their unknown-field examples no longer use `views`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Engine tests: one temporary folder helper and the schema check for every test module (#4816)
The view tests use the drawing tests' temporary folder and the schema tests' `conforms`, which were
private to their modules. `Folder` moves to the engine's test module and `conforms` is open to the
crate.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing views: inspect and edit a saved view (#4816, W17c)
E5 of the view-boundary plan (plan on #4816): what the view's window reads and
writes, for the views of a drawing (E4). Both are drawing commands with
input and output schemas, and neither evaluates a model.
- `drawing.view.inspect {view}`: its kind (base or projected), sheet, source,
parent and side, its direction as resolved (named only when it is exactly a
named view), its place, and each setting as {value, from, parent?, set}:
where the value comes from (the view, a parent by id, the default) and what
the view saves itself. A projected view says whether its display settings
follow its parent (`linked`: true, false or "partly"). `choices` gives the
display styles and the preset scales, so a window keeps no list of its own.
- `drawing.view.edit {view, set}`: one undo step. Each setting takes a value,
or null to follow the parent (a base view keeps a scale); the margin, a base
view's direction and the place change too. `linked: false` saves the display
settings at the values they have, `linked: true` clears them so they follow.
The reply names the fields changed and the views projected from it whose
settings changed with it. The record's rules hold after every edit; an edit
that changes nothing is no step.
The contract test keeps the edit's fields and the record's (ViewOverrides) one
list. The view's boundary comes with the next slice, which works out where each
saved view sits on its sheet from its design's extents.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing views: where a saved view sits on its sheet (#4816)
`Drawing::view_centre` gives a view's centre on its sheet: its point, or, lined up with its
parent, its own place along the free axis and the parent's centre across it. The free axis is
one rule on `ProjectedSide` (`along`, `on_free_axis`), which `ViewFrame::aligned` uses too.
`engine::drawing_view::on_sheet` puts a saved view on its sheet: its design's extents on its
plane, grown by its reference margin, at its scale, round its centre. The snapshot and saved
views share `measure` (refusing an assembly past the walk's limit or one with no bodies) and
`boundary`. With nothing to measure (the design has no bodies or places more than a view walks) a
view gets a 20 x 20 mm placeholder round its centre. A view's source is the design the drawing
is in, so the session's own model is what it measures.
`drawing.view.inspect` adds `boundary_mm` and `extents` (status `placeholder` and why, when there
is nothing to measure). It still evaluates no model.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Projection of new drawings: a preference, and drawing.set takes the projection (#4816)
A drawing's views follow its projection, first or third angle (third when nothing chooses).
`drawing.inspect` now shows it and `drawing.set {projection}` changes it as one undo step.
Preferences > Design gets a Drawings section with "Projection of new drawings"
(First angle / Third angle). A new drawing takes it as a new design takes "Units of new
designs": the app sets it after `drawing.create`, with no undo step of its own. A saved
value this version doesn't know reads as third angle; asked for over the control channel,
it is refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing sheets: one edit, one lookup, a border that lays out, and schemas (#3875)
The sheet commands change the drawing through `Session::edit_drawing`, so the same settings or
order again leave no undo step, and answer from one reply type. A sheet is looked up by id or
name on `Drawing` (`sheet_index`), where later commands that name sheets find it too.
A sheet's border must lay out on it: the record's check runs the border's own layout, the one
rule drawing uses, so a 15 x 15 mm sheet with the ISO border is refused when it is made, not
when it is drawn. `excluded` must be an object of `count` and `print`: a mistyped key changed
nothing unseen. A new sheet documents nothing and leaves nothing out unless told, and the add's
parameter documentation says it takes `documents` and `excluded` too.
The drawing's properties sit behind an `Arc`, as its sheets do, so an undo step shares them.
Each sheet command has an input and an output schema (#4035). A flattened part of a contract
reports its keys as the parent's own (`schema::flattened`, which `FeatureCommon` now uses).
The tests add the missing cases: 1000 sheets and the 1001st, `after` by id and unknown, the
resolution's bounds, a name two sheets share, the same order and settings again, an undone
delete in place, and the add's reset of what it documents and leaves out.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing ids: one id source per session, its seed kept for replays (#3890)
The drawing commands drew ids from a fresh random source each time, so a crash replay of a drawing
could never find what an earlier command made by its id. A session now has one id source
(`Session::ids`), kept across a file change, from which `drawing.create` draws its drawing's,
sheet's and source's ids: seeded alike and run alike, a session gives the same ids, and a failed
command draws none, so a replay of what succeeded draws the same. The crash journal keeps the seed
at each base and `replay.json` carries it, which `solidcraft-cli crashes replay` seeds the replay
with (`Session::seed_ids`).
Tests: two sessions seeded alike make a drawing with the same ids, another seed other ids.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing sheets: add, change, reorder and delete a drawing's sheets (#3875)
W18 S4: the sheets of a drawing, on the drawing session K4 made (#3890). Each command is one undo
step of the drawing and names a sheet by its id or, when only one has it, its name.
- `drawing.sheet.add {after?, name?, size?, orientation?, dpi?}`: a new sheet after the one named
(the last when not), taking that sheet's paper, border and resolution, with a name of its own
("Sheet 2") and nothing documented yet.
- `drawing.sheet.set {sheet, name?, size?, orientation?, dpi?, documents?, excluded?}`: one
setting at a time, the rest kept; `documents: null` clears what it documents, and `excluded`
changes only the flags it gives.
- `drawing.sheet.reorder {sheets}`: every sheet's id once, in the new order.
- `drawing.sheet.delete {sheet}`: a drawing keeps at least one sheet.
- Every change goes through the drawing's own rules (`Drawing::validate`), so what a saved file
would refuse is refused here. Changing a sheet's paper changes only the paper: regrowing its
border and moving what is on it is S5's. Labels in en, ja and zh.
Tests: a new sheet copying the paper it follows, after a named sheet too, and undone; each setting
changed with the others kept; nine refusals (resolution, sizes, orientation, name, an unknown
source, a flag, an unknown sheet, no sheet named) with the drawing unchanged; reordering, and four
orders that aren't every id once; a name two sheets share refused in favour of the id; the last
sheet kept.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing sheets: a new sheet's id from the session's id source (#3875)
`drawing.sheet.add` draws its id from `Session::ids` (#3890's one id source per session), so a
crash replay of "add a sheet, then change it by id" finds the sheet: seeded alike and run alike, a
session adds sheets with the same ids, and a failed add draws none.
Tests: two sessions seeded alike add the same sheet ids, another seed others, a failed add none.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing views: move or copy a view to a sheet (#4816)
`drawing.view.move_to_sheet {view, sheet, position_mm?, break_alignment?}` puts a saved view on
another sheet as one undo step, keeping its id, direction, source and settings. The views lined
up with it go with it and keep their places beside it; a view placed apart stays. Without a
position it keeps its place. A view lined up with its parent moves only with `break_alignment`,
and is then placed apart: at the position, else the sheet's centre. Its own sheet changes
nothing.
`drawing.view.copy_to_sheet {view, sheet, position_mm?, with_linked?}` makes new views, each
following none of the views it was copied from (Cameron's rule). A view with views lined up
with it needs `with_linked`: true copies them too, lined up with the copy; false copies it
alone. A projected view copied alone becomes a base view, looking the way it did (by name when
it is exactly a named view) with the settings it had as its own. The copy goes to the position,
else the sheet's centre.
A lined-up view's centre comes from the places alone: its place along the free axis, and its
parent's centre across it. So the views that travel with a view are shifted along their free
axis, and nothing needs the design's extents. `descendants` takes which views it follows, so the
edit's followers and the views lined up with a view are one walk.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing views: move and copy take a view's centre from the record (#4816)
Moving or copying a view to a sheet worked out a view's centre with its own copy of the
lined-up rule. It now uses `Drawing::view_centre`, and a lined-up view's place moves along
its free axis by `ProjectedSide::along`, so the rule is written once.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing sheets: a change of paper lists the views it leaves outside, and moves none (#3875)
W18 S5, its first part. `Sheet::drawing_space` is where a sheet's drawing goes: inside its
drawing border, or the whole paper with none. Nothing on a sheet is saved against the border,
so a change of paper regrows the border and its zones by itself.
`engine::drawing_view::outside` lists the views whose boundary (`on_sheet`) leaves their sheet's
drawing space, sides included. `drawing.sheet.set` lists them after a change of paper (size or
orientation; nothing else moves the space, so nothing else is measured), and moving or copying a
view to a sheet lists those it puts outside there. None is moved: where a view goes stays the
author's choice. A view with nothing to measure is listed by its placeholder, with why.
Tests: the drawing space with the ISO border, a filing margin and no border, and margins that
leave none; A3 to A4 either way up listing the right and top views with their boundaries and
moving nothing; a border through a view's edge leaving it inside, 0.1 mm less paper putting it
outside; a change of name checking nothing; undo; a move to a smaller sheet listed and a copy
that fits not; a placeholder listed as `past_walk_limit`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing ids and the sheet border: the tests the slices left out (#3890, #4816, #3875)
What the slices' planted faults found untested, now each caught by a test:
- a file change keeps the session's id source: a drawing made after `file.new` draws the same ids
in two sessions seeded alike;
- a copied view's ids come from the session's id source, seeded alike giving the same copies;
- a crash report's replay carries the session's id seed, from a caught panic (the seed when the
command started) and from the journal of an unclean exit (the seed at its base);
- a view short of the sheet's border by less than a rounding is still inside it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing views: listed by id, whatever order they were added in (#4816)
The record's rule says a drawing saves its later records by id, never in
the order they were added in. Views were kept, and saved, in the order they
were added, so the same views added in another order saved as other bytes.
- `Drawing::add_views` adds views where their ids put them. It is the one way
views are added: `drawing.view.copy` uses it, and so do the test fixtures.
- `Drawing::validate` checks the order, so every edit keeps it.
- Reading puts a file's views in id order before it checks the record. The
order means nothing, so a file in another order (saved before views went by
id, or edited by hand) isn't damaged: it opens, and saves by id.
- What a command lists in the drawing's order (the views a change of paper
leaves outside, an edit's followers) now comes by id.
No format change: no release has saved a view (the views change is still
pending).
Tests:
- views added out of id order are listed and saved by id;
- the same views added the other way round make the same drawing and the same
bytes, which read back as saved;
- a file that lists them the other way round reads by id, as the same
drawing, and saves as those bytes;
- an edit that leaves them in another order breaks the record's rule;
- the existing tests now index the file by id order, and the engine's lists
are by id.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing dimensions: the dimension record, saved by name on a view (#4896, W19b)
A drawing saves its dimensions: each on one of its views, measuring named
corners or edges of the view's model (a linear distance between two corners,
a straight edge's length, or the angle between two edges), with the direction
and reading W19a measures with, how far its line sits from what it measures,
where its text sits on paper, its decimals and any text shown instead of the
number. A reference names its body, the feature that made the body and its
occurrence path, never a picked point. Nothing worked out is saved.
- Dimensions are listed by id, read in that order whatever order a file has,
as views are: one helper pair (add_by_id, in_id_order) now keeps both lists.
- One cap for the names a saved model reference keeps
(MAX_REFERENCE_NAME_BYTES, used where 512 was written three times), and the
corner's edge cap shared with construct's vertex references.
- LinearDirection and Reading save as snake_case words; a direction along a
line saves as its two coordinates.
- A direction along a line is at most 1e9 each way (format::MAX_REAL, the
limit the file's number check holds every real number to): a whole number
past it passes that check, reads as a real, and would save as one the next
read refuses (found by the mutation test).
- One pending drawing-format file (dimensions.rs).
Tests: what each dimension saves and reads back as the same bytes, a file in
another order reads by id, an older drawing has none; every rule on both sides
of its cap, unknown fields at every level, the id rule, and the count cap; the
plate fixture carries one dimension of each kind, so the existing round-trip
and mutation tests cover them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Edges and corners by name: the matching step and the shared ends, each in one place (#4896, W19b)
A dimension finds an edge by name the way a fillet does, but never takes
the piece nearest an old pick, and finds a corner where its named edges
meet, refusing to choose between two. Both steps are split out of the code
that already does them, so there is one rule each:
- BodyEdges: a body's edge and face names, worked out once for every name
looked up on it, and `matching(name)`: the edge with that name, else the
one edge between the faces it names, else the pieces of it. locate_edge
now calls it and then takes the piece nearest the pick, as before.
- construct::shared_ends: the ends every named edge has, each once.
named_vertex keeps the one nearest its point, as before.
No behaviour change: every caller of locate_edge, find_edge and the vertex
references gets the same results. The existing naming, joint and
construction tests cover both paths; three new ones pin what they left
open: a vertex only where every named edge ends, the piece of a split
edge nearest the pick, and a lone match through the stem of a face taken
as a piece, with the warning.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* A body's frame through an occurrence path, in one place (#4896, W19b)
Document::body_frame_at(body, path): the placement the path leads to,
then the body's offset, without expanding every assembly instance. It is
what EmbossFace::body_frame composed for its saved path; that now calls
it, and drawing dimensions use it for the occurrence path they save. No
behaviour change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing dimensions found again in the model, strictly (#4896, W19b)
Document::resolve_dimensions(model, drawing, ids) works out each dimension
as the model has it now:
- An edge by its name, or the one edge between the faces it names
(BodyEdges::matching), never the piece nearest an old pick. A corner is
the one end all its named edges, or their pieces, share; two shared ends
are ambiguous, never "the nearer one".
- The body by its name and the feature that made it, placed through the
saved occurrence path (Document::body_frame_at). A same-named body made
by another feature isn't it.
- Status: ok; overridden (the text given is shown, the value still worked
out); ambiguous (the candidates, at most 16, none chosen); suppressed (its
feature is suppressed or rolled back, read from the evaluation's own
skipped mark); lost (why); unmeasurable (found, but the view can't
measure it so, in W19a's words, such as a line seen end-on).
- The value comes from W19a on the view's basis. An angle is the smaller one
between the two lines, or pi minus it on its other side, whichever way the
kernel runs the edges; a split edge's pieces on one line are still that
line. A split edge's length is ambiguous.
- The text: the override, else length_number in the drawing's unit (or the
design's) with the dimension's decimals, or degrees for an angle.
- The cost: each body named is looked up once per call, and its edge names
worked out once, however many dimensions name it. A body none names isn't
touched.
Not saved: nothing here changes the drawing. The last good value of a lost
dimension needs the session, so it comes with the commands (W19d).
Tests (engine, an 80 x 40 x 30 box, values by hand): front and right view
values, foreshortening and text, the drawing's own unit, several at once
and an unknown id; a change and its undo; a chamfer's 45 and 135 degrees,
and end-on in the front view; lost after a fillet and back on undo,
suppressed and back, a path to no occurrence, a new box with the same body
name; a notched edge's length ambiguous with both pieces while its end
corners still read 80 and its angle to the left edge 90 degrees, neither
exact; an angle on a piece of an edge whose slot is then suppressed still
reads 90 degrees, found through the stem of the face it was cut from, so
not exact; one edge named twice gives two corner candidates;
an override shown while the value follows the model.
Not covered by a test here: a part placed twice through real occurrences
(only a path to no occurrence is); the once-per-body cost (not measured).
A drawing unit outside display_units' five (um, yd, km, mil, nm, inch, µm)
shows millimetre numbers, the formatter's existing fallback; named as its
own follow-up.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing sheets: a sheet with anything on it goes only with it, when asked (#3875)
drawing.sheet.delete refuses a sheet with anything on it, and its
error names what is there: its views and the dimensions in them, the
first few ids of each. With `contents: "delete"` the sheet goes with
everything on it as one undo step, which an undo brings back whole.
What is on a sheet is said once, in the record (Drawing::on_sheet),
and Drawing::remove_sheet takes the sheet out with exactly that; each
later kind of record on a sheet (a sheet's own sketches) adds itself
there, so delete covers it with no change here. drawing.sheet.set's
list of a sheet's views uses the same function.
Nothing on another sheet goes with it: a view there projected from one
of the sheet's views would lose its parent, so the delete is refused
naming those views, to be moved or deleted first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing views: a view found by id by halves, in one place (#4816)
The views are listed by id, so Drawing::view and Drawing::view_index
find one by halves instead of walking the list. Every lookup by id
uses them: a view's chain of parents (so resolving a view no longer
walks the list once for each parent), the views' rules (which no
longer build a map of every view to check each chain), a dimension's
view, and the engine's view commands and outside check.
The rules check the id order before anything looks a view up, so a
file with its views out of order is still refused as before.
Measured on a drawing of 4096 views (1024 chains of a base view and
three views projected one from another), median of 30 runs: 4096
lookups by id took 6.85 ms by a linear find and take 105 us by
halves; the rules' check took 1.77 ms and takes 1.45 ms.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing views: a damaged chain names the view whose parent is missing (#4816)
A view whose chain of parents breaks further up was refused with "its
parent X isn't one of the drawing's views", naming a view that isn't
its parent. Further up the chain, the refusal now names the view whose
parent is missing: "its chain of parents reaches view Y, whose parent
X isn't one of the drawing's views". A missing parent of the view
itself reads as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing dimensions: add one to a view, and read it back (#4896, W19d)
drawing.dimension.add puts a dimension on a saved view of the open
drawing; drawing.dimension.inspect reads one back with what it
measures now. Adding is one undo step of the drawing; neither
evaluates the model.
What a dimension measures is picked and saved by name, never by the
point picked. Document::edge_reference and corner_reference turn a
pick into the record's names: an edge is a point on it in the design's
frame or {body, index} (its index among the body's edges as the model
lists them, as an ambiguous dimension's candidates are given), and a
corner is a point or the edges that meet there, each with the
occurrence path its body is placed through. A pick that doesn't name
one thing is refused, saying what it found: no edge, a point on
several edges, a corner of more than one body, edges that don't meet.
Each body's edges are placed in the design's frame once per pick, and
only the body picked has its edge names worked out.
The kind is given as the record saves it ({linear: {a, b, direction?,
reading?}}, {length: …}, {angle: …}), read by the record's own types,
so a command and a file read the same words. A new dimension's line is
10 mm from what it measures (until a dimension style gives it), shows
2 decimals, and its text is centred on its line; the text's default
place at its centred point (Q58) comes with the layout on paper.
The inspect reply has the record, the status (ok, overridden,
ambiguous, suppressed, lost or unmeasurable, with why), the value
(model mm, or degrees for an angle), whether it is foreshortened, the
text, whether every name was found by exact history, and an ambiguous
dimension's candidates.
One rule, one place: the corner rule's point tolerance is
construct::point_tolerance, shared by the picks; a dimension by id is
Drawing::dimension, which the resolver uses too.
Not here yet: edit, the view's list of its dimensions, the layout on
paper and the last good value (the next slices); delete waits with
view delete for the drawing UI's choice of one delete command or one
for each kind.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing dimensions: change a dimension's settings (#4896, W19d)
drawing.dimension.edit {dimension, set} changes what a dimension
shows and where, as drawing.view.edit changes a view: the offset of
its line, its text's place, its decimals, the text shown instead of
its number (null shows the number again), and its kind's direction,
reading or other side. The reply names the fields that changed. The
same values again change nothing and add no undo step (the drawing is
put back only when it differs). What a dimension measures isn't a
setting: to measure something else, add a new one.
A setting its kind hasn't got is refused (an angle has no direction,
only an angle has an other side); every changed record passes the
check a file read runs, so an edit can't save what opening the file
would refuse. A dimension's place in the list is
Drawing::dimension_index, which Drawing::dimension now uses too, as a
view's is.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing views: inspect lists a view's dimensions (#4896, W19d)
drawing.view.inspect answers `dimensions`: the ids of the view's
dimensions, in id order, so a caller can find them and read each with
drawing.dimension.inspect.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing views: a copied view takes copies of its dimensions (#4896, W19d)
drawing.view.copy_to_sheet copies each copied view's dimensions with
it, each with an id of its own on the copy, in the same undo step:
they are part of what the view shows (the W19d plan's question 1, its
recommended answer). A dimension's text placed `at` a point is from its
view's centre, so it keeps its place on the copy. A moved view keeps
its own dimensions as they are: they name the view by id.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing dimensions: the last good value, in memory (#4896, W19d)
A dimension that can't be measured now (its edge filleted away, its
corner gone) gives `last_good`: the value it last read this session,
as `value` gives it (model mm, or degrees for an angle). Each value a
dimension reads is kept by drawing and dimension in the session, and a
scratch copy keeps the session's. It is never saved (the W19b plan's
question 3), so a design's drawings don't change with every model
edit, and after reopening a lost dimension shows no number.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing dimensions: hostile values anywhere in the commands' params (#4896, W19d)
A hostile value in each place of drawing.dimension.add, .edit and
.inspect's parameters, at every depth (the kind, its picks and paths, a
corner's edges, the edit's settings), with 1 000-item lists, the
largest index and 600-byte text besides the engine's shared hostile
values: no panic, and the drawing keeps its rules. The engine's own
hostile test sends only its list of key names, none of which these
commands read.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing dimensions: a new dimension's defaults, tested (#4896, W19d)
The offset (10 mm), the text's place (centred) and the decimals (2) a
dimension is added with when not told.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* A plane carried by a transform, in one place
`assembly::map_plane` is public and gives `None` for a transform that flattens the plane; its two
copies (the occurrence mapping, which keeps the plane as it was, and the face anchor's, which
refuses) now call it and keep their own fallback. W20a's radial dimensions use it to carry a
view's plane into a body's frame.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing dimensions: a circle's or an arc's radius and diameter (#4896, W20a)
A radial dimension saves its edge by name and whether it shows the diameter. It reads the edge's
exact circle in the view's plane through the edge (`Body::planar_edges`, as the view draws it), so
a view that doesn't see the circle face on can't measure it; its text is R or Ø before the number.
`drawing.dimension.add` takes `{radial: {edge, diameter?}}`, a diameter by default for a whole
circle or half a turn or more (a hole's rim is two halves), and refuses a dimension of any kind its
view can't measure; `drawing.dimension.edit` switches `diameter`, keeping the id.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Sketch: snap where two recent centre and midpoint guides meet (#8694)
The alignment guide remembered one reference and did not move the cursor. Keep the previous centre or midpoint as well, and snap a drawing point to where their guides cross.
* Sketch: keep the recent-guide crossing snap off until asked (#8694)
The crossing snap was on whenever alignment guides were. Give it its own Sketch preference, off by default, so the guides stay visual until someone turns the snap on.
* Sketch recent snap: ar/he/vi strings and clippy borrow fixes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #9072
* MCP: --param-check checks calls to the running app (#9072)
`mcp --connect` (and `mcp` once it bridges to an app) took
`--param-check` and forwarded every call unchecked, while the app's own
session is always off. `Remote` now checks each `engine.execute` and
`engine.batch` against this server's command contracts before sending:
`strict` refuses a call, or a batch with such a step, without sending
anything, and `warn` adds `param_warnings` to the app's reply (each
step's result in a batch). A command with no contract here goes to the
app unchecked. `Auto` carries the headless session's level over when it
bridges to a late app. `batch_preflight::contracts` is the contract half
of the existing preflight, for a runner in another process.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
theme::apply always sets egui's theme to light or dark, and egui's own window
sync then told the native window to be light or dark. A macOS window told so
keeps that appearance and no longer reports the system appearance changing,
so with Theme set to System the app stayed light after macOS turned dark. The
app now syncs the window itself: the system's appearance while the theme
follows it, the theme's own otherwise (for the title bar).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #9025
Co-Authored-By: Claude Code <noreply@anthropic.com>
* Sketch: an old edge-on projection keeps its line when it is re-resolved (#9025)
A face seen edge-on was saved as two lines on the same two points, one each way, and is one line now. Re-resolving the link dropped both lines and built a new one under a new id, so an offset that names the first line as its source failed with "an offset source vanished or changed shape". Keep the first line and drop only the second.
Co-Authored-By: Claude Code <noreply@anthropic.com>
---------
Co-authored-by: Claude Code <noreply@anthropic.com>
Fold the pending design format changes (sketch-equation-curves, timeline-joint-folders) into format 44.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The check for a constraint that shrinks a curve to a point took under 1e-6
for a point. One solve could squeeze a 49 mm side to about 1.2e-6 mm and
the next take it the rest of the way, so neither solve tripped it (two
rectangles' corners made coincident, found by a local sketch fuzzer). Under
1e-5 now counts: in 4000 random sketches the constraints that still
collapsed a line went from 9 to 0, and every refusal rate stayed the same.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #9015
Co-authored-by: Cursor <cursoragent@cursor.com>
* Evaluate bare length offsets in the design's units (#9015)
Feature expressions like baseLength - 1 were subtracting millimetres because val() parsed with mm defaults and only applied design units to a final unit-less result.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* claim: #6921
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* CLI: run refuses unknown options and takes --param-check (#6921)
`solidcraft-cli run` now checks its options through `known_args`, so a
mistyped option fails instead of being ignored, and takes
`--param-check off|warn|strict` with the MCP server's words and default
`off`. `warn` prints each unknown key with a suggestion on stderr and
runs the step; `strict` stops at that step and writes nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* CLI: run's option check takes --param-check; warn on timed runs as steps complete
`run` keeps #6919's (#6918) unknown-option check, with `--param-check`
among the options that take a value. `Session::run_script_timed` now
takes the same per-step callback as `run_script_each`, so `run --timings`
or `--profile` at the warn level reports a step's warnings even when a
later step fails, instead of after the script (and not at all on a
failure).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #8644
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Web: open tabs share settings changes, so one tab no longer undoes another's (#8644)
Every open tab of the web app saved its whole copy of the settings to the
same browser storage, every 30 s and on focus loss, so a tab still holding
old settings put them back over a toolbar change made in another tab.
A tab now writes its settings in the frame they change, and the other open
tabs load them from the browser's storage event, so their own saves keep
them. Settings loaded that way aren't written back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* claim: #9016
Co-authored-by: Cursor <cursoragent@cursor.com>
* UI: keep Distance entry under Preferences and Report a bug (#9016)
Canvas value boxes used Foreground order, so they covered system windows. Draw them as Background chrome instead.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* Sketch Trim: a cut at a curve's own end leaves no piece of no length
A curve that ends where another one ends crosses it there only within the
intersection's tolerance (1e-6), but bracket dropped cuts at the curve's
ends only within 1e-9 of its parameter range. A line ending on an arc's end,
trimmed from its other end, was cut a hair inside its own end and kept a
piece about 1e-7 mm long (found by a local trim fuzzer: 78 such trims in
4000 sketches, and 8 breaks). bracket now also drops a cut within that
tolerance of the curve's ends, as add_cut merges cuts that close together.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Sketch Extend: a touch at an arc's growing end is that end, not a stop
An arc whose end is where a line ends that touches the arc's circle there
found the touch a hair beyond the end (within the intersection's tolerance),
stopped on it, and added an arc from that point to itself. A hit within that
tolerance of the growing end (1e-6, as for the far end) is now the end.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A constraint whose only solution squeezes curves to a point (a rectangle's
corner made the midpoint of its opposite side, or two opposite corners made
coincident) collapsed the whole rectangle while the sketch said solved. A
solve that shrinks a line or circle from over 1e-3 to under 1e-6 now counts
as one that fails, as one that throws points far away does: a strict edit is
refused, another keeps the sketch from before its solve, and the free solve
after the held ones in apply_in_order refuses the constraint.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- Skip drawing and interaction for history chips outside the strip's visible part, after their slot is recorded
- Keep a dragged or focused chip, and every chip while a popup is open
- Frame on a 4,880-body design: 20.3 ms -> 14.6 ms (M4 Max, release, headless)
- Test a_long_timeline_paints_only_the_chips_in_view
Co-authored-by: P'tite Tête <asso@ptitetete.com>
On a Windows account whose %TEMP% is a short (8.3) path, such as
C:\Users\MATHEU~1\AppData\Local\Temp, three tests failed because they built
their folders from std::env::temp_dir(), and the agent file scope refuses short
names ("not allowed for this agent: short (8.3) names such as `PROGRA~1`"):
- engine: cmd::render::settings_tests::embedded_hdr_capture_rejects_parameters_scope_and_cpu_primaries_without_mutation
- ui-egui: agent_chat::tests::file_actions_wait_for_the_user
- ui-egui: agent_chat::tests::permission_modes_decide_what_asks
They now use vfs::temp_dir(), which spells the folder with long names, as
libraries_tests and motion_tests already do (#1385, #1211).
Checked on Windows 11 x64 with the 8.3 TEMP: the agent_chat and
render::settings tests (113) pass, and clippy -D warnings is clean for both
crates.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #7287
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Command search: show a command's place in the ribbon, also inside section menus (#7287)
Hovering a command result in Search commands switches the ribbon to the command's
tab and marks its button with its tooltip; a command that has no button there
opens its section's drop-down on its row instead. Leaving the results (or closing
the box) goes back to the tab the user was on, so pressing S, typing and Enter
works as before.
Starting a command from the box makes its button or section label blink twice,
and a command started from a section drop-down marks that section's label while
it runs, like a pressed button.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Command search: the ribbon reveal is a preference, off by default (#7287)
Preferences › Interface › Show search results in the ribbon (`ribbon_reveal`) turns on the hover
preview, the blink after a start and the running mark on a drop-down's panel label. Off, the
ribbon behaves as on main.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Pending locale: Arabic, Hebrew, Japanese, Vietnamese and Chinese for the ribbon reveal setting
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
* claim: #7912
* Sketch: dimension a sketch entity against a model edge
A dimension took sketch entities only, so measuring a sheet-metal body's edge
was refused with "cannot dimension that combination of entities". The
entities now take a model reference too, an {edge: [x,y,z], body?} or a
{vertex: [x,y,z], body?}, which is projected into the sketch first, as the
constraint commands' operands are since #6348, and measured there.
The Dimension tool picks a body's edge or vertex when the sketch has nothing
under the cursor, and reads the pick back into the reference the command
takes.
* style: rustfmt the sketch dimension command
* Cap the entities one call may project
A dimension projects the model references among its entities into the
sketch, one link and one resolve each, and the loop had no cap: a single
request could make the engine resolve thousands of model edges before
the dimension refused the list. The count is now refused before anything
is projected, and the projection loop stops at the same limit the
Project command uses.
* Sketch dimension on a model edge: use the sketch store after the store refactor
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The cursor correction tries the constraints' free directions a whole step at
a time and took a step as soon as the cursor's distance fell, by any amount.
Along freedom that can't reach the cursor (the far end of a line held only
by its direction), retracting onto the constraints still moves the cursor by
rounding, so such steps were taken: the end walked away a step at a time, and
a line measured by an angle could shrink to nothing, as in #7635. A step now
has to move the cursor itself by more than the solve's tolerance.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #6237
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Settings profiles: save, switch, export and import input and interface settings (#6237)
A profile keeps the Interface, Navigation and Sketch preferences (mouse mode,
radial menu and gestures with every studio's lists, theme, scale, font, ...),
the language, the keyboard shortcuts, the toolbar, the S box favourites and
the imported theme it uses under a name. Machine-specific settings stay out
(3D mouse, graphics, units, AI, recent files).
- Preferences > General > Settings profiles: choose the active profile to
apply it, Save, Rename, Delete, Export... and Import...; a note shows when
the settings differ from the saved profile; Cancel takes a switch back.
- settings.profile.{list,save,apply,rename,delete,export,import} commands for
the control channel, palette and scripts; the engine checks the parameters,
the app does the rest.
- One `solidcraft-profile/1` file; import checks the format and name, drops
unknown keys and unreadable values, caps sizes, never switches, and never
panics. Each preference is applied through prefs::set, so a value this
computer refuses keeps its current one and is reported.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Glossary: settings profile, and "profile" among the words with more than one meaning (#6237)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Settings profiles: new strings in a pending locale file, not the catalogs (#6237)
New UI strings now go to assets/locales/pending/ (CONTRIBUTING.md), so
this pull request no longer edits the shared catalogs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Preferences snapshot: compare the toolbar, favourites and profile too
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Settings profiles: keep a command's several keys (#6237)
Since a command can have several keys (#6288), the keymap saves a list where it has more than
one. A profile's checks only took a single key, so saving or importing a profile dropped such a
command's shortcuts. They now read the keys as the keymap does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Settings profiles: Arabic, Hebrew and Vietnamese strings (complete catalogs) (#6237)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Tripper <tripper@localhost>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
* claim: #8927
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Dialogs: a pre-selected sketch line lying on a body edge goes in as that edge (#8927)
With a finished sketch shown, a click on a body edge that one of its lines
lies on picks the sketch line. Fillet and Chamfer take edges and no sketch
curves, so the pre-selection found no input to fill: the tool opened with
Edges empty and the selection was dropped, and only a second click with the
tool open (which picks the edge) made it work.
When nothing in the dialog takes a pre-selected sketch curve but an input
takes edges, look for the body edge the curve lies on from end to end
(display polylines, with room for a curved edge's chord sag) and take that
edge instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Preferences > Graphics > Viewport gets a Field of view row under Perspective view: the UI kit's
slider in degrees, 15 to 90, dragged or typed and kept to whole degrees, enabled only while
Perspective view is on. The preference is perspective_fov, in radians (default 0.6, about 34°,
so the default view draws exactly as before), and Restore Defaults puts it back.
One place decides the angle the view is drawn with: SolveApp::view_fov(), the preference with
Perspective view on and 0 with it off or while a sketch is open. A saved value outside 15°-90°
reads as the default. projection and fit_bounds read it (fit_bounds' second hard-coded copy of
the 0.6, tan(0.3), now goes through Camera::half_height), and so does the headless fit.
perspective_view() stays, as view_fov() > 0, so #4291 needs no change.
When the angle changes from one perspective angle to another, the camera's distance scales by
tan(old/2) / tan(new/2), with a running view animation's ends, so the target keeps its size and
the slider changes the depth, not the zoom. prefs::follow_view_fov does it once per frame and
after ui.prefs, Cancel and Restore Defaults; a parallel view keeps the last perspective angle,
and while Render is open the camera is Render's own, so the change waits for the design view.
The Render workspace's Perspective check box turns perspective on at the preference's angle,
not π/4; a saved render setup keeps its own. ui.prefs takes perspective_fov and refuses values
outside the range; ui.camera reports the angle the window draws with, and checks a replayed
camera against it, so its reply still replays. Field of view is in en, ar, de, he, ja, ko, vi and zh;
the ui.prefs settings list in docs/control-protocol.md and the guide's tour describe it.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #8865
* parameters.add: a parameter with no unit takes the design's units (#8865)
A new parameter given no unit took "mm" for any length or bare number, so
in an inch design `parameters.add {name: v, expression: "40/2"}` made a
20 mm parameter where the Parameters dialog makes 20 in. The unit a new
parameter infers is now a length in the design's units (mm designs are
unchanged); parameters.change on a new name and parameters.import share
the same inference. Angles and unit-less values keep their units.
A definition typed into a value of no fixed kind (a coupling's pitch,
a joint's value) still reads its bare number as millimetres, as that
field does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #8912
Co-authored-by: Cursor <cursoragent@cursor.com>
* View cube: rapid clicks add full turns from the in-flight target.
Match arrow-key retargeting so a second cube click turns from the glide destination instead of the mid-animation camera (#8912).
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Problem: a design saved before body names were pinned (#6403) is pinned by its first command
that changes it, as part of that command. Undoing that command went back to the unpinned design,
whose fingerprints no evaluated result matched any more, so the first undo after opening
evaluated the whole timeline again: 63-72 s in the SELENE rover (5,484 features) when opened
from the command line or headless (`Session::opened_at`), 0.08 s with this change. `doc.open`
pinned at the end of the command instead, which kept undo fast but left the design with unsaved
changes on opening.
Solution: `Session::opened_at` pins the names as it opens the design, with the names the file's
bodies have (no result changes, so nothing is evaluated), and takes the pinned design as the
saved one: opening is no change.
Claude-Session: https://claude.ai/code/session_013WUFq6QFBqmZjxwaKy2J1M
Co-authored-by: Markus Kaltenbrunner <kaltenbrunnermarkus@users.noreply.github.com>
Favorites, User and Model sections stay lined up when only some lists hold a long name (follow-up to #8551).
Co-authored-by: Cursor <cursoragent@cursor.com>
home.settings and ai.connected_toast follow the English change from Settings to Preferences (التفضيلات, as ui.preferences). Hover is no longer rendered as scrolling in the browser and navigation bar options, the Project options dialog label is fully in Arabic, and the trackpad zoom key help says to hold the key.
* claim: #8875
* Text fields: a right-click menu with Cut, Copy, Paste and Select All (#8875)
Every kit text field (dialog and sketch value fields, the sketch dimension
box, canvas value boxes, names, text areas, passwords, read-only text) now
calls one shared ui_kit::text_menu::menu after drawing its editor. Rows are
disabled when nothing is selected, the text is read-only or a password, or
the clipboard holds no text. The edit marks the response changed and gives
the focus back, so owners that commit on lost focus keep the field; a right
press inside the selection keeps it; Esc closes only the menu; expression
suggestions wait while it is open. New message ui.select_all in a pending
locale file; Cut, Copy and Paste reuse ui.cut, ui.copy and ui.paste.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #8860
* Timeline: delete after the marker counts and deletes the shown features (#8860)
With the bottom timeline filtered to one component, "Delete N Features After
History Marker" took the marker as a position among the shown features but
sliced the design's feature list with it. N counted features of other
components, and confirming deleted them too, including features before the
marker.
The entry now gets the feature's design position, the design's marker and
the shown positions, and deletes the shown features after the marker; the
label counts the same set. The history beside the browser shows every
feature and passes no filter, so it behaves as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Timeline: look up shown positions by binary search, test the entry before the marker (#8860)
The shown positions are ascending, so a binary search finds each one without
walking the list for every feature after the marker. The test now also checks
that a shown feature before the marker does not offer the entry.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A segmented control measured its options translated but painted them as
the caller passed them, so the many callers that pass English labels
(the Coil's Circular | Square and Inside | Center | Outside, Revolve's
One Side | Two Sides | Symmetric, Shell's direction, ...) showed English
in every language. Only where the row was too narrow and the control
fell back to its dropdown, which translates, did they read right.
Segments now paint, size, describe and tip their options translated;
the handles keep the option as passed.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #7435
* Viewport: the 3D mouse, the bridge and drag-zoom stop at the model, like the wheel (#7435)
The wheel and pinch hold the model point under the cursor, so the view
comes up to the surface and stops there. Every other zoom path passed no
anchor, so the 3D mouse cap, the navigation bridge and drag-zoom zoomed
straight through the surface into the body.
A device has no pointer, so the cap and the bridge now hold the model
point in the middle of the view; drag-zoom holds the point under the
pointer, like the wheel. With "Zoom to the cursor" off the old
zoom-through-the-model behaviour stays, as it does for the wheel.
The orbit half of the issue is a design decision and is untouched.
* Viewport: drag-zoom holds the point where the drag started (#7435)
The zoom anchor followed the moving pointer, so the zoom slid away from
what was pressed on as the drag went on. It now holds the press point,
the way the wheel holds the point under the cursor.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* 3D mouse: turn around the model in the middle of the view (#7435)
The cap and the navigation bridge turned the view around the view's
centre, which after a pan or from far away sits behind the face, so the
part swung away while it turned. They now turn around the model point in
the middle of the view (the middle of the bodies in view over empty
space), and keep it still on screen. Preferences > Navigation > "Turn
around the model in the middle of the view" switches back to the old
behaviour.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* 3D mouse: keep the turning point until the cap is back at rest (#7435)
The point the cap turns around was picked again every frame, so it slid
to whatever came into the middle as the view moved. It's now chosen when
the cap leaves rest and kept until the cap comes back, the way a mouse
orbit keeps the point it started on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* 3D mouse: show the point the view turns around (#7435)
While the cap turns the view, a faint ball in the accent colour marks the
model point it turns around, so it's clear what the turn holds still. The
point is now picked when the cap starts turning, not when it only pans or
zooms, so a pan shows no ball.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* 3D mouse: orbit is a choice of Traced or Classic (#7435)
The checkbox becomes a two-way choice on the 3D mouse section's form:
Traced turns around the model in the middle of the view when the turn
begins and marks it with a faint ball; Classic turns around the view's
centre as before. Traced stays the default.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Docs: the 3D mouse's Traced and Classic orbit (#7435)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Make model zoom and traced orbit opt-in (#7435)
---------
Co-authored-by: nomadsgalaxy <16734355+nomadsgalaxy@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #8899
Co-authored-by: ReDead <ReDead@users.noreply.github.com>
* Sketch: keep Diameter box when placing a second concentric circle (#8899)
A centre-point circle's press starts drag-to-draw; releasing on the same
centre (or close to the press) must keep the stage-1 Diameter field instead
of resetting it and risking a second on-click that finishes the shape.
Co-authored-by: ReDead <ReDead@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: ReDead <ReDead@users.noreply.github.com>
* claim: #8352
* Sketch: equation-driven curve, a fit point spline from x(t) and y(t) formulas (#8352)
sketch.equation_curve draws a curve from formulas in the expression
language and the design's parameters: x(t) and y(t) over a range of t, or
y = f(x) over a range of x. The curve is a cubic fit spline through fixed
points sampled from the formulas, as many as keep it within a tolerance
(0.01 mm by default, at most 500 points); a curve that ends where it starts
closes smoothly. The sketch keeps the formulas (one new design-format
file), places the points again before every solve so they follow the
parameters, and sketch.equation_curve.edit changes them, keeping the joined
end points. Both commands have input and output schemas.
* Sketch: Equation Curve window, its Edit menu item, labels and guide (#8352)
The ribbon's Equation Curve opens a window for the form (parametric or
explicit), the formulas, the range and the tolerance; OK runs
sketch.equation_curve. Right-clicking an equation curve offers Edit
Equation Curve, which reopens the window on its formulas and runs
sketch.equation_curve.edit. New labels and help in a pending locale file
(with ar, de, he, ja, ko, vi and zh), the icon, the glossary term and the
sketching guide.
* Equation curve: one if for closing the sampled points (clippy collapsible_if)
A held solve applying a constraint, or the edit's own solve, could find a far
solution of the constraints and move points millions of millimetres while
the sketch said solved. Such a result now counts as a failed one: the next
held solve is tried, or the edit is refused.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #7828
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Sketch: hold horizontal/vertical and line up with other points at once while drawing (#7828)
While drawing, the pointer lines up horizontally with the nearest sketch
point level with it and vertically with the nearest one plumb with it, at
the same time, as a dragged point already does; holding H/V with the
previous point still lines up across with another point. Dotted guides run
from the points lined up with (H, V or H+V), and the click adds a
Horizontal or Vertical constraint with each, in the shape's undo step,
unless the sketch already has it.
A click takes the tool out of the app while it runs, so refine_snap
treated every tool as the Line tool then: a rectangle's click could take a
line-extension inference its hover never showed. The tool's points and
kind are kept from the last hover, as the previous point was.
Preferences > Sketch > "Line up with other sketch points while drawing"
(sketch_align_points) turns it off for the old behaviour.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Sketch: lining up with other points is off unless turned on
Review on #8474: sketch_align_points defaults to off, so nothing changes for anyone who
doesn't turn it on. With it off, a click also treats the tool as before (the Line tool's
inferences), instead of keeping the tool's kind from the hover.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #8570
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Insert Design: pick one of the other open designs, or browse for a file (#8570)
With other designs open in tabs, Insert Design first lists them under the
button: one click inserts a saved one as a linked component; one with
unsaved changes inserts its saved file and the status line says so; one
never saved is greyed out, as there is no file to link. Browse... opens
the file picker, as the button did before, and it still goes straight
there with no other design open.
Preferences > Design > Linked designs > "Insert Design lists the designs
open in other tabs first" (insert_lists_open_designs) turns the list off.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Insert Design list: sit with the other menus' items, so run_item keeps its doc comment
Review on #8571: insert_design_items had landed between run_item and its comment. The
rebased menu-walk list keeps main's Target::HistoryControls beside Target::InsertDesign.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #8100
* History: say why an edited feature behind the marker changes nothing (#8100)
Editing a feature that sits after the history marker (double-clicking its
greyed-out item) rolls back to it, previews the change, and on OK puts the
marker back where it was: the feature stays rolled back, so nothing in the
view changes and nothing says why. That is the silent "pattern of a cut
does nothing" in #8100 (the reporter's design has its pattern behind the
marker; on main the pattern itself builds fine once rolled forward).
- timeline.redefine replies `rolled_back` and, when true, a `note`; it also
carries the feature's `warning` (for a rolled-back feature, the one its
constructor met building it at its place).
- The window shows that note in the status line after such an edit.
- A rolled-back item's tooltip in both History layouts says it is after
the history marker and not built.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Pending locale: Arabic, Hebrew, Japanese, Vietnamese and Chinese for the rolled-back notes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
* claim: #8639
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Sketch profiles: close the joints near-coincident curves leave open (#8639)
Two free-form curves that lie on each other along a stretch (a DXF that
carries an outline twice, with the same straight sides and different
corners) are cut into sampled pieces whose ends the graph joins into one
node within 1e-4 mm, but the pieces keep their own ends, a few millionths
apart. Only analytic edges were closed at those joints, so the sliver
loops between the two curves stayed open by up to 1e-5 mm, more than the
1e-6 a solid needs. The region around them took those loops as holes:
extruding it failed with "profile loop is not closed" and it drew no
fill when picked, so it looked unselectable.
Close a line's joint in each finished loop: line to line at their middle,
a line onto any other segment's end. On the reporter's DXF the band
between the outer two rings now extrudes (110.5 mm2 by 2 mm = 220.9 mm3),
and the zero-area slivers no longer form profiles (221 -> 216).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #8863
* Sketch: the status line after a dimension is in the interface language (#8863)
The Dimension tool built its "Dimension applied" status sentences (new
parameter, renamed parameter, plain value) and the driven-dimension note
with English format! strings, so they stayed English under a translated UI.
They now come from the catalog (one pending locale file: en, ar, de, he,
ja, ko, pl, vi, zh), with the typed parameter name passed in as is.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #8861
* History: Delete on a selected feature deletes the selection in both timelines (#8861)
With several features selected, Delete in a selected feature's menu deleted
the whole selection in the bottom timeline and only that feature in the
history beside the browser, which offered the selection as a separate
Delete Selected Features… entry.
Both menus now use one Delete entry: the selected features when the
right-clicked one is among them, labelled "Delete N Selected Features",
else that feature alone. The history beside the browser no longer adds its
own Delete Selected Features… to a selected feature's menu, since the entry
above covers it; folder menus keep it. The single-feature delete helper had
no callers left and is removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* History: a feature's menu leaves out Delete Selected Features… by a flag (#8861)
A right-click on a feature outside the selection selects that feature
first, so in a feature's menu the clicked feature is always selected. A
flag that says the menu is a feature's says the same without the lookup.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #8006
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Parts library: button head cap screws, ISO 7380-1 (#8006)
Insert Part offered the socket head cap screw but no button head, so a
design using button heads had to stand in a cap screw and accept the
wrong head. A button_head_cap_screw family now sits beside it, taking
the same parameters from the ISO 7380-1 table over M3 to M16.
The head is a new Head::Dome: the shank and the head revolve as one
outline, with the rim at dk, a short cylindrical flank, the arc of the
crown, and a flat top around the socket, so the hex socket stays a plain
cut on a plane. The standard draws the flank and the flat but dimensions
neither; they are 0.15 k and 0.7 s, and the comment on those constants
records what that costs against the standard's crown radius r1 and why
neither other fractions nor r1 taken as given does better. dk, k, s and
t are exact, each checked against three reproductions of the table.
Insert Part, Change Part Size and the bill of materials need no change:
they read the families and their table rows from parts.library.
The wider fastener request is issue 7692; this is only the one family
from it, and leaves the rest of that list open.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Parts: format after the rebase
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Pending locale: the Button head cap screw family name
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
* Components: copy and paste bodies and components between designs and components (#3390)
Squashed for the rebase onto main.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #3511
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Paste New: copy from a design other than the one pasted into
`component_clone::paste` read the design it copies from through the session it pastes into.
`build` now takes the source (document, evaluated model, unapplied moves) apart from the
target session; Paste New within one design passes the session as both, as before. Names are
numbered against the target's names.
From another design (`Source::copied`, a `ForeignCopy` snapshot taken at copy time):
- names are kept where the target doesn't have them, else numbered;
- the user parameters the copy uses come along, `w_2` where the target has a `w`, with the
copied expressions following;
- a copy whose features use what isn't in it (bodies, sketches, faces, parameters outside it)
is refused with a list of what; `FeatureKind::input_bodies` lists the bodies a feature uses;
- configurations stay with their design, and the target's timeline marker must be at the end;
- a body moved in from outside comes as an imported base named `BodyImport1`.
Body transfers are pointed at the copy's components before they are added: `add_feature`
checks that the components they name exist, which another design's don't.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Engine: paste bodies and components copied in another design
`component.paste_from_design` pastes what `Session::foreign_copy` holds, at the same world
coordinates as in its design, into the active component or `component` (placed by
`component_path`; a component placed more than once gets it in every instance), as one step
to undo:
- bodies as imported bodies, without their history, in one Import feature (named as any
import is, "Import1"); a body seen in another placement ("Cube (B:2)") pastes as "Cube";
names the target has are numbered;
- components as independent copies (Paste New from the snapshot): with their history,
appearances, materials, joints and motion inside them; joints to the outside stay behind.
Tests: bodies (position, naming, target component, undo; a body of another placement) and
components (history, appearances, a part placed twice, editing independently, save and reopen,
undo; user parameters renamed; refused when using a body outside it; bodies moved in).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Clipboard: copy in one design, paste in another
Copying or cutting bodies or components also keeps a snapshot of them (`ForeignCopy`); another
tab pastes it with `component.paste_from_design`: bodies as imported bodies, components as
independent copies (Paste and Paste New alike), folded and selected. Cut pastes as a copy
there, the original staying; Esc on the cut leaves nothing to paste anywhere.
The last copy in any design is what pastes: each copy or cut is numbered, and a tab whose own
copy is older empties its clipboard when it is switched to (`drop_stale`). A failed copy
anywhere leaves nothing to paste in any tab.
Tests: bodies and components pasted in another tab, the last copy winning across tabs, cut
as a copy. `copied_bodies_stay_with_their_design` gave way to the cross-design test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Clipboard: what is selected wins over a design row picked earlier
Copy copied the whole design as a linked component whenever the design's
browser row was still picked, even after a component, body or feature had
been selected in the view or another tab, and failed in an unsaved design.
The design row now gives way to anything selected, and switching tabs
drops the picked components of the design left.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Put the Paste from Another Design label in the pending locale file
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Tests: unground the placement before moving it (a design's first part is grounded)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drop locale keys main already folded into en.json
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Pending locale: Paste from Another Design in the complete catalogs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
* claim: #7409
* Keyboard Shortcuts: the read-only list says so and leads to the editable page (#7409)
Home › Keyboard shortcuts and Help › Keyboard Shortcuts open the
Shortcuts page read-only, in the Preferences window with its sidebar and
Cancel/Apply/OK, so it looked exactly like Settings › Shortcuts with
Import…, Reset All and recording missing. The read-only page now says
"Read-only list: shortcuts can't be changed here." and has an "Edit
shortcuts" button that turns the same page into the editable one.
Strings in assets/locales/pending/shortcuts-read-only-note.json (en, ja,
zh). Harness test opens it both ways, checks the note, clicks Edit
shortcuts, and finds Import…, Reset All and a recordable binding.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Pending locale: Arabic, Hebrew and Vietnamese for the read-only shortcuts note
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
* claim: #8864
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Viewport: with Orbit, Pan or Zoom lit, a press held still keeps the highlight under it (#8864)
A mode lit in the navigation bar, or a held Space, owns drags, not a still
press (#8438), but the hover pick counted any left press under such a mode
as navigation and dropped the highlight, so the line under a Select Other
hold went dark until the list opened. Only a preset's left-button binding
or Ctrl+left orbit navigates from the press on now; a lit mode's drag
still drops the highlight once it starts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Sketch Intersect: add Faces/Bodies and Projection Link options (#1110)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Sketch Project: add Geometry/Bodies and Projection Link options (#1110)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Sketch Project/Intersect: keep exact lines and arcs of whole bodies, no noisy rebuild warning (#1110)
Whole-body silhouettes and sections fit lines and arcs tightly and snap arcs
tangent to their neighbours. A rebuilt projection warns only when it removed
the user's own constraints or curves. Pending locales cover ar, he and vi, and
the preselection tests use the direct-pick workflow.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Select Other: label curves from Intersect as intersected (#1110)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* claim: #8692
* Browser: remember the folded card across runs, optional reveal on hover (#8692)
The browser card's fold (the chevron in its header) is now a preference,
saved with the other layout state, so the next run starts the way the user
left it. Preferences > Interface > Layout adds Shown / Reveal on hover: the
browser then stays out of sight until the pointer comes over its place or
the thin tab at its left edge, fading in like the navigation bar's Hide
until hovered, and stays while the pointer is inside or a menu, drag or text
edit started from it is open. It never folds in that mode.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Browser: move the reveal-on-hover messages to a pending locale file (#8692)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #8306
* Timeline: joints in feature folders (#8306)
timeline.group and timeline.move_to_group take joints as well as features, so
hundreds of joints fold into one folder tile. A closed folder shows one tile
whatever it holds; opening it shows its features and joints in both the bottom
and left History. Suppress, delete and ungroup on a folder reach its joints too
(one undo step), deleting a joint removes it from its folder, and an unknown
joint id or name is an error. Saved with the design; designs without joint
folders read and write as before.
* claim: #8862
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Expressions: Enter after `pi mm` keeps the unit, not a longer name (#8862)
A unit after a name or constant multiplies it, as after a number (`pi mm`
is pi millimetres), so it is a complete word there: the longer names are
listed unarmed and Enter submits what was typed. A unit at the start, or
after an operator or bracket, stays a prefix as #3664 chose.
Whether Enter completes is now worked out again every frame, unless a key
armed the list. It was kept from when the list first appeared for a word
and caret position, so it went stale when the text before the word was
replaced, or when the box took the focus before the names were gathered.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* claim: #7023
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Status bar: switch the mouse mapping from a menu (#7023)
The status bar gets a mouse-mapping button beside the units. It shows the
current mapping, and a click opens a menu of the mappings above the bar.
Picking one sets what Preferences > Navigation sets for it (bindings, wheel
direction, middle double-click, right drag orbits) through
choose_nav_preset. Nothing changes while the status bar is off (the
default).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Phanoob <phanoob@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #4176
* Preferences: mouse-wheel zoom speed (#4176)
A 0.25x to 4x multiplier for each wheel zoom step, applied in
navigation::handle so every canvas uses it. The 3D mouse keeps its own
speed. Values outside the range are refused when set and reset to 1x when
loaded.
* Control protocol: wheel_zoom_speed in the sorted ui.prefs list (#4176)
* Wheel zoom speed: its label in the complete catalogs (#4176)
* Arabic: fix the Resize Part hint (tanween style, part list, command name)
* Arabic: navigation bar Edge reads as position, tanween spelling in the geometry budget help
* claim: #8040
* wip: 3D line/spline/point tools (#8040)
* Take sketch.move3d off the toolbar help backlog now that it is agent-only (#8040)
---------
Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
* claim: #8794
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FAWPuFoh9qxn2uS2j53QZ
* Control token: a closing window removes only its own token and discovery files (#8794)
Under Flatpak each instance has its own loopback, so a second window
binds the same port number as the first and writes the same
<port>.token, <port>.pid and current.port in the shared data folder.
At exit the first window deleted them all, unconditionally, leaving the
second window running with no token file a client could read.
remove_token_file now takes this launch's token and removes the token
and process files only while the file still holds it; the process id
can't tell instances apart, as every Flatpak instance has the same one.
remove_current_port also takes the token and keeps the discovery file
while another launch's token file holds that port, and removes
current.pid with current.port (it was left behind).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FAWPuFoh9qxn2uS2j53QZ
---------
Co-authored-by: Claude <noreply@anthropic.com>
* claim: #8835
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Sketch: a selected text moves when dragged from anywhere inside its frame (#8835)
A drag had to start on one of the text's letters to move it. From the
space between letters, or inside a letter's outline, it found no geometry
and drew a selection box instead.
Off the sketch's own geometry, a drag from inside the selected text's
frame now grabs the text, as one from a letter does. Handles, dimensions
and geometry under the pointer still come first, and outside the frame a
drag from empty space still draws a selection box.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Trackpad: zoom with two-finger scroll while holding a key, chosen in Preferences (#8322)
Holding the zoom key turns a vertical two-finger scroll (or a wheel) into zoom at
the pointer, with the wheel zoom's speed, direction and anchor, so a laptop user
zooms without pinching. Preferences > Navigation > "Zoom with two-finger scroll
while holding": Cmd on macOS and Ctrl elsewhere (default), Option/Alt, Ctrl, or
Off. That key with the scroll did nothing before, so no default changes; Off
keeps it that way. Shift keeps its orbit.
* Trackpad zoom modifier: off by default (#8322)
Scroll keeps its usual meaning until a key is chosen in Navigation.
* Trackpad zoom modifier tests: Off is the default; tests that zoom choose Cmd (#8322)
* Trackpad zoom modifier: zoom once alongside the Ctrl+wheel pinch from #8539 (#8322)
A wheel the chosen key claims is no longer also read as a pinch, and a key that is not
chosen keeps main's pinch handling unchanged.
Align gets an Object Type row (Bodies | Components). In Components mode it runs the new
occurrence.align command, which moves only the chosen occurrence placements (their
transforms) instead of adding an Align feature to the shared component, so other instances
stay where they are.
- occurrence.align: from/to points, snaps, planar faces and planes in world coordinates,
flip, capture/pending, grounded and rigid-group refusals as occurrence.move.
- A viewport or browser-body pick takes the placement directly under the active component.
- A placement inside a subassembly placed more than once still moves (as occurrence.move
does) but the dialog warns, and the reply lists it under shared.
corner_free looked only in a fixed column at the view's right edge and took
the largest free rect there by area. Next to a tall dialog docked at the
right edge, or the Assistant in the Floating layout, the narrow strip beside
the pane won: the toasts were squeezed into it, their text broke inside
words, and with a wider pane they were not shown at all.
It now tries that column first and then columns ending at each obstacle's
left edge, nearest the corner first, and takes the first free rect that is
TOAST_WIDTH wide and tall enough for a toast, as the frame-rate readout
moves beside a right pane (#6755); failing that, a rect tall enough beats a
wider one too short to show any toast. Without a pane the stack stays
exactly where it was.
* claim: #7688
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Graphics: a background colour of your choice for the 3D view (#7688)
Preferences › Interface › Theme gets a Background row: the theme's
gradient (the default, unchanged) or one colour for the whole application,
filling the view in either theme. The window keeps its own colours, a
picture of the view (`ui.render`) takes the colour too, and `ui.prefs`
sets it as `viewport_background` ([r, g, b] or null).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Phanoob <phanoob@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat: add joint-aware assembly Move handles and live motion readouts
* test: improve render job failure diagnostics
* fix: preserve assembly Move compatibility and viewport theme colors
* ci: skip default label sync for fork PRs
* fix: keep Windows CI checks portable after main rebase
* test: cover RTL assembly strings and stable refraction job
* i18n: cover assembly Move in Vietnamese
* i18n: cover assembly Move in French
* fix: complete CGA palette for assembly motion handles
* fix: complete Assembly Move palettes and Turkish strings
* fix: retain assembly move cache answers and toolbar help
* style: format merged assembly Move navigation guard
* fix: align Assembly Move preferences and tests with main
* Fix offline update fixture synchronization and clipped prism intersections
Wait for all v5 manifest requests before deploying v6. Intersect hole-free prism sections over their common height, retaining the established B-rep path for newly clipped perforated sections. Preserve the failing property seed and add closed-form and cap-boundary regressions.
* claim: #8592
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* History: error and warning marks in the browser and on folded folders (#8592)
In the History "In browser" layout, a feature whose build failed gets the
error icon at the end of its row, one that built with a warning the
warning icon, with the message in the tooltip: the marks and text of the
History on the left. A folded History or Features folder carries the
worst mark of what it holds, and so does a folded folder in the History
on the left. Suppressed and rolled-back features are not built and get
none.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- active_body: take every body's owner in one pass (model_body_components) instead of searching the timeline body by body
- Timeline strip: find each chip's result in a table built once per frame (result_index) instead of a linear search per chip
- 4,880-body design: UI frame 31-32 ms -> 7 ms of CPU; tests pin the active body and the result lookup
Co-authored-by: P'tite Tête <asso@ptitetete.com>
* claim: #8095
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Properties: moment of inertia, principal axes and first moments of a part or assembly (#8095)
component.properties and occurrence.properties report each closed body's
inertia tensor about its centre of mass, and in total the tensor about the
total centre of mass (world axes, g·mm²), its principal moments and axes,
the first moments, and with `about: [x, y, z]` the tensor about any point.
The tensor comes from each body's welded export mesh, tessellated once per
body, scaled to the measured mass and turned by each placement; bodies add
by parallel axes. The maths is a new crates/geom inertia module.
The Properties window shows the first moments, the tensor, the principal
moments and axes, and the view marks the centre of mass with the principal
axes while it is open. Inspect → Measure → Properties now opens that window
(it ran the command and showed nothing).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Properties: Copy matrix, and the whole design's card titled just Properties (#8095)
Copy matrix, under the tensor, puts it on the clipboard as three lines of
tab-separated values at full precision, in the units shown. The card for
the whole design is titled Properties; one opened on a component,
placement or bodies keeps its name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Properties: measure beside the window, so a large assembly doesn't freeze it (#8095)
Properties ran on the window thread: 18 s on a 144-body design, with the
window answering nothing meanwhile (#5203). In the desktop app the card now
measures on a copy of the session on a worker, the way #4614 made agents'
measures work, says "Measuring…" until the reply is in, and Close drops it.
A quick measure still shows at once; headless, tests and the web measure in
place.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The wasm compile ran only by hand, so #8227 (a native-only DroppedFile::bytes() call in
crates/ui-egui) passed PR CI and the merge queue and broke the web build on main (#8534).
A merge group that touches the crates below the apps, the web app, the vendored crates, the
manifests, the toolchain or this workflow now runs `cargo xtask wasm`, and `ci` requires it.
Dependency advisories ran only nightly. A pull request or merge group that changes Cargo.lock
or a Cargo.toml now runs `cargo deny --locked check --warn unmaintained advisories`, and `ci`
requires it then. The nightly keeps the full advisories, bans and sources scan.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #4127
* Render preview: geometry budget from the device, readable refusals (#4127)
The Render preview refused every scene over a fixed 128 MiB of geometry. On
desktop the budget now follows the device's buffer limit (eight of its
largest buffers, at most 2 GiB; 2 GiB with the usual 256 MiB buffers). The
web keeps 128 MiB.
The product upload runs under its own GPU error scopes: an allocation the
device refuses is reported in the preview instead of reaching the device's
uncaptured-error handler, and the half-uploaded batches are dropped. Buffer
uploads map the buffer themselves so a refused buffer fails the upload
rather than panicking in create_buffer_init.
Refusals name the geometry size and the limit and say how to get under it,
on a theme panel card that stays readable on any preview background.
* Render preview: keep the previous geometry budget behind a preference (#4127)
* Render preview: adapt the device budget to main's GPU upload errors (#4127)
* claim: #5717
* ui(feat): let users choose visible toolbar tabs
Keep Sheet Metal and Weldments visible by default while adding persisted tab visibility controls and restoring specialty tabs for designs that use them.
Signed-off-by: Karl Von Richter<karl@embeint.com>
* ui(fix): respect hidden toolbar tabs in every design
Remove feature-based visibility overrides for Sheet Metal and Weldments. Keep all tabs visible by default and preserve the user choice across feature creation, opening designs, selection and undo.
Signed-off-by: Karl Von Richter<karl@embeint.com>
---------
Signed-off-by: Karl Von Richter<karl@embeint.com>
The nightly failed every day from 2026-10-16 to 2026-10-28 without anyone noticing. A new
nightly-alert job (scheduled runs only, the only job with issues: write) opens or comments on
one issue, "Nightly CI is failing" (label ci), when a nightly job fails, and closes it when a
nightly passes.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #7724
* Update now installs the newest release, checked when pressed (#7724)
* Update: close the unmanaged-install test after the rebase (#7724)
* Update: the refresh messages in Arabic, Hebrew and Vietnamese too (#7724)
* claim: #8533
Co-authored-by: Cursor <cursoragent@cursor.com>
* Parameters: show full names in the table and formula popup.
Hover tips carry the full identifier when Name cells or completion rows clip, and the completion list widens for long suggestions (#8533).
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
A new arch job repackages each release tarball as
solidcraft-<version>-linux-<arch>.pkg.tar.zst with makepkg, through the Craft apps' shared
action (storytold/craft-common-actions/actions/arch-package, pinned by commit). The package
is io.tangencylabs.solidcraft, named after the tarball's .desktop file, provides solidcraft
and conflicts with solidcraft-bin/-git. Each package also gets a version-less copy, and
docs/downloads.md links them.
* claim: #8183
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Navigation bar: can sit at the top of the view (#8183)
Preferences > Navigation bar gets an Edge choice, Bottom (default) or Top.
On top the bar sits under the ribbon, its menus open downwards and its
tooltips show below it; the OK/Cancel pair and the axis view move down
below it when they would overlap.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Navigation bar: the edge's labels in the complete catalogs (#8183)
ar, he, ja, vi and zh carry every English message; the Edge row's three
labels were missing from them, so `xtask i18n --check` and
`every_complete_catalog_carries_every_english_message` failed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Phanoob <phanoob@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
body_appearance_key hashed only the name of a body's assigned material, so
after material.edit changed that material's colour (or an undo or redo
changed it back) the key stayed the same and the viewport kept the cached
old look. The key now also hashes the material's resolved look; with an
unchanged material the cache is reused as before.
* Keyboard shortcuts: a command can have several keys
Click a key on the Keyboard Shortcuts page to replace it, or the + after it
to add another; every key runs the command and menus show the first. One
key is still saved as a string, several as a list. Taking a key over from
another command leaves that command its other keys. ui.shortcut takes a
slot and reports every key.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Keyboard shortcuts: follow main's search layout; fix prefs test for key lists
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Chamfer at a corner of two lines lying on each other cut both back and
added a line of no length between their ends; lines in line with each
other have no corner either. chamfer now refuses a corner angle of 0 or
180 degrees with Fillet's message, "the lines are parallel" (found by a
local trim fuzzer).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Resize Part: say what it works on instead of a parameter error; Parts opens the library (#8735)
Resize Part (parts.resize) started with no standard part selected, as the
ribbon starts it, ran the command with no parameters and showed
"`component` must be an id or a name". It now says to select an inserted
standard part, and that Scale resizes a body. Parts (parts.library) only
listed the library, so its button did nothing: it opens the Insert Part
dialog. The German and Dutch button labels said "scale" ("Teil skalieren",
"Onderdeel schalen"), like the Scale command: they now say change size.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Resize Part: the German label matches the command, and the test checks that nothing ran (#8735)
The German ribbon label reads "Bauteilgröße ändern", the name the command,
its dialog and its messages already use. The hint for a missing standard
part is now one function shared by the ribbon start and the Browser's
Change Part Size. The regression test also checks that the design and its
history stay as they were and no message carries the command's own error,
that a scripted parts.library still only lists the library, and that with a
standard part picked Change Part Size opens on it.
---------
Co-authored-by: semmelrinde <124386289+semmelrinde@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A placement row's menu starts from its component's menu, which takes
Turn Up Axis's enabled state from the component's first placement.
Move/Copy was already set again for the placement right-clicked; Turn Up
Axis and its two turns now are too, so a free second placement can turn
while the first is grounded, and a grounded one can't.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A trimmed circle ended on a line exactly where an arc started on that
line, held there by a point-on-curve tie. Trimming the line away dropped
the tie and left the two ends on one spot, untied. remove_curves now ties
each point held on a removed curve at one of its ends to that end before
the curve goes (found by a local trim fuzzer, 15 -> 12 cases in 2000
seeds).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #8480
* Pinch to zoom on touchpad: native Ctrl+wheel pinch zooms the view (#8480)
The viewport's pinch path read only `egui::Event::Zoom`; `immediate_wheel` leaves
wheel events carrying the zoom modifier to egui. Native Linux and Windows
touchpads report a pinch as Ctrl+wheel in points, so the gesture reached neither
path and the view did not move. `pinch_factor` now reads those wheels too, in
every wheel unit and scaled as egui scales them, and `ui.scroll` can send the
same gesture (`ctrl: true`) so scripts and scenarios can replay a pinch.
* fix: finish Ctrl+wheel pinch handling and replay validation (#8480)
* claim: #8561
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Dialogs: disable Apply until something changed (#8561)
The dialog footer gets a disabled state for Apply (Footer::nothing_to_apply,
Window::apply_enabled): drawn with the theme's dim ink, not clickable, not
reached by Tab and reported disabled to accessibility. Preferences enables
Apply only while something differs from its snapshot (what Cancel would put
back), so it is disabled when the dialog opens and again after Apply.
ui.prefs replies with apply_enabled.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #8746
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Move/Copy: changing Move Type keeps the move made so far, and picks follow the moved body (#8746)
Switching the Move Type reset the values, so the move made under the
earlier type was lost. The dialog now keeps it (`chain`) and the new type
moves the bodies on from there; params() composes the two into one
solid.move (one turn about the origin and a translation, as before).
While the new type's picks are missing, the kept move alone is made.
The new type measures from where the bodies now stand (Free Move's pivot,
Rotate's ring, round-edge centres and axes on a moved body). The picker,
the edge/face highlights, the centre marker and Rotate's axis line follow
the moving body, mid-drag too; a pick on it is taken back by the current
type's own move so it doesn't move with the move it sets. While the
triad is dragged the moved bodies' highlights are hidden and rebuilt once
on release.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #8382
* Browser: Show/Hide context menu on origin axis rows (#8382)
Right-clicking an origin axis row (X, Y, Z) now opens its own menu with
Show/Hide (V), through ui.originPart and browser.visibility like the
origin planes' entry. An axis takes no sketch, so Create Sketch is not
offered.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The Display menu's "Perspective while orbiting, ortho on faces" ran past
the menu's edge: a menu row painted its label at full length. It is now
cut before the row's detail and check, and shown whole on hover, as a
long name in a list is.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ui): keep selected sketch points highlighted
Add persistent point selection rings while retaining constraint-state markers, with regressions for pointer selection, dialog inputs, aliases and visibility. Fixes#1069.
* Move the sketch point selection tests into the viewport overlay tests (#1069)
Run them through a frame that clears its texture uploads, as the current egui
requires, next to the other overlay tests.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Victor Santos Oliveira <56417388+Vittuu@users.noreply.github.com>
Co-authored-by: Colin Faulkingham <colinfaulkingham@Colins-Mac-mini.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
* Render lights: uniform light list, solo and mute, and the Light editor
* Render lights: Lighting and Light editors to the approved design
* Render lights: the scale-test frame carries the product light list
* Restore the stable Render light mute target on the shared eye control
* fix: validate the viewport BRDF probe with current shader dependencies
* fix: compile legacy lighting controls only for tests
* fix: scope the legacy lighting preset import to tests
* fix: reuse the merged Add Light label and translations
* test: enable the implemented Add Light radial action
* fix: restrict the legacy CPU vector editor to tests
* test: reveal Output before checking Lights browser focus
* test: use the published Browser card rectangle for Output scrolling
* test: reveal Output children before exercising editor navigation
* claim: #8267
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Sketch Create menu: family flyouts open beside their row, on hover (#8267)
A family row (Line, Rectangle, Arc, ...) opened its variants as a dropdown
below the row, covering the rows under it. The flyout now opens to the
right of its row, level with it, and on the left only when the right has
no room. Hovering a row opens it, as the context menu's Appearance
flyout does; resting on another row closes it after the shared delay.
The context menu and the Create menu share the flyout timing
(SUBMENU_CLOSE_DELAY, flyout_hover, flyout_leaving) and placement
(flyout_at) in ui_kit::menu.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A vertex pick found its named edges and then picked each again by a point
on it, among the edges of every body. At a corner where another body's
edge runs past (a wall against a side wall), all three became that edge,
and the vertex its end 3 mm away. Take each named edge's ends from the
body that has it.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
menu_tests::linked_component_opens_its_part_in_a_tab_and_reports_a_missing_source
gave the background open of the linked part 600 frames. Headless frames
are quick, so on a loaded machine (the whole ui-egui suite at once) the
read can take longer, and the test then found the assembly still shown.
It now waits until the part is open, up to 120 s as Harness::settle and
the edit-in-place tests wait for a load.
Co-authored-by: Claude <noreply@anthropic.com>
* claim: #4160
* Sketch Fillet: 3D sketch lines are not offered, and say they can't be filleted (#4160)
The Fillet tool offered 3D sketch lines as picks, and sketch.fillet then
refused them with "`a` must be a line". The tool no longer offers them, and
the command refuses them with "not supported yet: filleting 3D sketch lines".
* Sketch Fillet and Chamfer: refuse all spatial sketch curves (#4160)
* Sketch corner test: give the picked curve its point (#4160)
main added a point to sketch-curve hits after this branch was written.
* Render shot editor: camera and frame controls, aspect presets, guides and the frame overlay
* Render shot: Shot editor and Scene overview to the approved design
* Distinguish Render stage variants from native window frames
* fix: restore library inspection after clearing shot selection
* test: follow the merged Output Check navigation
* test: inspect Check without requiring a preflight result
* test: cancel Shot removal through its shared dialog action
* fix: remove obsolete Inspector stage form after editor integration
* fix: preserve Appearance Advanced when clearing Wear focus
* test: expect the dedicated Check editor from Scene overview
Saving gave no sign that it worked, while an export names the file it wrote.
After Save or Save As, the status line now reads "Saved <path>".
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The dense solve updated each matrix element through bounds-checked get/add;
on slow scripted drags it was most of the time. The same operations in the
same order on row slices give bit-identical solutions, about twice as fast.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #4359
* Canvas and Decal say where to click after the image is chosen (#4359)
After picking an image the placement tool started silently, so it looked
as if nothing had happened. Show the tool's placement hint as a toast while
it waits for the click (translated), and end it once the image is placed.
* Canvas: include placement hint in viewport obstacles (#4359)
* claim: #8662
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Preferences: typing in a number field edits it instead of searching (#8662)
Type-to-search takes printable keys unless the focused field notes the key
sink. number_field (Autosave every, the Largest file to open limits) never
did, so its digits went into the search.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #4193
* Sketch Mirror: an origin axis can be the mirror line (#4193)
Mirror accepted only a straight curve of the sketch. It now also takes an
origin axis that lies in the sketch plane ({axis: X|Y|Z}), projected into
the sketch as a fixed linked line, so the copies stay symmetric to it and
follow the source when it moves.
* Sketch Mirror: use current sketch store (#4193)
In Preferences, letters typed in a drop-down list's own Search box (the
Font list, a 3D mouse's button assignments) went to the Preferences search
instead, which then filtered the page, usually to "No matching
preferences", and closed the list. The Preferences search takes typing only
when no text field held the keyboard on the previous frame, and the
drop-down's Search box never said it did. On the full Interface page the
number fields' focus check covered for it; once a search hid them, as when
the Font row is found by searching for "font", nothing did.
The Search box in searchable_dropdown_with now reports that it has the
keyboard, as the Preferences search box does.
Co-authored-by: Claude <noreply@anthropic.com>
* Render output: Effects and Check sections, Size presets panel and the Check tool chip
* Render output: Sampling, Effects and Check editors as child rows under Output
* fix: restrict legacy Output controls to test builds
* fix: cover status controls and reveal Effects test targets
* fix: include shared quality reviews in copied check reports
* fix: remove the obsolete Effects GPU guard after Wear integration
* Render appearance: click, drag and paint to assign, Pick Up, copy and paste, and Remove
* Exclude the workspace Shots strip from dialog width checks
* Exclude the collapsed Shots control from dialog width checks
* Retain assembly cache listings and measure actual dialog bounds
* fix: handle toast button rectangles in assignment status
* fix: restrict gallery note measurement helper to tests
* test: measure assignment help at the catalogue footer
* fix: share the command revision snapshot after restacking
* claim: #7445
* Color picker: add shared Hex input mode (#7445)
Accept strict RGB/RGBA Hex input while preserving exact opacity,
opaque/PBR restrictions, invalid drafts and existing U8/F behavior.
Keep explicit English fallback entries for the three new Hex messages
in Arabic and Hebrew, which final integration main now requires as
complete catalogs. No Arabic or Hebrew translations are claimed.
The user authorized this feature-only commit after full cargo xtask ci
stopped at the pre-existing geometry nonminimal_bool lint. The same
failure was reproduced without this patch at the published claim and
main. No unrelated lint fix or suppression is included.
* i18n: cover complete catalogs for Hex picker (#7445)
Add the three Hex message keys to Vietnamese in the feature's pending
locale file. Keep the unchanged English source values as explicit
fallbacks; no Vietnamese translations are claimed.
Audit the complete-catalog set (ar, he, ja, vi, zh) rather than only
the first catalog reported by CI. Preserve the existing completeness
test and all picker/Appearance code.
The completeness regression changes from red to green. Full local CI
and wasm checks remain blocked by failures independently reproduced
in clean upstream main; no unrelated fixes or suppressions are added.
* Appearance: use the shared opacity-aware color picker in Render (#7445)
Route the merged Render Appearance editor through the UI-kit picker,
passing exact opacity only for non-PBR looks. Keep a non-editable mixed
label and treat explicitly accepting the first color as a mixed edit.
Remove the external Hex field and duplicate parser, replace its gallery
entry with the shared mixed-aware picker, and update the control contract.
Preserve existing regression assertions and add actual Render UI tests
for alpha edits, precision, invalid drafts, PBR rejection and mixed edits.
Full local CI and wasm were run before this commit and remain blocked
by the separately reproduced unchanged upstream geometry lints and
prefs.bytes wasm error. No unrelated fixes or suppressions are included.
---------
Co-authored-by: mynoc <298906611+mynoc-96@users.noreply.github.com>
* claim: #8086
* Configuration table: a filter in the Add Column menu (#8086)
The + menu listed every parameter, feature suppression and body
material/appearance with no way to narrow it. A filter field now sits at
the top, takes focus when the menu opens, narrows the entries and the Face
Appearance submenu as typed, picks a lone match on Enter and clears when
the menu closes. A click into the field keeps the menu open (plus_header
closes on outside clicks; entries close it themselves), and the list keeps
its full height after the filter has emptied it once.
New kit element ui_kit::menu_filter (glyph, frameless editor, rule) so the
field does not read as an entry; gallery, i18n guard and docs/ui-kit.md
updated. Regression test in manage_dialogs_tests.
* claim: #6756
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Windows: never load a dxcompiler.dll found on PATH; FXC unless DXC ships beside the app (#6756)
wgpu's Dx12Compiler::Auto loads dxcompiler.dll by name, so Windows takes the
first one on PATH. SolidCraft ships none, and DXC 1.8.2505 from other programs'
folders (Obsidian, NVIDIA SDK Manager) crashed every start with
EXCEPTION_ILLEGAL_INSTRUCTION during the viewport shader compile.
solidcraft_gpu::dx12_compiler() now chooses: WGPU_DX12_COMPILER as wgpu reads
it when it names a compiler; otherwise DXC from the dxcompiler.dll beside the
program, by its full path, else FXC. The window, the headless/CLI instance and
the test helpers start with it, and dx12_compiles_with_fxc follows it.
WGPU_DX12_COMPILER=dxc keeps the old lookup.
Fixes#6756
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Sketch: points tied only through a removed curve's end stay tied
Removing a curve removed its orphaned end points with their constraints, so
two points made coincident with such an end lost their tie although they
still met: two rectangles' corners on one arc end came apart when the arc
was trimmed. They are now tied to each other before the point goes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Sketch: keep the tie test with the other trim tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Naming a face swept from a sketch flattened every curve of the sketch to
a polyline and measured its distance to each, for every face. On a sketch
with thousands of curves (a sheet with many cut-outs) that grew with
curves times faces. The curves are now flattened once per feature, each
with its box, and a curve whose box is farther than the best distance so
far is not measured.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Spanish (es-ES): catch up with en.json
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Navigation preferences: name the Shift key through the catalog
Spanish now translates the key name, so the page painted it in English.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #7356
* Orbit about the cursor: on an edge or sketch line, turn about the point under the cursor (#7356)
With Orbit around: point under the cursor, a drag that starts on a body
edge turned about the edge's middle, which on a long edge is far from
the cursor, and one that starts on a sketch curve turned about the
centre of the bodies in view. Edge and sketch curve hits now carry the
point under the cursor, which the pick already measured, and
pivot_under uses it. An edge hit's `mid` is unchanged, so selections
still record the edge's middle.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #8139
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FAWPuFoh9qxn2uS2j53QZ
* 3MF export: a component's bodies as one object with parts (#8139)
file.export and file.save_mesh wrote every body as its own 3MF object and
build item, so a slicer loaded a multi-body part as separate objects and
could arrange them apart on the bed. With objects: "component", each placed
component that holds two or more bodies is written as one object whose
<components> are its bodies' meshes, built once, so PrusaSlicer, OrcaSlicer,
Bambu Studio and Cura load it as one object with a part per body, each of
which can take its own filament. The design's own bodies, and a component
with a single body, stay objects of their own. The default (objects: "body")
writes the same file as before.
- io: model_xml_parts and write_3mf_parts write the parts; export_with_parts
maps body names to them. model_xml_with and export_with_quality give the
same output as before.
- engine: mesh_parts reads `objects` (component is refused for formats
other than 3MF); component_parts groups the placed bodies by placement,
each part named after its occurrence (Bracket:1, Bracket:2).
- file.save_mesh passes the parts on when it also sends the file to a slicer;
file.send_to_slicer's own folder export is unchanged.
Fixes#8139.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FAWPuFoh9qxn2uS2j53QZ
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Render wear: a Wear row and editor for the aging layers
* test: read Wear header text from accessibility label values
* test: restore body picks for Wear header recovery assertions
* Render environment editor: rotation, intensity, backdrop colour and ground through render.environment.set
* Render environment editor: rotation ring in the view with one undo step per drag
* Render environment: Source, orientation, backdrop, ground and atmosphere sections
* Keep segmented controls inside their allocated rows
* fix: compile legacy environment controls only for tests
Where several curves cut at one place, trim pinned the new end onto the
first of them. At the touch point of two circles that was a rectangle side
passing through, and once a later trim removed the side the arc ends were
loose: a trimmed dog bone kept two untied ends.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Sketch Coincident: refuse a line's two ends
Making a line's two ends coincident shrinks it to a point; a Parallel or
Perpendicular on it afterwards has no direction to hold and a later solve
could throw points far away while reporting solved, as with a line's own end
as its midpoint (#8562).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Sketch Coincident: keep the own-ends test with the other Coincident tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #3693
* Fix timeline playback to show each rebuilt feature
* Move History playback into the Inspect ribbon
* Refuse Play History up front when playback couldn't run
Starting Play and the per-frame check that stops it were looking at
different app state, so Play could be accepted during an apply, a delete
check or queued history work and then stop silently on the next frame.
Both now use one playback_blocked check.
The UI also reads the engine's PlaybackAction as the typed enum instead of
matching strings, so a bad reply is an error rather than a quiet toggle.
* Put Play History in the History controls
History now shows five controls in transport order: start, previous,
play, next, end. Play runs timeline.play, so it turns into Stop while
playback runs, same as the ribbon button and the shortcut.
It's on by default (#3688, #3740, #4216). Anyone who wants the old four
controls back can right-click them and pick Hide Play Button, or clear
history_play_button under Preferences > Interface. In a named vertical
History too narrow for five buttons beside its grip, Play is left to the
ribbon rather than overlapping the grip.
* Draw the History Play button as a plain play/pause glyph
The Play button in the History controls now uses the same drawn glyphs as
its neighbours: a filled triangle, and two bars while History plays. The
triangle is bigger than the step arrows so it doesn't read as Forward.
The ribbon command keeps its timeline_play and timeline_stop icons.
* Keep the playback changes clear of PRs queued ahead
Several PRs in the merge queue touch the same lines, so this moves ours
aside without changing behaviour:
- history_play_button and its reset-test row sit with the other History
preferences, not beside view_confirm_corner (#7248 adds status_bar there).
- History keeps main's history_transport_width() for four controls (#6430
and #7459 call it); the Play button's count uses history_controls_width.
- A new or opened document stops playback after dim_view::cancel(), clear
of the line #7819 adds after reset_chat.
- The Play/Stop label swap moves from interactive_command_info, which #7906
rewrites, into the ribbon's command_info.
* Stop playback after the command completes, clear of #6433#6433 rewrites the session.execute line in execute_command, and our playback check sat right under it, so the queue couldn't merge the two. Checking the revision after command_completed does the same job a couple of lines later.
---------
Co-authored-by: cfaulkingham <colin.faulkingham@gmail.com>
* claim: #7834
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Viewport: vertex markers where edges are drawn (#7834)
Each body's vertices (the ends of its pickable edges, once each) show as
small screen-space squares in the edge colour in the styles that draw
edges: shaded with edges, wireframe, and shaded with hidden edges, where
hidden vertices show faintly like hidden edges. Plain shaded draws none.
A marker is a lifted line from the vertex to itself in the body's block,
so it is instanced with the edges, cached with them, and depth-tested
like a highlight band. The existing vertex hover ring stays on top.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Viewport tests: count edge segments apart from vertex markers (#7834)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Vertex markers: an option, off by default (#7834)
Adds the Vertex markers preference (Preferences > Graphics, off by default) and draws the dots only when it is on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Open refused a saved model whose text records (a feature's warning, a body or
sketch name) held more than 65,536 bytes, although Save writes them, so such a
design was evaluated again on every open.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Viewport: cut dashed lines to the view, so a huge distance can't fill the memory
epaint makes one shape per dash and steps along a line in f32. The
viewport's dashed lines (the Extrude taper handle, Measure's legs, the
fillet radius guide, the inference and snap guides, a sketch text's
frame) were dashed over their whole projected length. With an Extrude
distance of 1e9 mm the taper line was billions of points long: the
frame never finished and filled the memory within a second.
`viewport::dashed_line` dashes a line that lies in view exactly as
before, and cuts one that reaches far off screen to the view (grown by
two dash periods) first, keeping its dashes in phase with its start.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Viewport: compute a cut dashed line's ends in f64
A line a trillion points long lost its cut ends in f32: they collapsed to one point, and the part in view drew nothing. The new test caught it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Two new commands, view.next_tab and view.previous_tab, with Ctrl+Tab and
Ctrl+Shift+Tab as their default keys: they appear on the Keyboard Shortcuts
page and can be rebound or cleared like any other. The window steps along the
tab strip and goes round at either end; one design alone stays as it is, and
a "save changes?" prompt keeps its tab shown. The engine only echoes the
step (no design edit, no undo step); engine.batch refuses them like the
other steps that go to another design.
Co-authored-by: Phanoob <phanoob@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #5066
* Pattern counts: name a length-unit parameter, refuse fractional counts (#5066)
A count parameter created with a length or angle unit now gets an error
that names it and says to set its Unit to none. A count that evaluates to
a fraction is refused instead of being rounded silently.
A mesh body's row (an imported STL, 3MF, OBJ, ...) now shows a new
mesh_body icon, a triangulated cube in the mesh accent, instead of the
solid body's grey cube. Hovering a body row says Mesh body, Surface body
or Solid body. Body::is_mesh() decides; nothing new is saved.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #7744
* Vendor winit 0.30.13 with Wayland touchpad pinch and rotation gestures
The 0.30 Wayland backend never binds zwp_pointer_gestures_v1, and
WindowEvent::PinchGesture / WindowEvent::RotationGesture only come from the
macOS and iOS backends, so a trackpad pinch produces no input at all on
Linux and Wayland (two-finger scroll still works, since X11/XWayland
reports it as a wheel). egui-winit already maps the events to
egui::Event::Zoom and navigation::handle already zooms the camera from
that, so the Wayland backend was the only missing piece.
Upstream the gesture support is part of winit 0.31 (rust-windowing/winit#4338),
which eframe 0.36 cannot use while it still requires winit 0.30, and the
0.30 backport rust-windowing/winit#4778 was closed unmerged. So ship the
same fix locally, upstream-style-applied and stripped of example and test
targets, with its unit tests for the gesture state machine kept under
cargo test -p winit --lib -- pointer_gesture:
- src/platform_impl/linux/wayland/seat/pointer/pointer_gesture.rs: bind
the global (versions 1..=3), one pinch gesture object per pointer,
destroyed before the pointer is released; begin/update/end become the
two events for the window the gesture began over. Two fingers only;
the protocol's scale is relative to the gesture's beginning, so the
event carries the change since the previous update; rotation is
negated to winit's counterclockwise degrees; a non-finite scale is
dropped, and so are updates of a gesture whose begin we never saw.
- seat state wiring: bind the global, track and drop the gesture object.
- wayland-protocols' unstable feature, platform notes on both events,
changelog, patch-section entry in Cargo.toml, vendor/README, and
Linux/Wayland notes in the troubleshooting guide and the UI tour.
Retire this vendoring once an eframe release consumes winit 0.31 or
later; the cleaned branch is kept as
https://github.com/purp0w/winit/tree/wayland-pinch-gestures.
* ci: retrigger after flaky occurrence-cache test
---------
Co-authored-by: Francisco das Chagas Danta Filho <dev@dantas-filho.me>
Parallel, collinear, equal or concentric of a curve with itself was added
as a constraint that always holds; perpendicular or tangent to itself was
refused as geometry that can't move. These six now refuse one curve
picked twice, as Coincident already does for a point, and leave the
sketch as it was. Constraints already in a design are untouched.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #7919
* Pick by the body's own size, not the camera's distance (#7919)
The occlusion test that decides whether an edge or vertex is hidden by the
face in front allowed a slack of `scene_radius(app) * 0.02`, and
`scene_radius` ends with `.max(app.cam.half_height() * 2.0)`: in an
orthographic view that is `0.016 x camera distance`. Zooming out grew the
slack without bound until a whole body fitted inside it and every edge behind
the front face counted as visible, and edges and vertices are offered before
faces, so they won the pick. Cutting the slack from the scene also gave a
small part in a large assembly a tolerance many times its own size.
`pick_slack` now takes a size and returns the mesh's own chord tolerance with
room to spare, `DisplayQuality::Coarse.chord(size) * 4.0`: a facet sits at
most a chord tolerance inside its surface, and that tolerance is already a
fraction of the body's size, so the slack no longer moves when the camera
does. Each body is tested with its own bounding box, which the loop already
holds. `view.pick` does the same, so an agent's pick still matches a click on
the same pixel. Sketch profiles and origin planes belong to no body and keep
a scene-level value, `scene_pick_slack`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
A line's own end held at its middle shrinks the line to a point. The
solve accepted that, and a later angle, tangent or parallel on the line
had no direction to hold: its points went millions of millimetres away
while the sketch still reported solved. MidPoint now refuses the pick,
saying why, and leaves the sketch as it was. Constraints already in a
design are untouched (only the command refuses).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #4750
Co-Authored-By: Claude <noreply@anthropic.com>
* Extend: pick two curves to extend both to where they meet (#4750)
`sketch.extend` gains a second form: `curves: [a, b]` names two lines or
arcs, with an optional `at: [[x,y],[x,y]]` naming the end of each to grow
(default: the end nearest the meeting point). Both grow along their own
line or circle to where they cross and the two ends are joined with one
Coincident, in one undo step. `moved` is the joined end's id.
- Where two crossings are possible (line and arc, two arcs) the one with
the least total growth is used; a tie is refused.
- A curve that already reaches the point does not grow. An end held by
other geometry or a constraint stays and gets a new line or arc.
- Parallel or collinear lines, curves that never meet, a point behind the
chosen end, the same curve twice, splines, ellipses, infinite lines,
offset geometry and malformed `curves`/`at` are refused with no change.
- The single-curve form is unchanged; its code is shared through
`grow_line` / `grow_arc`.
Canvas: in the Extend tool, Shift-click two curves. Shift goes through
the tool modifier registry as the new `extend_pair` action, and Extend
turns off Shift's midpoint snap. Escape drops a pending first pick. The
sketching guide describes the gesture.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* claim: #7893
* Toasts: one stack in the view's bottom-right corner, one width, buttons as in dialogs (#7893)
* Control protocol: toast_place in the sorted ui.prefs list (#7893)
* Toasts: a sketch test clears the status notice before clicking the view's corner (#7893)
The test's line end lands in the bottom-right corner, where notices now stack
and take clicks as they did at the bottom centre; the test is about the circle.
* Toasts: the pattern scenario's circle sits clear of the view's corner (#7893)
After the refit, a circle at y 0 landed in the bottom-right corner, where the
tool's prompt now stacks and takes clicks. The circle moves 40 mm up; the
pattern it checks is the same.
* claim: #8358
* Components: a component named like the design is reachable by name (#8358)
Document::find_component resolved the design's own name to the root component (id 0) before
searching the component list, so a component that shares the design's name was unreachable by
name: appearance.assign reported `no component `CurtainRailPlug`` (the same lookup serves every
command that takes a component by name).
An exact component name (or component id) match now wins over the design-name fallback; "0" and
"root" still always mean the design.
Regression test: a_component_named_like_the_design_takes_its_appearance_by_name (fails on main
with the reported error, passes with the fix).
* claim: #8521
* feat: give History feature folders their own symbol
A folder of features in the History on the left drew the same folder icon
as the Browser's folders. It now draws feature_group, a pile of history
chips with the front one in blue, and so do Add to New Folder and Remove
from Folder in the History's menu. Other uses of folder keep it.
* claim: sheet.* typed parameter schemas
* Sheet metal feature commands: typed parameter contracts
sheet.flange, sheet.perforate, sheet.hem, sheet.fold, sheet.unfold, sheet.refold and
sheet.convert read their parameters by hand, so a misspelt key or a value of the wrong
shape was ignored or failed late, and describe_command had only prose for them.
Each now has a contract in schema/sheet.rs, checked in front of the unchanged handler,
and returns FeatureResult. The values no shared type fits (sheet-edge points, an edge
flange's edges, a fold's line and fixed point, the flange type, the positions, the
relief shape and the perforation pattern) get types that call the handlers' own
readers, split out of cmd/sheet.rs, and give their messages. The rule's values
(thickness, K-factor, bend radius, reliefs) stay out of the contracts: the handlers
refuse them, and strict refuses them as unknown keys.
A test passed a nested `base` object whose `sketch` the flange never reads; it now
names the sketch at the top level.
Part of #2829 (#6581).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Sheet metal contracts: a further bend's angle stays as lenient as before
The flange handler reads a `then` bend's `angle` that is no expression
(`true`, a list) as left out and bends it 90 degrees. The rebased
contract had moved that check into the shared reader, so the handler
refused such a call even with the check off.
The reader is back to the handler's behaviour, and `ThenSteps` is the
stricter one, as the schema guide asks: warn reports such an angle and
strict refuses it. Tests cover the reader's leniency, the type's
refusal and the three check levels on one flange.
Part of #2829 (#6581).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Browser: the history in the browser, as a History layout (compact mode)
Settings > Interface > History gains "In browser" (view.history_layout dock: browser):
neither timeline is shown, and each component's History folder lists its sketches and
features in history order with the marker. Dragging the marker rolls the history back
through timeline.roll_to; Find in Timeline reveals the feature in the browser.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Browser history: sketches listed under their feature as in the vertical History
With Sketches: Under their feature, the history in the browser (compact mode) lists each
sketch under the first feature built from it, through the shared history_rows (#6494)
without reference rows: same rule, same fold key, so switching the History layout keeps
what is open. A drop above a row rolls back its listed sketches with it. Selecting a feature
lights the rows of its sketches, and selecting a sketch lights the features built from it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Browser: with the history in the browser, the Sketches folder starts closed
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Browser history: sketch rows have the eye of their Sketches folder row
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* History in the browser: Arabic, Hebrew and Vietnamese for the layout's name
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Dialogs: scroll a long refusal into view with its Create Issue (#8454)
A dialog's refusal comes last in its fields. When the fields fill the
dialog, the refusal sits under the fold: an Extrude Cut that the kernel
refused showed only its first line, "boolean Cut: the operation failed: no",
and no Create Issue. The dialog now scrolls a new refusal into view once:
a refusal on OK at once, a preview's once no field is being typed in, so a
field never moves away while it is typed in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Dialogs: decide the reveal in one function and test it
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Lists many PRs add to: no written lengths, one file per theme (#7747)
* xtask lists: the fixed-table allowlist in order, each with its own reason
* Lists: the wide-character blocks and the drawing scale presets as slices too (#7747)
* Lists: the drawing units test reads UNITS as a slice (#7747)
A copied body transfer was pasted like any feature and given the target
component as its owner. A transfer belongs to the component it moves bodies
to, so the saved design was refused on open ("body transfer owner must match
its destination"), and a pasted transfer had nothing to move anyway: the
copies go into the component they are pasted into. Paste now leaves body
transfers out and says so in its notices; a clipboard of only transfers is
refused.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #8410
* Preferences: reverse trackpad orbit direction (#8410)
Add "Reverse trackpad orbit direction" under the pan settings in
Preferences > Navigation. orbit_reverse defaults off, so older settings
files load unchanged, and turns the Shift + two-finger orbit the other
way on both axes, as natural scrolling flips both. Pan, zoom and mouse
orbiting are untouched. Labels in en, ar, de, he, ja, ko, vi and zh as
a pending locale file; orbit_reverse in the control protocol's sorted
settings list.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #8542
* CI: report missing translations for complete catalogs before review (#8542)
`cargo xtask i18n --check` already reads the pending locale files and fails
when a complete catalog lacks an English message, but no workflow ran it, so
the gap surfaced only in the full test run after approved-for-ci. It now runs
in Format and repository checks, and CONTRIBUTING names the complete catalogs.
* Drawing W06a: exact orthographic hidden lines for the analytic subset (#2455)
crates/geom/src/hlr (L0): prepare once per body, then hidden_lines for a
scene of placed bodies. Edges and face silhouettes are projected from their
exact curves, split where projections cross, and each piece is classified by
rays toward the viewer against the trimmed faces: visible, hidden or
explicitly uncertain, never guessed. Planes, cylinders, cones, spheres and
tori are supported whether stored as analytic records, revolutions or spline
patches; freeform faces are reported and make what they could hide
uncertain. Screen geometry is Seg2 with the W01a accuracy vocabulary: exact
lines and conic arcs where the projection is exact, bounded chords
otherwise. docs/drawing-hlr.md holds the contract and its limits.
* geom/hlr: handle the brep records main gained in #3093
Main's exact model storage (#3093) added Curve::SurfaceIntersection,
Surface::PlaneThrough and Surface::PlacedRevolution, and the HLR matches on
the records no longer compiled. A plane through three points is a plane: it
is trimmed and intersected as one, and a box stored that way draws the same
bits as one with axis-stored planes. The procedural intersection curve and
the placed revolution (whose affine placement may scale it unevenly) are
refused by prepare as not supported yet, until the kernel adapter handles
them. The records' positions move with the body's centre like the others.
* geom/hlr: pad plane faces' boxes, so a plane record off by rounding still hides
Plane faces took the box of their boundary chords unpadded, flat along the
normal. When the plane record and its edges disagree by rounding (a unit cube's
top plane one ulp above its edges, a 100 mm cube's 1e-11), the ray toward the
viewer reached the plane just outside that box, the slab test culled the hit,
and the back edges came out Visible with no doubt. Every face's box is now
padded by 1e-6 of its size, as curved faces' boxes already were. Reported by
DevonHope on #2457.
* geom/hlr: limits from review: trim samples, scene extent, crossing pairs, surface groups
From DevonHope's review of #2457:
- Options::trim_samples must be 1 to MAX_TRIM_SAMPLES (4096).
- Placed bodies must stay within MAX_COORDINATE (1e12 mm, as section and brep
bound stored numbers). Placements 1e200 apart made the scene's size infinite
and every tolerance taken from it with it, so everything came out Visible.
- The crossing search counts its candidate pairs before it starts and meets
each pair only in the first grid cell the two segments share, instead of
keeping a set of the pairs met, which could outgrow wasm's 4 GB before the
500 million pair budget tripped. The same pairs in the same order.
- prepare groups faces on one quadric or torus by comparing each face with
the faces of its kind whose size agrees to the tolerance (sorted), not with
every face; at most 50 million comparisons. prepare_cancellable polls a
cancel callback between edges, faces and comparisons.
* geom/hlr: boundary chords on a grid, and no fine pass over coincident curved chords
The cost items of DevonHope's review of #2457:
- Every ray hit asked how far it lies from its face's boundary by scanning all
of the face's chords (4096 per circle; a plate's face with 256 holes has
about a million). The chords are now bucketed on a uniform grid over the
boundary's box, cells at least as long as the longest chord, and the test
looks only at the cells around the point. It finds exactly the chords the
scan finds.
- Chords of two curved curves that lie on each other end to end (aligned holes
seen along their axes) were cut into 16 by 16 pieces and intersected, to
find crossings that are only noise within the curves' tolerance. They are
now skipped; visibility changes where a coincident run starts or ends, and
chords that only partly overlap still find those.
- Fixtures: a plate with holes, coaxial cylinders seen along their axis, and
300 placements.
* geom/hlr: pad boxes by the body's coordinates too; coincident chords only to rounding
Review of the previous commits found two holes:
- The padding grew with the face's size, but rounding grows with where the body
was modelled: a 0.001 mm cube 1e7 mm out, its top plane one step (1.9e-9 mm)
above its edges, still had its hit culled. The pad now adds 1e-12 of the
body's offset.
- Two chords were taken for coincident when their ends lay within each other's
sag, which in a coarse scene can hide real crossings: an arc can bulge across
a line 1 mm off its chord twice. Only chords that coincide to rounding are
skipped now; identical projections, the costly case, still are.
* geom/hlr: coincident curves meeting at shared sample points do not cross
Timing the review's scenes showed that skipping coincident chords was not
enough: chords of two coincident curves also touch end to end at every sample
point they share, and each touch was a crossing to refine and split at. 64
coaxial cylinders seen along their axis gave 8.3 million crossings (19 s), 64
cylinders one behind another along the line of sight 4.2 million (214 s;
before these commits the crossing budget refused it). A touch at a shared
sample point is now dropped when the two curves also coincide, to rounding, on
both sides of it. Where a coincident run starts or ends one side differs, and
the touch is kept.
* geom/hlr: the own-face margin allows for rounding where a body was modelled
The far 0.001 mm cube of the previous commit showed a second effect of records
rounded at large coordinates: its top plane, one ulp (1.9e-9 mm) above its
edges, hid the top edges themselves, because a ray leaving an edge met its own
face 3.2e-9 mm along, past the own-face margin of 1e-6 of the face (1.4e-9).
The margin now also covers 1e-14 of the body's offset (about 45 ulps of its
coordinates), which only matters for parts far smaller than their distance
from the origin.
* geom/hlr: no parity test for a hit on the boundary, where it is uncertain anyway
* docs/drawing-hlr: the fixtures of the review's second round
---------
Co-authored-by: MateuszSychXpl <151755914+MateuszSychXpl@users.noreply.github.com>
* Title blocks: what a cell shows and where its text goes (#3875)
geom::cell holds a title-block cell's content: parts of text and fields
(component, drawing, sheet, revision or prompt, by key, with an optional
unit and decimals kept as written for the resolver to check), at most 64
parts and 4096 bytes of text, with no control character but a line break.
Beside it are a resolved value (live, overridden and drifted, or missing,
with "-" for each part that couldn't be read and why), an override with
the value it replaced, and the projection a drawing's views use (first or
third angle), which the title block's symbol will read.
A fit warns and never cuts: a count of characters, not bytes, with its own
count for text in Chinese, Japanese, Korean or Yi characters.
The cell text rule places a label and a value: the label's baseline 1 mm
plus its cap height below the cell's top, the value box under it inset by
1 mm, one line centred in the box, and wrapped or several lines down from
its top at the font's pitch, with a warning when the last falls below the
box. Wrapping by width, width warnings and glyphs a font lacks need the
text measured, so they wait for the paper scene's text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Title blocks: definitions and their place on a sheet (#3875)
geom::title_block holds a title block's definition: its size (1 to 1000
mm a side), its outline and cell line widths, and up to 64 cells. Each cell
has a snake-case name of its own, a rectangle measured from the block's
lower-right corner, an optional one-line label with its cap height, and a
value: text from cell content, with its cap height, alignment and fit, or
the projection symbol, which reads the drawing's projection and is never
fixed in the block.
validate() checks a definition on its own, as when one is read: cells
inside the block and not overlapping (touching is fine), label and value
cap heights that fit, and the content's own limits. layout() puts the
block on the drawing border's lower-right corner of a sheet, refusing one
that doesn't fit, and gives each cell's rectangle, its label's place and
its value box by the cell text rule. On A3 landscape a 180 x 54 mm block
runs from (230, 10) to (410, 64); it fits A5 landscape and every larger
sheet, but not A5 upright.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Cell text: a value box lies within the paper's coordinates (#3875)
place_lines took any finite, ordered value box, but centring one far out (min x 1e308, max x
1.1e308) overflows: the anchor came back infinite, with no warning. 0xlillith found it in review
of #3877.
The paper scene already bounds every coordinate it holds by MAX_COORDINATE_MM (twenty times
the longest sheet side). That limit is now public, and place_lines keeps its box inside it, so
its anchor is always finite: one rule for what a paper coordinate may be.
Tests: the hostile-box test refuses both overflow cases from the review and a box just past the
limit on each side, and places lines in a box the size of the whole paper limit. The fuzz test's
numbers now include values past the limit (1.1e5, ±1e308).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Title blocks: padding from the lines' inner edges, one label rule, nothing unread (#3875)
A cell's text is padded from the inner edge of its lines: each side of a cell moves in by half
the line drawn on it (the outline's on the block's edges, a cell line's inside), so a wide
outline never covers the text. Before, the padding ran from the line's centre.
The label's one-line rule lives with the cell text rules (`cell::check_label`), where parts
lists will find it. The cell types nothing read yet (`Override`, `Resolved`, `State`,
`Problem`, `Reason` and `MISSING`) wait for the resolver that will read them.
The tests check the exact thresholds inside the lines (a label that leaves no room, a value
that just fits), pick each of the lines, alignment and wrapping on their own so every
combination comes up, and lay fuzzed blocks out on every standard sheet, either way, with and
without the filing margin.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Title blocks: the drawing border's line, descenders, room where the block sits (#3875)
From DevonHope's review of #3877:
- Each cell's lines are clamped to the inner edge of the drawing border's line. That line runs
along the block's right and bottom edges, and along any other edge it reaches, as when the
block fills the border. Aabb2::intersection, the 2D twin of Aabb3's, does the clamping.
- layout checks each cell's room again where the block sits (place_cells, which validate runs
at the origin), so rounding at the real corner can't turn a cell inside out.
- The cell text rule counts descenders, at 3/10 of the cap height (ISO 3098's lettering B).
They may reach into the padding under a value box, never past it to a line. place_lines
warns Height when they would, and validate asks it whether one line fits.
- The module doc no longer links to a private function.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #4816
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing views as data: named views, scales and the map onto paper (#4816, W17a P1)
What a drawing view is, before anything draws it (geom::drawing_view):
- NamedView: the six principal views and the isometric views from the four upper corners, each
an exact basis (a Plane whose normal points back at the viewer), never taken from a camera,
whose pitch clamp would tilt a top view, with no -0 to write out; `iso` reads as the
front-right one.
- basis_looking: any other view from its direction, an up and an origin (paper x = direction x up,
paper y = x x direction), refusing a zero direction, an up along it, or an origin that isn't
finite or within 1e12 mm. W07's
{direction, up, origin} view reads through it.
- Scale: paper : model as two positive numbers, read and written as "1:2", from 1:100000 to
1000:1; Scale::fit picks the largest ISO 5455 preset (50:1 to 1:10000) at which a view fits a
space (W07's "fits the page" default), compared as size x paper <= room x model so an exact
fit at a reduction isn't lost to rounding.
- ViewFrame: a view's basis, scale and place on paper, the point of its plane at `centre`
landing at `position_mm`. Its paper map moves the view's curves and their accuracy onto
paper (paper error = scale x model error); `boundary` maps a box of its plane onto paper from
its four corners.
In geom, where Seg2 lives:
- Similarity2 and Seg2::similar (new): scale, turn and shift a segment exactly, an arc staying an
arc. Similarity2::new refuses a scale that isn't above 0 and works the turn out once, not per
point;
- Seg2::map_controls: a segment's control points mapped by any function. It was doc's
sheet::map_controls; its four callers in doc use the method now, unchanged.
CurveAccuracy::scaled gives a curve's accuracy at another scale.
Tests: each named view's basis and viewer side, and the same bases from a direction and up; a 50 mm cube in every principal view at 1:1 and
1:2, and in isometric (edges 50 x sqrt(2/3), outline 50 x sqrt(2)); the view's centre on its
position (W07's 100 x 20 front view on A3 at 2:1); scales read, written and refused; the fitted
scale, an exact fit at 1:5 and ISO's smallest; no -0 in any named view's axes; an origin refused by
its own message; hostile bases, centres and positions; every segment kind moved by a similarity,
and the similarities refused; scaled accuracy.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing views: the bottom view pinned, square near parallel, bounded scales and boxes (#4816)
From DevonHope's review of #4817:
- The tests pin all six principal views exactly. The direction-and-up test uses literal ups,
so a bottom view turned half round now fails it.
- basis_looking squares x to the direction before normalising. Near the 1e-9 threshold the
normal was up to 8.8e-9 rad off; now it is about 1e-16.
- Scale::fit fits nothing for a size or room that is negative or not finite. A size of 0 still
sets no limit that way.
- Each side of a scale is 1e-6 to 1e6, so a scale writes at most 49 characters.
- ViewFrame::boundary returns a Result. It refuses a box that is inside out or not finite on
paper, instead of dropping a corner.
- paper_map, boundary and seg_to_paper say which coordinates they take.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The first export of a paper scene, as SVG first (jyviko and 0xlillith on #4890), from the research
in the plan's §16. A3b adds ellipses, hyperbolas and Béziers above degree 3, which this refuses as
"not supported yet".
- `geom::paper::Prepared`: writers read a scene through `Scene::prepare`, which checks it first, so
none reads one unchecked. Clipping goes there too, once (A8).
- `geom::paper::arc_pieces`: an arc's ends in pieces of at most a quarter turn, the last its own end.
A half turn drawn from its ends is ill-conditioned: rounded to the nanometre (six decimals of a millimetre) its
centre moves up to 1e-2 mm, where a quarter turn's stays under 1e-6 mm.
- `io::paper::paper_svg`: one user unit a millimetre, the size in mm on the root; y flipped by
writing the height less y, so the file's numbers are page coordinates and an anticlockwise arc
has sweep flag 0; lines `L`, arcs `A` (never large-arc), cubics `C`, parabolas `Q`; fills
black and even-odd; one path per item; groups as `data-object` and `data-kind` (object ids can't
be XML ids). No background, so cutters see only the marks.
- Numbers are whole nanometres (six decimals of a millimetre): no exponent, no "-0", the same bytes every time. They are the
writer's own state too, so a joint is written once and a closed path ends exactly on its first
point. Numbers are written straight onto the document, with no text made per number. A width
under half a nanometre is written as one, and a piece too short for the grid is merged into the
next, both counted in the report; a path's last piece has no next, so it is kept however short
(unless it has no length). Output over 64 MiB is refused.
Tests: the A3 drawing border's whole document; quarter, 270°, clockwise and whole-turn arcs, and a
circle of two halves giving the same four quarters; the quarter-turn boundary both sides; cubics and
parabolas, and the conics refused; the number format; a closed path whose start and end round
apart; a sliver merged; an open path's short last line and arc kept, and one of no length dropped; read back through our own SVG reader (lines exactly, the arc as an arc);
the scene's JSON mutated into the writer (never a panic).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* File format families: drawings share the design format's versioning (#3890)
The format code works for a family of files: its own tag, its own numbers and
its own folder of change files, with one reading of a file's format, one
refusal of a file a newer build saved and one upgrade for every family. Designs
(solidcraft/N) are one family and keep everything they had: FORMAT and FORMATS
are as they were, and pending_names, format_string, version_of and read mean
what they did. Drawings (solidcraft-drawing/N) are the other, with format 1 as
their first record and each later change one file in
crates/doc/src/drawing/format/pending/, as design changes are. Nothing saved
changes.
A design tag never reads as a drawing's or the other way round, and a file that
names no format is a design from before format strings, never a drawing. Every
refusal names its family; a design's read word for word as before. A design
listing a pending change this build doesn't have is now refused as a newer
build's before its content is checked, as a newer number already was, so a
newer build's fields never read as damage. The list reader is
take_names(v, key, noun), the shape #4811 uses for its own list.
For the maintainer, the release step changes: scripts/fold-format-pending.sh
now folds every family that crates/doc/build.rs lists (FAMILIES), each into
its own next number: designs into crates/doc/src/format/v<N>/ as before,
drawings into crates/doc/src/drawing/format/v<N>/ from 2. With nothing pending
in a family it says so and moves nothing. Its test folds a drawing change into
v2 beside the design changes. docs/releasing.md, CONTRIBUTING.md and AGENTS.md say so.
build.rs lists each family's changes from its own folder, each folder's
versions contiguous from its own base.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing record: format 1 of the drawing family (#3890)
A drawing is now a record of its own (crates/doc/src/drawing.rs): its name,
the designs it draws (sources: an id and the design file's path, relative to
the drawing's folder as a linked design's is), its properties under the design
metadata's rules, its units (each design's own, or one length unit of the
expression unit table), its content language (a BCP 47 tag) and its sheets.
A sheet holds its paper, a copy of its drawing border's profile, the
resolution its pictures are rendered at (300 dpi unless it says, 25 to 2400),
the component it documents and whether it is left out of the sheet count or
of printing. This is format 1 of the drawing family; each later kind of
record adds one file in crates/doc/src/drawing/format/pending/.
The rules: one id counter for every object, raised above the largest id when
a file is read and never rewound; every reference is an id that must resolve,
or the file is damaged and refused, never patched; 1 to 1,000 sheets, at most
64 sources, names of 1 to 256 bytes, paths of 1 to 4,096 bytes without NUL.
The same drawing always saves as the same bytes.
Drawing::from_json reads at most 16 MiB of text, then runs the family's format
check, the number check, the upgrade, the typed record (unknown fields refused
everywhere) and Drawing::validate. The number check is now one function every
family calls, Family::check_numbers, told which field holds other tools' data;
a design's refusals read as before.
Nothing saves or opens a drawing yet: the container is the next step.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing ids: random and collision-free from format 1 (#3890)
A drawing's objects now take random ids, the form jyviko chose on #3890 from
#1017: no counter, so objects added on two branches never share an id and
never edit one line. The allocator is one module for drawings now and designs
later (#626 Phase 3), crates/doc/src/ids.rs: an IdSource gives nonzero u64s by
splitmix64 from a seed, each checked against the ids in use and drawn again
when it is one, or when it is 0. A run never draws a number twice, so at most
one draw per id in use comes before a free one, and a seeded run over a file
the same seed made still gets a new id. IdSource::seeded gives the same ids
for tests and replays; IdSource::from_entropy seeds a session from the
standard library's hash keys, the time and a count, with no new dependency.
In a file an id is 13 characters of Crockford base32, lowercase
(0000000000001 to fzzzzzzzzzzzz): ids::encode writes it and ids::decode reads
only that spelling, case-sensitive, so an id has one form to search for.
Anything else, 0 included, makes the drawing damaged. The drawing loses
next_id (a file that holds one is refused), and Drawing::new and allocate_id
take the IdSource. Messages name ids as the file writes them. The component a
sheet documents stays its design's own number until designs move over.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing record: a drawing too large to reopen isn't saved (#3890)
from_json reads at most MAX_DRAWING_BYTES (16 MiB), but to_json had no limit. Other tools'
properties are kept as they came, and indenting them can grow a drawing read in at about 4 MB
past 16 MiB (two million zeros in one array), so the next read refused what the save had just
written. 0xlillith found it in review of #3891.
to_json now returns a Result and refuses JSON longer than a read takes ("drawing too large to
save (at most 16 MiB)"), so every drawing saved opens again. Nothing writes drawing files yet, so
the error reaches the future file writer before it publishes anything.
Test: that case is refused, and a smaller compact drawing saves and reopens unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing files: a drawing saved as a container or as plain JSON, never read as a design (#3890)
K3 of the drawing storage plan (W11.2): the file a drawing is saved in. Nothing writes one from a
command yet; opening and saving come with K5.
- A drawing container is a design container holding `drawing.json` (Deflate, as `document.json`)
in place of `document.json`, and `container.json` says so:
{"content": "drawing", "format": "solidcraft-container/1"}. A design's `container.json` is as it
was, with no `content`. The plain JSON form is the drawing's own text. Both give the same bytes
every save, so a drawing can live in version control as text.
- `io::file_kind` tells a design from a drawing by `container.json` or by the JSON's format. The
design reader refuses a drawing ("this file is a SolidCraft drawing, not a design: open it as a
drawing") and the drawing reader a design; plain JSON only asks when it isn't a design, so
reading a design costs nothing more. A container's content this build doesn't know, or a drawing
container that also holds a design's `document.json`, is refused.
- `read_drawing` (bytes, either form), `read_drawing_file` (one seekable handle, the design
reader's checks) and `write_drawing_container`; `DRAWING_EXT` (`scdwg`) and
`is_drawing_path` beside the design's.
- The container writer's set-up and finish, the file opening and the container sniffing move into
one function each, which designs and drawings both use. The container's own checks (budgets,
entries, the ZIP directory) are the design container's, word for word.
Tests: a drawing round-trips through both forms byte for byte, and the container holds exactly
`container.json` and `drawing.json`; `file_kind` on both forms of both kinds; each reader refuses
the other's file; unknown content, a stray `document.json`, a missing drawing, an unknown
container version and a damaged drawing refused; both forms from disk, and a directory refused;
mutation fuzz of both forms (an error or a drawing, never a panic).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Bill of materials types: phantom, reference, purchased and inseparable (#3874)
A component's bill of materials type is a part property (`bom_type` in its metadata, set with
component.metadata.set and read by component.metadata, as part_number is), so older releases
keep it and no format change is needed. It isn't a design property: the Design Properties
dialog doesn't list it and document.metadata.set refuses it. A word this build doesn't know
reads as normal and is kept.
assembly.bom counts by type: a phantom has no row and what it holds counts where it is placed,
in the same rows as the same parts there; a reference counts nowhere, and nothing inside it
does; purchased and inseparable components are one row, and in parts only an inseparable
component's purchased parts are listed too. Rows of one part number but different types are
different rows. Rows and children carry bom_type. With no types set the rows are as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Glossary: bill of materials and bill of materials type (#3874)
The bill of materials types are a new idea, so the glossary gets their
term, with the five types, and "normal" gets its fourth meaning in the
table of words with more than one meaning.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Bill of materials types: a plain line for each (#3874)
The guide's table and the glossary entry say what each type is for in
plain words: something you make, something just to group, something to
use as a reference, something you buy, something made as one piece.
The names stay the five standard ones.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Bill of materials: kits fold together only when their contents' types match (#3874)
With group_children, two kits with one part number share a row when what they hold reads the
same. That reading (kit_key) compared each child by part number and count, but not by its bill
of materials type, though rows themselves are told apart by type. So a kit holding a made bolt
and a kit holding a bought one, both KIT, folded into one KIT x2 row listing the first kit's
bolt twice: the other type was lost, and which one depended on creation order. 0xlillith found
it in review of #5107.
kit_key now includes each child's type. Equal part number, type and contents still fold.
Test: two KIT kits each holding a BOLT fold into one row of two; once one bolt is purchased they
are two rows, each listing its own bolt. Planted fault (the type left out of the key): caught.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A border profile capped every length at 5000 mm (the largest sheet's side) but two: a
centring mark's reach inside the drawing border and a trimming mark's length. A file could
give either a huge whole number, such as 18446744073709551615. The read's number check passes
whole numbers, so the profile took it as 1.8e19 mm, and saving wrote it as a decimal the next
read refused: a drawing that saved but didn't open again. K2's fuzz test of drawing files
(#3890) found it.
Both now take the cap the margins have, `MAX_SIDE_MM`, in `validate`, so the file is refused
when it's read. Layout's own checks against the sheet are unchanged, so a 5000 mm mark still
validates and is refused only on a sheet too small for it.
Tests: the hostile-profile test refuses 5000.000001 mm and 1.8e19 mm for each mark, and takes
5000 mm on its own; 4 planted faults (each cap removed or made one short) are all caught.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The built-in sheet's centring marks now run from the trimmed sheet's edge to the drawing border.
From the grid reference border, as before, a mark was the same length as a grid field mark, and on
A3 it sat on one (the sheet's centre falls on a field edge), so it no longer read as a mark once the
grid reference border's outline is left out (Cameron, after the prototype on #3875).
- DrawingBorderProfile.centring_mark_start: sheet_edge (the built-in's, and when not given) or
grid_border (ISO 5457's figure 5); ISO's full border in the tests asks for grid_border.
- A mark still needs a length: one that stops on the drawing border needs room outside it on the
side it starts from, every margin for sheet_edge, the grid reference border for grid_border.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
geom::paper holds what a sheet shows as plain data: groups of strokes and fills, each group tagged
with the object that drew it (an opaque id) and its kind, with one validation pass over a bounded
record count. Seg2's checks and the coordinate rule move into profile.rs so section and paper share
them, and drawing_sheet takes its side and line limits from paper, so each limit is written once.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* test(oracle): reviewer-aligned parameterized workshop plate fixtures (#4950)
Independent analytic W23 expected values and seven verified models,
including four R10 corners and the 2 mm tangent top chamfer.
Document accepted Drawing states and commit regenerated docs/oracle.md
from successful workflow run #29 (90/90 pass).
* fix(oracle): correct W23 chamfer centroid and clarify drawing session ownership
* Drawing sheets: sizes and the ISO 5457 border (#3875)
A sheet is a paper size and an orientation: ISO A0 to A5, Letter, Legal,
ANSI A to F, architectural A to E1, or a custom size (finite, above 0 and
at most 5,000 mm a side), landscape by default and either way for every
size.
A border profile is the rule for its border, and layout() works it out
for a sheet: the frame inside the margins, and the reference fields with
their labels. The ISO profile has a 20 mm filing margin on the left and
10 mm elsewhere, a 0.7 mm frame, fields of 50 mm out from the centre
lines with the corner fields taking what's left, rows lettered from the
top without I and O (AA, AB and on past Z), columns numbered from the
left, and an A4 sheet labelled on its top and right only. A profile can
divide the frame equally instead, or have no grid.
The records sit in geom, next to the section records, so saving,
rendering and export can all use them. Nothing is saved or drawn yet:
there is no command and no format change. Margins that leave no drawing
space, values that aren't finite, fields outside 1 to 1000 mm, more than
200 equal fields and unknown JSON fields are refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing sheets: ISO 5457's centring and trimming marks (#3875)
A drawing border profile now also holds the grid reference border and the
centring and trimming marks, either of which can be left out. The ISO
profile follows ISO 5457:1999 (read in its identical adoption,
IS 10711:2001):
- the grid reference border 5 mm outside the drawing border on every
side, with the field labels between the two;
- four centring marks on the sheet's centre lines, from the grid
reference border to 10 mm inside the drawing border;
- at each corner a trimming mark of two overlapping 10 x 5 mm
rectangles, in the margin outside the grid reference border.
The labelled sides are now a rule worked out for each sheet: ISO 5457's
(the top and right of A4, all four sides of the others) or fixed sides.
So one profile labels every size correctly. validate() checks what
doesn't depend on the sheet, for when a profile is read; layout() calls
it, then checks the rest against the sheet.
Refused:
- a grid reference border that isn't finite, is negative or is wider
than a margin;
- a centring mark that isn't above 0, would reach past the sheet's
centre lines, or is on a drawing border those lines miss;
- a trimming mark that isn't above 0, is wider than it is long, is
wider than the margin left outside the grid reference border, or is
longer than half the sheet's shorter side.
The grid rule's JSON is now externally tagged ("none",
{"from_centre": {"field_mm": 50}}), as the sheet size is, so an unknown
field is refused in every form; the internally tagged "none" ignored
one. Nothing is saved yet, so the shape change costs nothing.
The names follow the glossary, and ISO 5457's "drawing sheets": the
module is drawing_sheet (was sheet, which means sheet metal elsewhere
here), DrawingBorderProfile and SheetLayout (were BorderProfile and
BorderLayout), sheet_border and drawing_border (were paper and frame),
and Sheet::size_mm (was paper_mm). The first part of S1 is still a draft,
so the renames are made here rather than in a commit of their own.
The extra centring marks ISO 5457 asks for on sheets larger than A0
aren't here yet. Still nothing is saved or drawn.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawing sheets: a lighter built-in border, the filing margin a choice (#3875)
Cameron's decisions after seeing the sheet in the app, 2026-10-23:
- No trimming marks in the built-in profile: the filled corner blocks
take a lot of ink. trimming_mark_mm stays, so a profile can still ask
for them.
- Centring marks stop at the drawing border: from the grid reference
border to the drawing border, never into the drawing area (on A4 they
crossed the tables). centring_mark_mm keeps its meaning, how far a
mark reaches inside the drawing border, and now takes 0, the built-in
value, as grid_border_mm already takes 0 for "on the drawing border".
A mark of no length (a reach of 0 with no grid reference border) is
refused.
- Margins of 10 mm all round. ISO 5457's 20 mm filing margin on the
left, for drawings that get bound, is a named choice: filing_margin,
off when not given. On, it stands in for margins.left_mm, which is
kept, and still checked, for when it's off again. The 20 mm is
FILING_MARGIN_MM, and validate() and layout() both take the margins
from one helper, laid_out_margins().
A3 landscape is now 400 x 277 at (10, 10), with eight 50 mm columns.
With the filing margin it is 390 x 277 at (20, 10) again, the same as a
20 mm left margin set by hand. A profile with all three choices on
(ISO 5457's full border) lays out exactly as the built-in profile did
before, and the tests keep the old hand values for it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 03:01:08 -04:00
899 changed files with 158177 additions and 6314 deletions
# Cargo.lock or a Cargo.toml changed on a pull request or in a merge group.
if [ "$DEPS_RUN" = true ]; then
test "$ADVISORIES" = success
fi
# A merge group that reaches the wasm build.
if [ "$WASM_RUN" = true ]; then
test "$WASM" = success
fi
- name:Note a carried result
if:needs.changes.outputs.carried != ''
env:
@@ -1033,9 +1090,13 @@ jobs:
run:|
echo "This push only merges main into a change that already passed; the heavy checks were not repeated. Passing run: $CARRIED" >> "$GITHUB_STEP_SUMMARY"
# Every crate below the apps and the web app must build for the browser (`cargo xtask wasm`, a
# cargo check, no tests). In the merge queue for a group that reaches the wasm build (Select
# checks' `wasm`), where `ci` requires it; by hand with cross_compile.
@@ -603,6 +603,31 @@ Original SolidCraft drawings, Open for owner review; no runtime assignments.
| assets/icons/ui/design/svg/model/finish_edit.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| `assets/locales/pending/sketch-tracking-guides.json` | SolidCraft contributors | Original alignment-guide preference labels and help text | MIT OR Apache-2.0 |
| assets/locales/pending/point-clouds.json | SolidCraft contributors | Original point-cloud UI strings and translations | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/feature_group.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/mesh_body.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/timeline_play.svg | SolidCraft contributors | Original history playback icon; canonical grey_cube base and original action cue | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/timeline_stop.svg | SolidCraft contributors | Original history playback icon; canonical grey_cube base and original action cue | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/status_pin.svg | SolidCraft contributors | Original analytic pin icon for persistent viewport results | MIT OR Apache-2.0 |
| assets/locales/pending/render-wear.json | SolidCraft contributors | Original UI labels and translations | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/equation_curve.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/locales/pending/sketch-recent-snap.json | SolidCraft contributors | Original preference messages for the recent centre and midpoint snap | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/construction_line.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/construction_line_horizontal.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/construction_line_vertical.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/construction_line_angle.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/construction_line_parallel.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/construction_line_perpendicular.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/construction_line_tangent_external.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/construction_line_tangent_at.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/construction_line_tangent_two.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/construction_line_bisector.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| docs/book/src/generated/equation-circle.png | SolidCraft contributors | rendered by `cargo xtask book` (`solidcraft-cli snapshot docs/book/scripts/equation-circle.json`; gitignored) | MIT OR Apache-2.0 |
| docs/book/src/generated/equation-flower.png | SolidCraft contributors | rendered by `cargo xtask book` (`solidcraft-cli snapshot docs/book/scripts/equation-flower.json`; gitignored) | MIT OR Apache-2.0 |
| docs/book/src/generated/equation-involute.png | SolidCraft contributors | rendered by `cargo xtask book` (`solidcraft-cli snapshot docs/book/scripts/equation-involute.json`; gitignored) | MIT OR Apache-2.0 |
| docs/book/src/generated/equation-line.png | SolidCraft contributors | rendered by `cargo xtask book` (`solidcraft-cli snapshot docs/book/scripts/equation-line.json`; gitignored) | MIT OR Apache-2.0 |
| docs/book/src/generated/equation-parabola.png | SolidCraft contributors | rendered by `cargo xtask book` (`solidcraft-cli snapshot docs/book/scripts/equation-parabola.json`; gitignored) | MIT OR Apache-2.0 |
| docs/book/src/generated/equation-spiral.png | SolidCraft contributors | rendered by `cargo xtask book` (`solidcraft-cli snapshot docs/book/scripts/equation-spiral.json`; gitignored) | MIT OR Apache-2.0 |
| docs/book/src/generated/equation-wave.png | SolidCraft contributors | rendered by `cargo xtask book` (`solidcraft-cli snapshot docs/book/scripts/equation-wave.json`; gitignored) | MIT OR Apache-2.0 |

@@ -98,7 +98,7 @@ More commands, the Windows cross-build and the browser build are in [docs/buildi
runs its own design session, or with `--connect` it drives the running app so you can watch.
Claude Code: `claude mcp add solidcraft -- solidcraft-cli mcp`. If a terminal doesn't find
`solidcraft-cli` (older Windows installers don't put it on `PATH`), give its full path (by default
`"C:\Program Files\SolidCraft\solidcraft-cli.exe"`). Tools, access rules and an example session:
`"C:\\Program Files\\SolidCraft\\solidcraft-cli.exe"`). Tools, access rules and an example session:
[docs/mcp.md](docs/mcp.md).
## Scripts
@@ -150,3 +150,4 @@ Tinkercad are trademarks of their respective owners, named only to describe comp
constUSAGE_RUN: &str=" solidcraft-cli run <script.json|design.sc|part.step> [--in design.sc|part.step] [--out FILE]... [--save FILE] [--quiet] [--results] [--timings] [--profile] [--json-out FILE] [--allow-errors]\n solidcraft-cli run --in <design.sc|part.step> [--out FILE]...\n (a timeline feature that fails fails the run and nothing is written; --allow-errors writes anyway)\n (--timings prints {timings: [{index, command, ms}], total_ms} for a script, with --results its results too;\n --profile adds each command's profile: {boolean_ms, tessellate_ms, other_ms}, the kernel's share)\n";
constUSAGE_RUN: &str=" solidcraft-cli run <script.json|design.sc|part.step> [--in design.sc|part.step] [--out FILE]... [--save FILE] [--quiet] [--results] [--timings] [--profile] [--json-out FILE] [--allow-errors]\n [--param-check off|warn|strict]\n solidcraft-cli run --in <design.sc|part.step> [--out FILE]...\n (a timeline feature that fails fails the run and nothing is written; --allow-errors writes anyway)\n (--param-check checks each step against its command's contract: warn reports on stderr, strict stops the run)\n (--timings prints {timings: [{index, command, ms}], total_ms} for a script, with --results its results too;\n --profile adds each command's profile: {boolean_ms, tessellate_ms, other_ms}, the kernel's share)\n";
/// `eval`'s lines in the usage.
constUSAGE_EVAL: &str=" solidcraft-cli eval <script.json|design.sc|part.step> [--in design.sc|part.step] [--json-out FILE] (alias: inspect)\n solidcraft-cli eval --in <script.json|design.sc|part.step> [--json-out FILE]\n (with both inputs, runs the positional script on --in before measuring; does not automatically save the modified design)\n (--json-out FILE writes the JSON that run (with --results, else the measurement) and eval print to FILE instead of stdout,\n as plain UTF-8: a shell's > can re-encode it, e.g. Windows PowerShell adds a byte-order mark)\n";
"ui.part_size_needs_a_part":"Vælg en indsat standarddel (en skrue, møtrik, skive eller stift) for at ændre dens størrelse. Brug Skalering til at gøre et emne større eller mindre.",
"ui.part_size_shared":"Genopbygger delen og erstatter alle tilføjede features. Alle forekomster af komponenten opdateres.",
"A Single hole snaps to the middle of its face and to the centres of its round edges":"A Single hole snaps to the middle of its face and to the centres of its round edges",
"Base Center":"Base Centre",
"Center X":"Centre X",
"Center Y":"Centre Y",
"Center of mass":"Centre of mass",
"Centre to centre":"Centre to centre",
"Circle center":"Circle centre",
"Colour":"Colour",
"Copy all licences":"Copy all licences",
"Customize Toolbar":"Customise Toolbar",
"Customize…":"Customise…",
"Face center":"Face centre",
"Favorites":"Favourites",
"Favourite":"Favourite",
"Mass Center":"Mass Centre",
"Modeled":"Modelled",
"Select the centre the pattern turns around first":"Select the centre the pattern turns around first",
"Snap to face and edge centres":"Snap to face and edge centres",
"cmd.sketch_slot_arc_center":"Centre Point Arc Slot",
"cmd.sketch_slot_center_point":"Centre Point Slot",
"cmd.sketch_slot_center_to_center":"Centre to Centre Slot",
"color.copy":"Click to copy colour values",
"color.edit":"Click to edit colour",
"color.gamma":"Showing colour values in 0-255 gamma space",
"color.linear":"Showing colour values in 0-1 linear space",
"color.selected":"Selected colour",
"dxf.cancelled":"DXF export cancelled",
"dxf.cancelling":"Cancelling DXF export",
"help.apache":"Apache licence 2.0",
"help.appearance.assign":"Gives the selected bodies, faces or components a colour or a material look, without changing their weight. Use it to tell parts apart or to preview a finish.",
"help.face.delete":"Removes the selected faces and lets the neighbouring faces close the gap, or leaves the gap open as a surface. Use it to take off a rounded edge, a hole or a boss that is in the way.",
"help.font_details":"The interface uses egui's built-in fonts (Ubuntu Light and Hack, under the Ubuntu Font Licence and the MIT/Bitstream Vera licence; Noto Emoji and the emoji icon font, under the OFL and MIT). Sketch text uses Liberation Sans (SIL OFL 1.1). The Crafting Apps' optional font pack, craft-fonts (BIZ UDPGothic, Shippori Mincho, BIZ UDMincho, Noto Sans CJK SC, Noto Sans Arabic; SIL OFL 1.1), is not bundled in SolidCraft builds.",
"help.licences":"Licences",
"help.material.assign":"Sets what the selected bodies are made of, such as steel, aluminium or ABS. Use it so that the mass and weight match the real part.",
"help.mit":"MIT licence",
"help.solid.draft":"Tilts the selected faces by the angle you set, measured from a neutral plane. Use it on moulded or cast parts so that they slide out of the mould.",
"help.solid.revolve":"Spins a sketch profile around an axis to make a round solid. Use it for shafts, knobs, bowls and any part that is round about a centre line.",
"help.solid.split_body":"Cuts a body into separate bodies with a plane, the face of another body or a sketch line. Use it to make parts that are printed, machined or coloured on their own.",
"help.solid.split_face":"Divides the selected faces along a plane, the face of another body or sketch curves, without cutting the body. Use it to give part of a face its own draft, colour or offset.",
"help.third_party":"Third-party licences",
"home.new_mm_hint":"Millimetres · opens in Design",
"navbar.description.separator":"A thin line that groups neighbouring slots.",
"pattern.alignment_hint":"Counts include the original. Centre balances copies; Flip chooses the extra side for even totals.",
"prefs.bottom_centre":"Bottom centre",
"prefs.logo_theme":"Theme's colours",
"prefs.max_file_size":"Largest file to open",
"prefs.max_file_size_design":"Design files",
"prefs.max_file_size_flag":"This run started with --max-file-size {size}, which applies until SolidCraft closes. These settings apply when it starts without it.",
@@ -24,6 +76,11 @@
"prefs.max_file_size_step":"STEP and import files",
"prefs.max_file_size_web":"The web app opens STEP and import files up to 512 MB and designs up to 256 MB: a browser tab can't hold larger files. The desktop app can raise these limits.",
"prefs.megabytes":"MB",
"prefs.sketch_conflict_colour":"Conflicting geometry in the conflict colour",
"prefs.sketch_conflict_colour_note":"While a sketch's constraints conflict, the curves and points a failing constraint holds are drawn in the conflict colour too, not only its glyph or dimension. Off: they keep their usual colours.",
"prefs.sketch_construction_lines":"Extended construction line tools",
"prefs.sketch_recent_snap":"Auto snap to recent centres and midpoints",
"prefs.sketch_recent_snap_note":"While drawing, snap the cursor where the guides of the two most recent circle centres or straight-edge midpoints cross. Off: those guides stay visual and the cursor does not move.",
"prefs.sketch_state_icons":"Sketch icons in the colour of their state",
"prefs.sketch_state_icons_note":"In History and the browser, a sketch's icon takes the colour of its state (under constrained, fully constrained or conflicting) and its tooltip names the state. Off: every sketch has the same icon.",
"prefs.sketch_tracking_guides":"Centre and midpoint alignment guides",
@@ -35,6 +92,9 @@
"prefs.theme_follow_note":"Reloaded whenever another tool rewrites it. Choosing a theme stops following it.",
"prefs.theme_follow_off":"Off",
"prefs.theme_follow_stop":"Stop following",
"prefs.trackpad_zoom_modifier.help":"Hold only this key to zoom at the pointer with vertical scroll. Off keeps the existing scroll behaviour.",
"render.aging_airborne_help":"Airborne dust is a separate finite homogeneous scattering volume. It changes transport and raw EXR, not the surface-aging stack. Existing volume/glass/denoising restrictions remain explicit; no atmosphere is added automatically.",
@@ -62,6 +122,9 @@
"render.aging_target_help":"A named target must still match its captured index and name. Global masks are world-anchored; they do not follow moving parts. This is neither cavity detection nor true edge wear. Filter the list to reach additional bodies.",
"render.background_hdr_separate":"The current HDR resources illuminate and reflect on surfaces; they are not automatically displayed as a panorama behind the subject. Changing a gradient does not rotate or recolour the lighting.",
"render.background_lighting_section":"HDR illumination · separate from camera background",
"render.background_no_alpha_override":"A camera background is not added to transparent output silently. Raw-master regrading uses captured coverage and does not reapply denoising; the source EXR remains unchanged.",
"render.background_not_light":"Background colour is presentation, not an additional lighting source.",
"render.background_regrade":"Apply to retained raw master",
"render.background_stage_occlusion":"This shot includes physical stage geometry. A large floor or wall can fill the frame and hide a camera-only background. Clear or resize the stage deliberately, or change its physical reflectance. Regrading cannot remove captured geometry.",
"render.background_workflow":"Choose the background you intend: a camera-only colour/gradient, a physical stage that receives light, or HDR illumination. They are independent. Live Preview is a modelling view; render a new image or regrade a retained master to see camera finishing.",
"render.base_color":"Base colour · sRGB",
"render.bloom_help":"Creative output-only glow from positive linear highlights, spread with three normalised box passes. Radius is in output pixels. Added in linear display light before sRGB; not a lens, volumetric effect or convergence improvement. The raw EXR is unchanged.",
"render.cpu_area_editor_help":"Full side vectors in millimeters; U cross V points towards the emitting side. This differs from GPU half-edge vectors.",
"render.cpu_light_help":"Up to eight directional lights. An empty custom list turns them off; selecting a preset restores its lights. Colours are linear RGB and intensity is relative, not watts.",
"render.effects_gpu_only":"These effects require GPU path tracing. Changing the page never switches the renderer automatically.",
"render.effects_help":"Combine ordered colour looks and image-space lens effects. Rendering and raw-master regrading use the same saved settings. Raw EXR stays unchanged. These are original artistic controls, not calibrated lens or branded-film emulations.",
"render.effects_reset_lens":"Reset lens effects",
"render.effects_seed":"Pattern seed",
"render.env_colour":"Colour",
"render.environment.radial_only":"Centre and radius apply to Radial mode.",
"render.error_finish_gesture":"finish the modelling gesture before previewing",
"render.error_finish_operation":"finish the current modelling operation before entering Render",
"render.hdr_primaries_undeclared":"The source does not declare primaries; review the renderer colour requirements.",
"render.image_cancelled":"Render cancelled",
"render.image_cancelling":"Cancelling",
"render.image_scene_stale":"Finish the modelling gesture or refresh the scene before checking or starting an image.",
"render.inspector_colour":"Colour",
"render.layer_down":"Move down",
"render.layer_enabled":"Layer enabled",
"render.layer_remove":"Remove layer",
@@ -90,6 +171,18 @@
"render.layer_up":"Move up",
"render.lens_effects_help":"Flare uses up to four separated bright sources in the actual linear image. Ghosts, halo, streaks, chromatic offsets and radial distortion are artistic image-space approximations. Spatial effects need opaque output; outside samples extend the image edge. Grain is deterministic. No physics, EXR or geometry is altered.",
"render.library_inspect_only":"Select to inspect; nothing is applied automatically. Favourites and recent history belong to this open document session.",
"render.library_rig_help":"Fit once at an explicit centre and physical extent. Complete rigs include passive cards; calibration blockers still refuse. Conservative emitter/card clearance is checked before saving.",
"render.library_source_preview_help":"Authored base-colour source map. This is not a rendered or measured material preview.",
"render.library_unfavorite":"Remove favourite",
"render.light_behaviour":"BEHAVIOUR",
"render.light_preview_help":"Live view is unshadowed; Area lights use a centre-sample approximation. Render uses the complete light model.",
"render.material_color_replace":"Base map replaces body colour",
"render.material_map_required":"Choose a colour image or a normal image before using maps.",
"render.material_maps":"Planar colour and normal maps",
"render.material_maps_help":"PNG/JPEG up to 1024×1024; 16 MiB encoded total. U and V are orthogonal world-mm repeat vectors (maximum 128 repeats). Colour is sRGB; normal data uses OpenGL +Y. Files are captured at Check/Start.",
"render.opaque_preview_help":"Opaque PBR and simple colours; blended transparency. Refraction, HDRI and final rendering are not available yet.",
"render.page_aging":"Aging & contamination",
"render.page_background":"Backgrounds",
@@ -108,22 +206,45 @@
"render.radial_distortion":"Radial distortion",
"render.ribbon_aging":"Aging",
"render.ribbon_effects":"Effects",
"render.ribbon_studios_help":"Browse implemented studio compositions and review their replacements before applying. Concept-only scenes remain labelled.",
"render.sharpening":"Detail sharpening",
"render.soft_focus":"Soft focus",
"render.stage_panels.centre":"Centre",
"render.stage_panels.source_note":"Authored base-colour source map. Not a measured material.",
"render.studio_area_help":"Up to four one-sided rectangles. Orthogonal half-edges U and V define size; U × V faces the lit side. Coordinates are world-space millimetres.",
"render.studio_center":"Centre X, Y, Z (mm)",
"render.studio_hdr_help":"Radiance .hdr, 2:1 panorama, up to 2048 × 1024. EXR is unavailable. The colour assumption applies only when primaries are unspecified.",
"sketch_text.align_center":"Centre",
"ui.3d_mouse_kind_cap":"Return to Centre",
"ui.3d_mouse_looks_like_cap":"The reading stays put while you hold it, the way a Return to Centre device does.",
"ui.3d_mouse_orbit_tip":"Traced: the 3D mouse turns the view around the part of the model in the middle of the view when the turn begins, marked by a faint ball. Classic: around the view's centre",
"ui.3d_mouse_step_pull":"Pull the cap towards you, hold it, then let go.",
"ui.3d_mouse_switch_cap":"Calibrate as Return to Centre",
"ui.agent_chat_minimize":"Minimise to a strip",
"ui.ai_mode_edits_note":"Modelling commands run without asking; saving, opening, exporting and other file or destructive actions still ask.",
"ui.center":"Centre",
"ui.center_arc_follows_cursor":"Centre point arc follows the cursor's direction",
"ui.center_arc_follows_cursor_tip":"A centre point arc runs clockwise or counter-clockwise the way the cursor goes round its centre. Off: always counter-clockwise from the start.",
"ui.centerline":"Centreline",
"ui.colour_vision":"Colour vision",
"ui.colour_vision_note":"Changes the colours that carry a meaning: sketch states, axes, bends, analyses and icons. If you use Windows colour filters, keep this Off.",
"ui.colour_vision_off_tip":"The theme's own colours",
"render.material_limits":"최대 128개 바디 설정, 분리된 유리 32개, 코팅 64개, 텍스처 연결 128개. 같은 맵은 메모리를 공유하며 실제 장치 및 전체 메모리 제한은 유지됩니다.",
"render.material_metallic_map":"금속성 맵 (선형)",
@@ -1894,9 +2244,11 @@
"render.output_advanced":"고급",
"render.output_backend_ready":"준비 완료",
"render.output_check":"확인",
"render.output_check_failed":"검사 · 오류",
"render.output_check_help":"확인을 실행하면 렌더링을 시작하지 않고 요청을 검토합니다.",
"render.output_check_summary":"검사 · 참고 {count}개",
"render.output_copy":"복사",
"render.output_copy_report":"보고서 복사",
"render.output_destination":"저장 위치",
"render.output_destination_help":"완성된 이미지를 내보낼 때 대상 파일을 선택합니다.",
"render.output_dimensions":"크기",
@@ -1906,16 +2258,20 @@
"render.output_fit":"맞춤",
"render.output_format":"형식",
"render.output_format_status":"PNG는 완료된 표시 이미지 그대로입니다. 선택적 GPU EXR은 표시 필터 전 선형 FLOAT32 방사 휘도를 보존합니다. 미리보기 캐시나 다른 작업으로 바꾸어 내보내지 않습니다.",
"render.output_grading":"색 보정",
"render.output_image":"이미지",
"render.output_largest":"{width} × {height} 최대",
"render.output_presets_help":"형식, 품질 및 검사 목록은 출력 편집기에 있습니다.",
"render.output_quality":"품질",
"render.output_quality_waiting":"품질 규약 대기 중",
"render.output_ready":"렌더링 준비 완료",
"render.output_run_check":"검사 실행",
"render.output_size_limit":"{method}: 한 변 최대 {dimension} px, 총 {pixels} 픽셀. 가능한 최대 크기: {width} × {height}.",
"render.output_size_ok":"크기 정상 · {width} × {height} px",
"render.output_tools_inspector":"출력 검사기에서 크기와 형식을 편집하세요.",
"render.output_view_image":"이미지 보기",
"render.paint_label":"칠하기",
"render.painting_with":"{name}(으)로 칠하는 중",
"render.passive_stage":"Render-only stage",
"render.passive_stage_help":"Finite passive panels belong only to rendered images. They do not add CAD bodies or mass. Placement is fixed after applying; intersecting product bounds refuse. CPU does not provide diffuse bounce lighting.",
"render.perspective":"원근 카메라",
@@ -1934,6 +2290,7 @@
"render.pick_help":"바디, 면 또는 구성요소를 선택합니다. 빈 공간을 클릭하면 선택이 해제되고 Shift 또는 Ctrl로 추가하거나 전환합니다. 강조 표시는 실시간 제품 뷰에만 나타납니다.",
"render.pick_label":"선택",
"render.pick_up_label":"가져오기",
"render.pick_up_prompt":"외관을 가져올 객체를 클릭하세요",
"render.pick_waiting":"제품 선택은 아직 지원되지 않습니다.",
"render.preview":"통합 미리보기",
"render.preview_help":"래스터 셰이딩과 블렌딩 투명도를 사용합니다. 굴절, HDRI 및 최종 렌더링은 아직 지원하지 않습니다.",
@@ -1952,10 +2309,14 @@
"render.radial_compare":"비교",
"render.radial_fit":"제품 맞추기",
"render.radial_save":"샷 저장",
"render.recent_looks":"최근 외관",
"render.regrade_label":"색상 재조정",
"render.reload_settings":"저장된 설정 다시 불러오기",
"render.remove_override":"재정의 제거",
"render.replace_all":"모두 바꾸기",
"render.replace_label":"바꾸기",
"render.replace_saved_surface":"Replace an existing saved binding for this target",
"render.saved_surfaces_help":"Pinned built-in maps follow the selected occurrence in local millimeters. A replaced or missing target is an error, never an automatic reassignment.",
"render.scene_all_shots":"모든 샷 ({count})",
"render.scene_batch_reason":"모든 샷에는 아직 지원되지 않는 저장된 샷 렌더링 대기열이 필요합니다.",
"render.scene_check_now":"지금 검사",
"render.scene_check_passed":"검사 통과 · 참고 {notes}개",
"render.scene_check_stale":"검사가 오래됨 · 다시 검사",
"render.scene_check_unchecked":"아직 검사하지 않음 · 참고 0개",
"Drawing files and the paper renderer are not implemented. Planned tools show their options where defined.":"لم تُنفّذ ملفات اللوحات ولا عارض الورق بعد. تعرض الأدوات المخطط لها خياراتها حيثما حُدّدت.",
"Drawing workspace":"مساحة عمل اللوحات",
"Edit Drawing Table":"تعديل جدول اللوحة",
"Edit Table":"تعديل الجدول",
"Export Drawing":"تصدير اللوحة",
"Finish the current operation before entering Drawing":"أكمل العملية الحالية قبل الدخول إلى مساحة اللوحات",
"Hole Callout":"بيان الثقب",
"Image":"صورة",
"Inherited from parent view":"موروث من المسقط الأصل",
"Manage sheets and docking":"إدارة الأوراق والإرساء",
"NOTES":"الملاحظات",
"No drawing document":"لا يوجد مستند لوحة",
"No drawing geometry available":"لا تتوفر هندسة للوحة",
"No drawing objects":"لا توجد عناصر في اللوحة",
"No drawing sheet is available yet.":"لا تتوفر ورقة لوحة بعد.",
"No drawing view available":"لا يتوفر مسقط للوحة",
"No sheet available":"لا تتوفر ورقة",
"Not available":"غير متاح",
"Not configured":"غير مُعدّ",
"Not implemented. These options describe the planned interface; no drawing data is changed.":"غير منفّذ. تصف هذه الخيارات الواجهة المخطط لها؛ لا تتغير أي بيانات للوحة.",
"Note":"ملاحظة",
"Paper & layout":"الورق والتخطيط",
"Paper colour":"لون الورق",
"Paper colour will be available when the sheet renderer is implemented.":"سيتوفر لون الورق عند تنفيذ عارض الورقة.",
"Parent view":"المسقط الأصل",
"Parts & Tables":"الأجزاء والجداول",
"Parts List":"قائمة الأجزاء",
"Print":"طباعة",
"Print Drawing":"طباعة اللوحة",
"Profile & units":"ملف التعريف والوحدات",
"Projected View":"مسقط مشتق بالإسقاط",
"RESOURCES":"الموارد",
"REVISION":"المراجعة",
"References":"المراجع",
"Resources":"الموارد",
"Return to Design to use this command.":"عُد إلى مساحة التصميم لاستخدام هذا الأمر.",
"Review":"مراجعة",
"Revision Table":"جدول المراجعات",
"Revision Tbl.":"جدول المراجعات",
"SOURCE":"المصدر",
"SYMBOLS":"الرموز",
"Screen appearance":"مظهر الشاشة",
"Section View":"مسقط مقطعي",
"Sheet Settings":"إعدادات الورقة",
"Sheet unavailable":"الورقة غير متاحة",
"Size & orientation":"الحجم والاتجاه",
"Source BOM":"قائمة مواد المصدر",
"Source Flat":"النمط المسطح للمصدر",
"Source design":"التصميم المصدر",
"Source properties":"خصائص المصدر",
"Space":"توزيع المسافات",
"Space Annotations":"توزيع المسافات بين التعليقات التوضيحية",
"Drawing files and the paper renderer are not implemented. Planned tools show their options where defined.":"קובצי שרטוט ומנגנון רינדור הנייר טרם מומשו. כלים מתוכננים מציגים את האפשרויות שלהם במקומות שבהם הוגדרו.",
"Drawing workspace":"סביבת עבודה לשרטוט",
"Edit Drawing Table":"עריכת טבלת שרטוט",
"Edit Table":"עריכת טבלה",
"Export Drawing":"ייצוא שרטוט",
"Finish the current operation before entering Drawing":"יש לסיים את הפעולה הנוכחית לפני המעבר לסביבת השרטוט",
"Manage sheets and docking":"ניהול גיליונות ועיגון",
"NOTES":"הערות",
"No drawing document":"אין מסמך שרטוט",
"No drawing geometry available":"אין גאומטריית שרטוט זמינה",
"No drawing objects":"אין עצמים בשרטוט",
"No drawing sheet is available yet.":"אין עדיין גיליון שרטוט זמין.",
"No drawing view available":"אין מבט שרטוט זמין",
"No sheet available":"אין גיליון זמין",
"Not available":"לא זמין",
"Not configured":"לא מוגדר",
"Not implemented. These options describe the planned interface; no drawing data is changed.":"טרם מומש. האפשרויות האלה מתארות את הממשק המתוכנן; נתוני השרטוט אינם משתנים.",
"Note":"הערה",
"Paper & layout":"נייר ופריסה",
"Paper colour":"צבע הנייר",
"Paper colour will be available when the sheet renderer is implemented.":"צבע הנייר יהיה זמין כאשר ימומש מנגנון רינדור הגיליון.",
"Parent view":"מבט אב",
"Parts & Tables":"חלקים וטבלאות",
"Parts List":"רשימת חלקים",
"Print":"הדפסה",
"Print Drawing":"הדפסת שרטוט",
"Profile & units":"פרופיל ויחידות",
"Projected View":"מבט מוטל",
"RESOURCES":"משאבים",
"REVISION":"גרסה",
"References":"הפניות",
"Resources":"משאבים",
"Return to Design to use this command.":"יש לחזור לסביבת התכן כדי להשתמש בפקודה זו.",
"Review":"סקירה",
"Revision Table":"טבלת גרסאות",
"Revision Tbl.":"טבלת גרסאות",
"SOURCE":"מקור",
"SYMBOLS":"סמלים",
"Screen appearance":"מראה המסך",
"Section View":"מבט חתך",
"Sheet Settings":"הגדרות גיליון",
"Sheet unavailable":"הגיליון אינו זמין",
"Size & orientation":"גודל וכיוון",
"Source BOM":"רשימת חלקים של המקור",
"Source Flat":"פריסת המקור",
"Source design":"תכן המקור",
"Source properties":"מאפייני המקור",
"Space":"ריווח",
"Space Annotations":"ריווח הערות הסבר",
"Space Notes":"ריווח הערות",
"TABLES":"טבלאות",
"Text style":"סגנון טקסט",
"Title":"כותרת",
"Title block":"טבלת כותרת",
"UI preview":"תצוגה מקדימה של הממשק",
"Units & precision":"יחידות ודיוק",
"Update Drawing":"עדכון שרטוט",
"View":"מבט"
},
"vi":{
"0 objects":"0 đối tượng",
"ASSEMBLY":"CỤM LẮP RÁP",
"Add Drawing Revision":"Thêm lần sửa đổi bản vẽ",
"Add Revision":"Thêm lần sửa đổi",
"Add sheet (not implemented)":"Thêm tờ (chưa triển khai)",
"Align Annotations":"Căn chỉnh chú thích",
"Align Notes":"Căn chỉnh ghi chú",
"Align Views":"Căn chỉnh hình chiếu",
"Annotate":"Thêm chú thích",
"Apply (not implemented)":"Áp dụng (chưa triển khai)",
"Auto (Theme)":"Tự động (theo giao diện)",
"Balloon":"Bóng chú thích",
"Base View":"Hình chiếu cơ sở",
"Border & template":"Khung viền và mẫu",
"Browse Drawing Resources":"Duyệt tài nguyên bản vẽ",
"CHANGES":"THAY ĐỔI",
"Centre Mark":"Dấu tâm",
"Check Drawing":"Kiểm tra bản vẽ",
"Close drawing panel":"Đóng bảng bản vẽ",
"DRAWING ANNOTATE":"CHÚ THÍCH BẢN VẼ",
"DRAWING LAYOUT":"BỐ CỤC BẢN VẼ",
"DRAWING PARTS":"CHI TIẾT BẢN VẼ",
"DRAWING REVIEW":"RÀ SOÁT BẢN VẼ",
"Datum":"Chuẩn",
"Detail View":"Hình chiếu chi tiết",
"Dock at bottom":"Gắn ở cạnh dưới",
"Dock at top":"Gắn ở cạnh trên",
"Dock beneath Browser":"Gắn bên dưới cây thư mục",
"Dock left":"Gắn bên trái",
"Dock right":"Gắn bên phải",
"Dock sheet tabs":"Gắn các thẻ tờ bản vẽ",
"Drafting defaults":"Thiết lập mặc định khi lập bản vẽ",
"Drawing & title block":"Bản vẽ và khung tên",
"Drawing Dimension":"Kích thước bản vẽ",
"Drawing Line":"Đường bản vẽ",
"Drawing Note":"Ghi chú bản vẽ",
"Drawing defaults (not available)":"Thiết lập mặc định của bản vẽ (chưa có)",
"Drawing files and the paper renderer are not implemented. Planned tools show their options where defined.":"Tệp bản vẽ và bộ kết xuất trang giấy chưa được triển khai. Các công cụ dự kiến hiển thị tùy chọn ở những nơi đã được định nghĩa.",
"Drawing workspace":"Không gian làm việc bản vẽ",
"Edit Drawing Table":"Sửa bảng trong bản vẽ",
"Edit Table":"Sửa bảng",
"Export Drawing":"Xuất bản vẽ",
"Finish the current operation before entering Drawing":"Hoàn tất thao tác hiện tại trước khi chuyển sang không gian Bản vẽ",
"Hole Callout":"Chú dẫn lỗ",
"Image":"Hình ảnh",
"Inherited from parent view":"Kế thừa từ hình chiếu gốc",
"Insert Image":"Chèn hình ảnh",
"Leader":"Đường dẫn chú thích",
"Leader Note":"Ghi chú có đường dẫn",
"Manage sheets (not implemented)":"Quản lý tờ (chưa triển khai)",
"Manage sheets and docking":"Quản lý tờ và vị trí gắn",
"NOTES":"GHI CHÚ",
"No drawing document":"Không có tài liệu bản vẽ",
"No drawing geometry available":"Chưa có hình học bản vẽ",
"No drawing objects":"Không có đối tượng bản vẽ",
"No drawing sheet is available yet.":"Chưa có tờ bản vẽ nào.",
"No drawing view available":"Chưa có hình chiếu bản vẽ",
"No sheet available":"Chưa có tờ",
"Not available":"Chưa có",
"Not configured":"Chưa cấu hình",
"Not implemented. These options describe the planned interface; no drawing data is changed.":"Chưa triển khai. Các tùy chọn này mô tả giao diện dự kiến; không có dữ liệu bản vẽ nào bị thay đổi.",
"Note":"Ghi chú",
"Paper & layout":"Giấy và bố cục",
"Paper colour":"Màu giấy",
"Paper colour will be available when the sheet renderer is implemented.":"Có thể chọn màu giấy khi bộ kết xuất tờ bản vẽ được triển khai.",
"Parent view":"Hình chiếu gốc",
"Parts & Tables":"Chi tiết và bảng",
"Parts List":"Danh sách chi tiết",
"Print":"In",
"Print Drawing":"In bản vẽ",
"Profile & units":"Hồ sơ và đơn vị",
"Projected View":"Hình chiếu dẫn xuất",
"RESOURCES":"TÀI NGUYÊN",
"REVISION":"LẦN SỬA ĐỔI",
"References":"Tham chiếu",
"Resources":"Tài nguyên",
"Return to Design to use this command.":"Quay lại không gian Thiết kế để dùng lệnh này.",
"Drawing files and the paper renderer are not implemented. Planned tools show their options where defined.":"Drawing files and the paper renderer are not implemented. Planned tools show their options where defined.",
"Drawing workspace":"Drawing workspace",
"Edit Drawing Table":"Edit Drawing Table",
"Edit Table":"Edit Table",
"Export Drawing":"Export Drawing",
"Finish the current operation before entering Drawing":"Finish the current operation before entering Drawing",
"Hole Callout":"Hole Callout",
"Image":"Image",
"Inherited from parent view":"Inherited from parent view",
"Manage sheets and docking":"Manage sheets and docking",
"NOTES":"NOTES",
"No drawing document":"No drawing document",
"No drawing geometry available":"No drawing geometry available",
"No drawing objects":"No drawing objects",
"No drawing sheet is available yet.":"No drawing sheet is available yet.",
"No drawing view available":"No drawing view available",
"No sheet available":"No sheet available",
"Not available":"Not available",
"Not configured":"Not configured",
"Not implemented. These options describe the planned interface; no drawing data is changed.":"Not implemented. These options describe the planned interface; no drawing data is changed.",
"Note":"Note",
"Paper & layout":"Paper & layout",
"Paper colour":"Paper colour",
"Paper colour will be available when the sheet renderer is implemented.":"Paper colour will be available when the sheet renderer is implemented.",
"Parent view":"Parent view",
"Parts & Tables":"Parts & Tables",
"Parts List":"Parts List",
"Print":"Print",
"Print Drawing":"Print Drawing",
"Profile & units":"Profile & units",
"Projected View":"Projected View",
"RESOURCES":"RESOURCES",
"REVISION":"REVISION",
"References":"References",
"Resources":"Resources",
"Return to Design to use this command.":"Return to Design to use this command.",
"Drawing files and the paper renderer are not implemented. Planned tools show their options where defined.":"図面ファイルと用紙レンダラーは未実装です。予定されているツールでは、設定項目が定義済みの場合に表示されます。",
"Drawing workspace":"図面ワークスペース",
"Edit Drawing Table":"図面の表を編集",
"Edit Table":"表を編集",
"Export Drawing":"図面をエクスポート",
"Finish the current operation before entering Drawing":"現在の操作を完了してから図面ワークスペースに移動してください",
"Hole Callout":"穴注記",
"Image":"画像",
"Inherited from parent view":"親ビューから継承",
"Insert Image":"画像を挿入",
"Leader":"引出線",
"Leader Note":"引出線付き注記",
"Manage sheets (not implemented)":"シートを管理(未実装)",
"Manage sheets and docking":"シートとドッキングを管理",
"NOTES":"注記",
"No drawing document":"図面ドキュメントがありません",
"No drawing geometry available":"利用可能な図面ジオメトリがありません",
"No drawing objects":"図面オブジェクトがありません",
"No drawing sheet is available yet.":"図面シートはまだ利用できません。",
"No drawing view available":"利用可能な図面ビューがありません",
"No sheet available":"利用可能なシートがありません",
"Not available":"利用不可",
"Not configured":"未設定",
"Not implemented. These options describe the planned interface; no drawing data is changed.":"未実装です。これらの設定項目は予定されているインターフェースを示すもので、図面データは変更されません。",
"Note":"注記",
"Paper & layout":"用紙とレイアウト",
"Paper colour":"用紙の色",
"Paper colour will be available when the sheet renderer is implemented.":"用紙の色はシートレンダラーの実装後に設定できます。",
"Parent view":"親ビュー",
"Parts & Tables":"部品と表",
"Parts List":"部品表",
"Print":"印刷",
"Print Drawing":"図面を印刷",
"Profile & units":"製図規格と単位",
"Projected View":"投影ビュー",
"RESOURCES":"リソース",
"REVISION":"改訂",
"References":"参照",
"Resources":"リソース",
"Return to Design to use this command.":"このコマンドを使用するには、デザインワークスペースに戻ってください。",
"Review":"確認",
"Revision Table":"改訂表",
"Revision Tbl.":"改訂表",
"SOURCE":"参照元",
"SYMBOLS":"記号",
"Screen appearance":"画面表示",
"Section View":"断面ビュー",
"Sheet Settings":"シート設定",
"Sheet unavailable":"シートを利用できません",
"Size & orientation":"サイズと向き",
"Source BOM":"参照元の部品表",
"Source Flat":"参照元の展開図",
"Source design":"参照元のデザイン",
"Source properties":"参照元のプロパティ",
"Space":"等間隔",
"Space Annotations":"注釈を等間隔に",
"Space Notes":"注記を等間隔に",
"TABLES":"表",
"Text style":"文字スタイル",
"Title":"タイトル",
"Title block":"表題欄",
"UI preview":"UIプレビュー",
"Units & precision":"単位と精度",
"Update Drawing":"図面を更新",
"View":"ビュー"
},
"zh":{
"0 objects":"0 个对象",
"ASSEMBLY":"装配",
"Add Drawing Revision":"添加图纸修订",
"Add Revision":"添加修订",
"Add sheet (not implemented)":"添加图纸页(未实现)",
"Align Annotations":"对齐标注",
"Align Notes":"对齐注释",
"Align Views":"对齐视图",
"Annotate":"标注",
"Apply (not implemented)":"应用(未实现)",
"Auto (Theme)":"自动(跟随主题)",
"Balloon":"零件序号",
"Base View":"基本视图",
"Border & template":"图框与模板",
"Browse Drawing Resources":"浏览制图资源",
"CHANGES":"更改",
"Centre Mark":"中心标记",
"Check Drawing":"检查图纸",
"Close drawing panel":"关闭制图面板",
"DRAWING ANNOTATE":"图纸标注",
"DRAWING LAYOUT":"图纸布局",
"DRAWING PARTS":"图纸零件",
"DRAWING REVIEW":"图纸审阅",
"Datum":"基准",
"Detail View":"局部放大视图",
"Dock at bottom":"停靠到底部",
"Dock at top":"停靠到顶部",
"Dock beneath Browser":"停靠到浏览器下方",
"Dock left":"停靠到左侧",
"Dock right":"停靠到右侧",
"Dock sheet tabs":"停靠图纸页选项卡",
"Drafting defaults":"制图默认设置",
"Drawing & title block":"图纸与标题栏",
"Drawing Dimension":"图纸尺寸标注",
"Drawing Line":"图纸线段",
"Drawing Note":"图纸注释",
"Drawing defaults (not available)":"图纸默认设置(不可用)",
"Drawing files and the paper renderer are not implemented. Planned tools show their options where defined.":"图纸文件和纸张渲染器尚未实现。规划中的工具会显示已定义的选项。",
"Drawing workspace":"工程图工作区",
"Edit Drawing Table":"编辑图纸表格",
"Edit Table":"编辑表格",
"Export Drawing":"导出图纸",
"Finish the current operation before entering Drawing":"请先完成当前操作,再进入工程图工作区",
"Hole Callout":"孔标注",
"Image":"图像",
"Inherited from parent view":"继承父视图",
"Insert Image":"插入图像",
"Leader":"引线",
"Leader Note":"引线注释",
"Manage sheets (not implemented)":"管理图纸页(未实现)",
"Manage sheets and docking":"管理图纸页与停靠位置",
"NOTES":"注释",
"No drawing document":"没有图纸文档",
"No drawing geometry available":"没有可用的图纸几何",
"No drawing objects":"没有图纸对象",
"No drawing sheet is available yet.":"目前尚无可用的图纸页。",
"No drawing view available":"没有可用的图纸视图",
"No sheet available":"没有可用的图纸页",
"Not available":"不可用",
"Not configured":"未配置",
"Not implemented. These options describe the planned interface; no drawing data is changed.":"尚未实现。这些选项展示规划中的界面,不会更改任何图纸数据。",
"Note":"注释",
"Paper & layout":"纸张与布局",
"Paper colour":"纸张颜色",
"Paper colour will be available when the sheet renderer is implemented.":"图纸页渲染器实现后即可设置纸张颜色。",
"Parent view":"父视图",
"Parts & Tables":"零件与表格",
"Parts List":"零件明细表",
"Print":"打印",
"Print Drawing":"打印图纸",
"Profile & units":"制图规范与单位",
"Projected View":"投影视图",
"RESOURCES":"资源",
"REVISION":"修订",
"References":"引用",
"Resources":"资源",
"Return to Design to use this command.":"请返回设计工作区使用此命令。",
"Drawing files and the paper renderer are not implemented. Planned tools show their options where defined.":"Zeichnungsdateien und die Papierdarstellung sind noch nicht implementiert. Geplante Werkzeuge zeigen ihre Optionen an, sofern diese bereits festgelegt sind.",
"Finish the current operation before entering Drawing":"Beenden Sie den aktuellen Vorgang, bevor Sie zum Zeichnungsarbeitsbereich wechseln",
"Hole Callout":"Bohrungsangabe",
"Image":"Bild",
"Inherited from parent view":"Von der übergeordneten Ansicht übernommen",
"Insert Image":"Bild einfügen",
"Leader":"Bezugslinie",
"Leader Note":"Hinweis mit Bezugslinie",
"Manage sheets (not implemented)":"Blätter verwalten (nicht implementiert)",
"Manage sheets and docking":"Blätter und Andockpositionen verwalten",
"NOTES":"HINWEISE",
"No drawing document":"Kein Zeichnungsdokument",
"No drawing geometry available":"Keine Zeichnungsgeometrie verfügbar",
"No drawing objects":"Keine Zeichnungsobjekte",
"No drawing sheet is available yet.":"Es ist noch kein Zeichnungsblatt verfügbar.",
"No drawing view available":"Keine Zeichnungsansicht verfügbar",
"No sheet available":"Kein Blatt verfügbar",
"Not available":"Nicht verfügbar",
"Not configured":"Nicht konfiguriert",
"Not implemented. These options describe the planned interface; no drawing data is changed.":"Nicht implementiert. Diese Optionen beschreiben die geplante Oberfläche; es werden keine Zeichnungsdaten geändert.",
"Note":"Hinweis",
"Paper & layout":"Papier & Layout",
"Paper colour":"Papierfarbe",
"Paper colour will be available when the sheet renderer is implemented.":"Die Papierfarbe wird verfügbar sein, sobald die Blattdarstellung implementiert ist.",
"Parent view":"Übergeordnete Ansicht",
"Parts & Tables":"Teile & Tabellen",
"Parts List":"Teileliste",
"Print":"Drucken",
"Print Drawing":"Zeichnung drucken",
"Profile & units":"Profil & Einheiten",
"Projected View":"Projizierte Ansicht",
"RESOURCES":"RESSOURCEN",
"REVISION":"REVISION",
"References":"Referenzen",
"Resources":"Ressourcen",
"Return to Design to use this command.":"Wechseln Sie zum Design-Arbeitsbereich zurück, um diesen Befehl zu verwenden.",
"Drawing defaults (not available)":"도면 기본 설정 (사용 불가)",
"Drawing files and the paper renderer are not implemented. Planned tools show their options where defined.":"도면 파일과 용지 렌더러는 아직 구현되지 않았습니다. 예정된 도구는 정의된 옵션이 있는 경우 해당 옵션을 표시합니다.",
"Drawing workspace":"도면 작업 공간",
"Edit Drawing Table":"도면 표 편집",
"Edit Table":"표 편집",
"Export Drawing":"도면 내보내기",
"Finish the current operation before entering Drawing":"도면 작업 공간으로 전환하기 전에 현재 작업을 완료하세요",
"Hole Callout":"구멍 주석",
"Image":"이미지",
"Inherited from parent view":"상위 뷰에서 상속됨",
"Insert Image":"이미지 삽입",
"Leader":"지시선",
"Leader Note":"지시선 메모",
"Manage sheets (not implemented)":"시트 관리 (미구현)",
"Manage sheets and docking":"시트 및 고정 위치 관리",
"NOTES":"메모",
"No drawing document":"도면 문서 없음",
"No drawing geometry available":"사용 가능한 도면 형상 없음",
"No drawing objects":"도면 객체 없음",
"No drawing sheet is available yet.":"아직 사용 가능한 도면 시트가 없습니다.",
"No drawing view available":"사용 가능한 도면 뷰 없음",
"No sheet available":"사용 가능한 시트 없음",
"Not available":"사용 불가",
"Not configured":"설정되지 않음",
"Not implemented. These options describe the planned interface; no drawing data is changed.":"아직 구현되지 않았습니다. 이 옵션은 예정된 인터페이스를 설명하며 도면 데이터를 변경하지 않습니다.",
"Note":"메모",
"Paper & layout":"용지 및 배치",
"Paper colour":"용지 색상",
"Paper colour will be available when the sheet renderer is implemented.":"시트 렌더러가 구현되면 용지 색상을 설정할 수 있습니다.",
"Parent view":"상위 뷰",
"Parts & Tables":"부품 및 표",
"Parts List":"부품 목록",
"Print":"인쇄",
"Print Drawing":"도면 인쇄",
"Profile & units":"프로파일 및 단위",
"Projected View":"투영 뷰",
"RESOURCES":"리소스",
"REVISION":"리비전",
"References":"참조",
"Resources":"리소스",
"Return to Design to use this command.":"이 명령을 사용하려면 디자인 작업 공간으로 돌아가세요.",
"prefs.viewport_analysis_status_help":"Show active analyses above the viewport, open their legends from each item, and use Esc to turn off the newest analysis. Turn off to restore corner legends and the previous Esc behaviour.",
"ui.viewport_status_edges":"{count} edges",
"ui.viewport_status_from":"from {direction}"
},
"ar":{
"prefs.viewport_analysis_status":"إظهار حالة التحليل في منفذ العرض",
"prefs.viewport_analysis_status_help":"عرض التحليلات النشطة أعلى منفذ العرض وفتح مفتاح كل تحليل من عنصره. ينهي Esc أحدث تحليل. أوقف الخيار لاستعادة المفاتيح في الزاوية وسلوك Esc السابق.",
"ui.viewport_status_edges":"{count} حواف",
"ui.viewport_status_from":"من {direction}"
},
"de":{
"prefs.viewport_analysis_status":"Analysestatus im Ansichtsfenster anzeigen",
"prefs.viewport_analysis_status_help":"Zeigt aktive Analysen über dem Ansichtsfenster und ihre Legenden am jeweiligen Eintrag. Esc beendet die neueste Analyse. Ausschalten stellt Ecklegenden und das bisherige Esc-Verhalten wieder her.",
"ui.viewport_status_edges":"{count} Kanten",
"ui.viewport_status_from":"aus {direction}"
},
"he":{
"prefs.viewport_analysis_status":"הצגת מצב ניתוח בחלון התצוגה",
"prefs.viewport_analysis_status_help":"הצגת ניתוחים פעילים מעל חלון התצוגה ופתיחת המקרא מכל פריט. Esc מכבה את הניתוח האחרון. כיבוי האפשרות משחזר את המקרא בפינה ואת פעולת Esc הקודמת.",
"prefs.viewport_analysis_status_help":"활성 해석을 뷰포트 위에 표시하고 각 항목에서 범례를 엽니다. Esc로 최신 해석을 끕니다. 끄면 모서리 범례와 이전 Esc 동작으로 돌아갑니다.",
"ui.viewport_status_edges":"모서리 {count}개",
"ui.viewport_status_from":"{direction} 방향에서"
},
"vi":{
"prefs.viewport_analysis_status":"Hiện trạng thái phân tích trong khung nhìn",
"prefs.viewport_analysis_status_help":"Hiện các phân tích đang bật phía trên khung nhìn và mở chú giải từ từng mục. Esc tắt phân tích mới nhất. Tắt tùy chọn để khôi phục chú giải ở góc và hành vi Esc trước đây.",
"ui.part_size_needs_a_part":"Zaznacz wstawioną część znormalizowaną (śrubę, nakrętkę, podkładkę lub kołek), aby zmienić jej rozmiar. Aby powiększyć lub pomniejszyć bryłę, użyj polecenia Skala.",
"ui.part_size_shared":"Przebudowuje część, zastępując wszystkie dodane elementy. Wszystkie wystąpienia tego komponentu zostaną zaktualizowane.",
"cmd.sketch.construction_line.vertical":"Reta de construção vertical",
"cmd.sketch_arc_center_point":"Arco por ponto central",
"cmd.sketch_arc_tangent":"Arco tangente",
"cmd.sketch_arc_three_point":"Arco por 3 pontos",
@@ -1864,6 +1874,7 @@
"ui.constant":"Constante",
"ui.constraints_conflict":"Restrições em conflito",
"ui.construction":"Construção",
"ui.construction.ribbon_label":"Linhas aux.",
"ui.context_menu":"Menu de contexto",
"ui.continuity_g0":"G0 vinco",
"ui.continuity_g1":"G1 tangente",
@@ -2161,6 +2172,7 @@
"ui.pan_zoom_orbit":"Deslocar, zoom, orbitar",
"ui.parallel":"Paralelo",
"ui.parameters":"Parâmetros",
"ui.part_size_needs_a_part":"Selecione uma peça padrão inserida (um parafuso, uma porca, uma arruela ou um pino) para alterar o tamanho. Para aumentar ou diminuir um corpo, use Escala.",
"ui.part_size_shared":"Reconstrói a peça, substituindo quaisquer recursos adicionados. Todas as ocorrências deste componente são atualizadas.",
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.