* claim: #5849
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G1b41qKhyhmZoiAFYt9YwX
* Context menu: a right-click on a face of a selected body keeps the selection (#5849)
A right-click selects what is under the cursor first, so the menu is about it. The check
for "already selected" compared the pick with each selected item by identity, so a face
of a selected body never matched: right-clicking a face of a selected body replaced the
whole selection with that one face, and the face menu opened instead of the body menu.
A face, edge or vertex of a selected body now counts as part of the selection, whether the
body is selected on its own or through an occurrence. The right-click then leaves the
selection alone, as it already did for a face in a multi-selection of faces (#3733).
selection::same_item is unchanged: dialogs and Select Other compare picks with it, and
there a face of a selected body is a different pick.
Fixes#5849.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G1b41qKhyhmZoiAFYt9YwX
* Pass the held keys in the selected-body context menu test
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>
* Move: placements of one part moved together each go the way asked (#4768)
A part placed more than once (an imported assembly's corners, screws or
fan blades) is one body in its component. Moving several of its
placements together moved that body once per placement listed, each
placement turning the move its own way, so the copies flew apart.
When solid.move names one part through two or more placements, it now
moves those placements in the world, as occurrence.move does with space
world: a placement held by a rigid group (an imported assembly) moves with
the outermost placement holding it, one inside another being moved isn't
moved again, and root-component bodies listed are moved as bodies, all in
one undo step. Other moves are unchanged, and so is a move with copy.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Free the grounded first part before the placement-move tests move it
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>
* Test: a text's words follow its text parameters, and so does its cut
A sketch text whose words are an expression of text parameters (`Label + '-L'`): a parameter edit
and a configuration row that sets the parameter relabel the text, the text's cut follows, and the
expression is saved with the design.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsiqyXWBFGTCX9J51bhysc
* Text parameters: a sketch text's words built from them follow every change
A design's parameters are numbers; a text's words (a part label, a size code) could only be typed.
Text parameters hold words: `parameters.add` or `parameters.change` with `unit: "text"` (the
expression is the text, quotes optional), and `sketch.text` and `sketch.edit_text` take
`text_expression` (quoted texts and text parameters joined by `+`, `Label + '-L'`) instead of
`text`. The text is laid out anew from its expression on every evaluation, so a parameter edit,
a configuration row or a script relabels it and the features built on it (a cut, an emboss)
follow in the same rebuild. The design file keeps the text parameters and each text's expression
(one pending format change); a configuration table's parameter column can be a text parameter;
the Parameters dialog lists them under the unit `text`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsiqyXWBFGTCX9J51bhysc
* Text parameters: the joints' same-but-features check compares the text parameters
The joints are solved once per change unless the design differs only in its features
(same_but_features, #6372), which names every Document field. The text parameters are a
field of their own and change what a text's words evaluate to, so two designs that differ
in them are not the same but for their features.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
`cargo clippy --workspace --all-targets -- -D warnings` fails on Rust 1.97: the
reflection radiance bound in crates/render/src/raytrace/image.rs (#5494) trips
`clippy::manual_range_contains` (`v < 0.0 || v > 1e12`).
It now reads `!(0.0..=1e12).contains(&v)`. The two differ only for NaN, which the
`is_finite` check in front of it already refuses, so nothing else changes.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Project: don't lay a second copy over geometry the sketch already shows
A sketch on a face already holds the face's border, linked. Projecting that face
again, or one of the border's edges, added a second set of curves and points over
the first: on a rounded corner that shows as extra points and lines. Project now
skips a reference whose curves are all linked into the sketch already (lines,
circles, arcs), reports it as `already_projected`, and refuses a pick of nothing
new so it adds no undo step. Points on their own (the origin, a vertex) still
project.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Projecting the face a sketch already shows adds no second copy in the face scenario
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: #976
* One set of X, Y and Z colours for every axis (#976)
The origin axes, the navigation cube's triad, 3D sketch triads and joint
origins drew `axis_x/y/z` (255,0,0 / 0,255,0 / 2,2,248), the view triad and
the headless picture drew `triad` (220,60,60 / 60,170,60 / 60,100,230) and
the Move gizmo drew `gizmo` (225,70,60 / 80,190,80 / 70,130,240). The
`triad` and `gizmo` tokens are gone: every axis reads `axis_x/y/z`, now the
view triad's values (the engine's `view::colors::AXIS_*`, so headless
pictures are unchanged, and the set the red-green table was measured for).
Older theme files' `triad` and `gizmo` lists still read and set the axes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #5860
* Doc: stop the names of split faces from growing with every split
* Joints: the long-piece-name test takes the longest name a piece now gets
Split-face names are bounded at 512 bytes since #5860, so the test cannot
build one over 512; it takes the longest the history gives.
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>
* Docs: refresh every UI screenshot, GIF and video from the current UI
Re-record the README demo, the rover build, the sample plate and the class-A surface pictures
with 0.22.1 in the default dark theme. The demo script is fixed for the current sketch dimension
boxes and MCP file limits, and the rover replay and class-A pictures now have scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Docs: cut the idle time out of the demo and rover recordings
The README demo plays at 6x and the rover replay at 15x with a constant-rate camera orbit, with the stretches where nothing on screen changes cut out by the new trim_idle.py.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
#5187 fixed the frozen Render preview: product frames now set ss_ready
themselves instead of keeping the flag from the last Design frame, so
paint() no longer blits that stale offscreen picture. Nothing tested it.
The new test draws a see-through Design frame (offscreen, for
order-independent transparency) and then an opaque product frame on the
same renderer, and checks the middle pixel shows the product. With the
product branch's ss_ready update removed it fails with the stale red
Design colour, [255, 129, 129, 255].
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* i18n(uk): translate 188 pending messages
Bring the Ukrainian catalog up to date with en.json after rebasing onto the latest main. Covers the crash-report form, AI preferences, preferences pages, solid tool help, free orbit and history controls. Keys stay sorted via cargo xtask lists.
* i18n(uk): translate 114 pending messages
Rebase onto the latest main and bring the Ukrainian catalog up to date with
en.json. The new messages cover the mouse-gesture editor and navigation
preset help, the frames and sheet-metal dialogs, fillet control radii, the
timeline folder actions, the motion keyframe controls, library appearance and
library file actions, and the measure dialog.
Also correct "Add Key" to "Додати ключовий кадр": the only user of that
message is the motion keyframe button, and the catalog already translates
"Keys" as keyframes.
Keys stay sorted; cargo xtask lists --check and the i18n catalog tests pass.
* Sort the Ukrainian catalogue
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>
* Home: a theme's File view follows its own colours where it sets none (#4868)
and no built-in theme sets them, so every theme kept Dark's or Light's blue rail and grey cards
under its own app bar and headings. A theme that leaves them unset now gets them from its own
palette: page and cards from its panel and dialog, the rail from its accent deepened as Dark's
and Light's are, the primary action from its soft accent, and each ink the one of its own colours
that reads best there. Dark and Light themselves are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Leave a theme that sets no colours as its base palette
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>
* i18n(da): complete upstream catalog and align CAD help terminology
Add 371 missing Danish translations, refresh changed source messages,
and independently rewrite six help descriptions from SolidCraft sources.
Preserve all 2397 keys, canonical ordering and placeholders.
* Sort the Danish catalogue
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>
* test: cover malformed and unbounded recovery metadata
Add regression tests for oversized JSON, mismatched entry IDs, and
non-regular recovery metadata. These expose failures in the baseline
recovery reader before the implementation fix.
Signed-off-by: Taha Zarif <tahazarif380@gmail.com>
* fix: bound recovery metadata reads and validate entry identity
Reject oversized and non-regular recovery metadata before reading it,
and bound the read if a file grows after inspection. Require each JSON
entry ID to match its filename for orphan listing and recovery loading,
preventing unrelated save paths from being attached to a recovered design.
Signed-off-by: Taha Zarif <tahazarif380@gmail.com>
* Format recovery.rs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Signed-off-by: Taha Zarif <tahazarif380@gmail.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #6226
* Offset Face: split a face off its seam and try again, as booleans do (#6226)
A hole's wall arrives from a STEP file as one whole turn bounded by one wire that
runs along its seam twice, where SolidCraft builds the same wall as two halves.
Offset Face heals the body before it moves anything, and healing such a face
fails inside truck with `This wire is not simple`, so the panic guard turned the
whole operation into a kernel internal error. Since importing is how a part with
a hole usually arrives, Offset Face did not work on imported parts with holes at
all.
`seam_faces` already answers this for booleans: split every such face in two
along a plane through its axis, run the operation, and compose the history back
over the body as it was given. This gives the offset path the same retry.
- `seam_faces::one_body` is `boolean`'s answer for an operation that takes one
body, with `needed_one` beside `needed`.
- `offset_faces` now does its healing, its face picking and its offset inside one
closure, so the retry runs the whole of it on the split body; the first attempt
is guarded, so the failure is an error the retry can act on rather than an
unwind. The faces are picked per body, because the pieces are numbered
differently once a face is split — the points do not move, so they pick the
same surface either way.
The regression test is in `seam_faces_tests.rs`, beside the boolean ones: a
20 x 20 x 10 block with a 6 mm hole, read back from STEP, whose top moves 2 mm
and lands on the same volume as moving the built body's top, with its history
intact.
Checked against `main` before and after: the three kernel tests that fail here
(`blend::…seam_twice`, `step_in::…parallel_body_preparation`,
`tests::a_box_cut_round_a_small_hole…`) fail the same way without this change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: CarlosKeese <dudukeese@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* claim: #4544
* Add Norwegian Bokmål translation
* Refresh Norwegian Bokmål translations against current main
* Drop Norwegian messages whose ids are no longer in the English catalogue
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: #5103
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Redefine Sketch Plane names the plane a sketch is on now (#5103)
For a sketch with no body-face attachment the panel only said so. It now shows
a Current plane row: the origin plane, a construction plane's name, or the
plane it is offset from, turned from or flipped from. English, Japanese and
Chinese messages added.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drop needless to_string calls in the plane name format
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>
* Themes: Okibi (dark) and Akari (light), from the Okibi design system
Two built-in themes mapped from the Okibi design system v1.8
(https://kaievns.github.io/okibi-akiri-design-system-spec/): cool charcoal
ash with a single ember accent, and its warm-paper light twin. Every one of
the 156 colours is set, so nothing falls back to the Dark or Light palette.
* Give the Okibi and Akari themes an icon palette
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: #6392
* perf: apply a joint solve with indexed joints and occurrences
Every change to a design re-solves its joints and applies the solution. Applying it searched
the joints for each value and, for each occurrence the solve places, searched the occurrences
for it and walked its parents' first occurrences by searching again.
Joints and occurrences are now indexed once, keeping the first of each id as the searches
found it, and the parents' transform is read through the index as the loop leaves it
(component_transform shares the walk, transform_through).
Synthetic design, headless (2000 components at the root with one component inside each, 4000
joints, a solution placing every occurrence): apply 14.1 -> 2.2 ms (median of 12).
The new test applies a solution that moves a component and the components inside it, and
checks each lands where the solution put it, read through its parent as the loop has just
moved it.
* claim: #6394
* perf: group bodies, features and occurrences once in component.list
component.list searched every body, every feature (three times) and every occurrence (twice)
for each component, so its cost grew with components times contents. Bodies, features,
occurrences and children are now grouped by component once, keeping their order, and each
component reads its own groups. The output is unchanged.
The new test lists nested components and checks each gets only its own bodies, features,
sketches, occurrences and children, in timeline order.
* Dialog curve picks keep their sketch (#3726)
* Format
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Format
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Adapt context-menu test to Sel::SketchCurve's sketch field
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Path Flange picks keep their sketch too
Path Flange (#4096) landed after this PR and still looked its path up in the newest sketch with those ids.
---------
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Home: stop a recent card spinning forever when its picture can't be made (#4655)
A recent design whose thumbnail job returned no picture (empty or unreadable design) kept the loading placeholder, which draws a spinner, with no retry. Record such keys as a terminal 'failed' state and draw a box placeholder instead; opening or saving the design clears it and tries again. Sample previews are untouched: a failed sample build still keeps its shipped picture.
* Format the gallery sample row
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 row itself showed the key the command runs from (#1810), but its help
tooltip still named the default one.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
With G bound to the centre rectangle, G, Escape, R started the centre
rectangle again, because R's family remembered the variant used last
(#4308). A remembered variant that has a key of its own no longer takes
over the family's key; one without a key still comes back on it.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
When an agent's models come from several providers (Hermes and OpenCode
name each one "Provider · model" or say "Provider: ..." in its
description), the chat header shows a provider drop-down before the model
drop-down, and the model drop-down lists only that provider's models
without the provider in front. It opens on the provider of the model in
use; picking a provider alone changes nothing. ui.agent_chat takes
provider and reports providers and provider.
* i18n(pt-BR): translate 319 missing messages
Catch up with en.json: the new Preferences pages, assistant settings,
navigation preset help, the bug report form, history names, feature
folders, component links and copies, joint keys and the reference
geometry setting. Messages added to en.json after this branch started
stay in English for now.
* i18n(pt-BR): translate Delete as Apagar, not Excluir
"Excluir" (Latin excludere, to shut out) means to leave out or exclude;
"apagar" is the Portuguese for deleting, wiping something out. Use
apagar for every Delete already in the catalog (22 messages: commands,
the delete confirmation, rules, rows, sets, keys, crash reports), with
its forms: apaga, apagados, apagado(s), apagá-lo. The Delete key's name
stays as it is printed on the keyboard.
* Revert "i18n(pt-BR): translate Delete as Apagar, not Excluir"
This reverts commit 3bc73c5930.
Keep the catalog's existing word for Delete until the term is agreed
for the whole catalog in its own issue; the change can follow as a
separate PR.
* i18n(pt-BR): use Excluir in the five new Delete messages
Apply the review's suggestions: the new messages keep the catalog's
existing word for Delete.
* claim: #3983
* Agent skill for reproducible bug reports (#3983)
skills/solidcraft-reporting has an agent gather the version, steps and
exact error, reproduce the problem in a fresh design (a command script or
one batch with rollback off), shrink it while it still fails, check the
numbers with measure and a screenshot, keep private designs out, search
for duplicates, and write the report in one shape: Reproduced on, Steps
with a minimal script, Expected, Actual, Attachments.
Listed in skills/README.md and docs/mcp.md; the bug template's steps
field points agents to it. The skill-name check now covers its files.
* Reporting skill: GitHub takes a design only as a .zip
* Show scroll arrows on the theme row when more themes are hidden
* Fade the theme row edges and use the chevron icon for its scroll arrows
* Remove the outline left on the theme row arrows after a click
* Trim the theme row scroll arrow tests
* Drop the earlier theme row arrows' test and labels
* claim: #6389
* perf: write each control reply to the socket in one piece
The control server wrote each reply with writeln!(out, "{reply}") on the unbuffered TcpStream.
Formatting a serde_json::Value hands the writer every string, number and punctuation mark
separately, so each became its own write system call. A reply of a few megabytes (component.list
on a design with thousands of components) took about half a second to send.
Every reply is now serialized to one buffer and written at once. Writing one large Value to a
loopback TcpStream: 1.79 MB took 1,052 ms with writeln! and 4.9 ms with one write_all.
The test counts the write calls for a 2000-component reply (one) and reads it back.
In an assembly the bottom timeline listed every feature of every component,
unreadable with a few dozen parts. While a component is active (or one is
picked in the browser), it shows only that component's features and those of
the components inside it. A toggle ahead of the features names the component
and switches to the whole design and back. The marker and drops map from the
shown features to the design's positions.
Tests: the_timeline_shows_only_the_active_components_features,
a_slot_among_the_shown_features_is_a_design_position.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #4323
* Gruvbox Dark and Dark Soft: readable selected tab labels (#4323)
The Model tab and the shared tabs (Assemble, Insert, Inspect, Manage,
View) draw their selected label in selected_ink on accent_soft. Both
Gruvbox dark variants inherited the default dark selected_ink #9cc7f5,
which reaches only 4.48:1 on #48544f (Gruvbox Dark) and 4.13:1 on
Set selected_ink to the Gruvbox foreground #ebdbb2 in both files:
5.76:1 and 5.31:1. Tabs with their own accent keep their own ink.
A test checks the selected label of every workspace and contextual tab
against its soft background in Dark, Light and every built-in theme
file.
Every STL, OBJ and 3MF export meshed each body with a chord tolerance of
2e-4 of its size, with no way to ask for finer: a 3 mm hole in a 200 mm
part came out with about 14 sides. file.export and file.save_mesh take
`quality`: standard (as before, the default), fine (5e-5) or finest
(1e-5), and Save As Mesh has a Quality row, remembered like its format.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #5024
* Hole: refuse a position that isn't on a face (#5024)
`solid.hole` took any `position` and drilled from the nearest face, so a
point 1 mm past a box's side was accepted and cut a hole across the edge.
A position given as a point (numbers or length expressions) must now lie on
a face of a body, within the same tolerance the plastic features use for
their placement (the check is shared with them). Holes at sketch points are
unchanged.
* Hole: a position over the mouth of a bore or opening of a planar face is on that face (#5024)
A counterbore is centred on an existing bore, in the plane of the face round it.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* claim: #5329
* Update Links: show which linked design it reads, off the window thread (#5329)
Update Links read and rebuilt every linked design on the window thread, so the
window froze, with nothing on screen, until it finished. From the toolbar it now
runs as a file operation: off the window thread, with the overlay naming the
linked design being read (cover (2 of 3)), its rebuild step and progress, and
Cancel. The assembly stays as it was until the update is in, and it lands as one
undo step. The command reports each design through doc::progress::item.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The cap pass redraws every back face behind a marked pixel with depth test
Always to put the section plane's depth there, and it also wrote colour.
The last back face drawn painted the cut, so a body behind the cut (one
inside another's hollow, an assembly's inner parts) showed through it in
its own colour and outline, like an X-ray. The pass now writes depth only;
the marking passes have already drawn the nearest back face's colour (the
same split as #4622).
Test: a_body_behind_the_cut_does_not_colour_the_cap in
crates/gpu/tests/capture.rs (a red body in a green box's hollow, 4 mm
behind the cut: the picture is the same with and without it, hatched and
plain, orthographic and perspective). On main 472 pixels differ.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The viewport's picker took the nearest face along the ray whatever the
section removed. Over a cut it hovered the cut-away face in front of the
plane (the outer wall of a sectioned hub, say); the shader clipped that
face's tint, but its outline, drawn dimmed where the model hides it, ran
behind the cap and showed through it. While orbiting with the pointer on
the model the hovered face changed from frame to frame, so lines and
patches of the far side came and went over the hatch.
The picker now does what view.pick already does: faces, edges and corners
on the removed side are skipped (the ray walks past them), and where the
plane crosses a body nearer than any kept face, the cap hides the faces,
edges and corners behind it and nothing is hovered there. The walk that
tells whether the plane is inside a body moves into
view_pick::body_cut_at so both pickers share it.
Test: view_pick_tests::the_mouse_picker_stops_at_a_section_cap (a box
with a pocket, cut through the pocket: over the cut material nothing is
picked, over the pocket its floor, and with the section cleared the top
face). It fails on main, which picks the removed top face.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #5938
* Edge flange: height measured from the outer sharp, the inner sharp or the bend tangent (#5938)
An edge flange feature takes height_from (outer, inner or tangent) in sheet.flange and Height From in the Flange dialog, saved only when it is not the outer sharp. A height shorter than the bend takes is refused with the smallest height that fits.
* Edge flange: material outside and tangent bend positions (#5941)
An edge flange's position can now be material_outside (the inner virtual sharp on the edge) or tangent (the bend's outer face tangent to the edge's face), in sheet.flange and the Flange dialog. The bend starting at the edge was already the outside position, which now also answers to bend_outside.
* claim: #6252
* Open hosted designs in the web app and add an embed layout
Fixes#6252.
Fetch HTTPS design links with bounded reads and open them as unsaved copies.
Keep model panels and joint controls in embeds without browser persistence,
application chrome, or unload prompts. Add a full-app link and iframe docs.
Cover query parsing, hostile URLs, size limits, and the compact layout.
* Translate the linked-design messages into Japanese and Chinese
* claim: #4167
* Viewport: ambient occlusion no longer shows ghost copies of the model (#4167)
The AO shader used the same 32 sample offsets at every pixel, and nothing smoothed the
result. On a flat face, each sample darkened its own shifted copy of a nearby part's outline.
Where the radius is large on screen (a big design seen close up), those copies show as faint
separate ghosts of the part. Where it is small, they pack into stepped bands along contacts
(#3407).
The sample pattern is now turned by a different angle at each pixel, using interleaved
gradient noise of the pixel position (Jimenez 2014). The copies break up into an even fine
grain, which 2x supersampling smooths further. The cost is per pixel only (one noise value,
one sine and one cosine). The 32-sample loop, the depth prepass and AO off are unchanged.
Test: ambient_occlusion_draws_no_copies_of_a_part (viewport.rs) renders a block on a plate
from above with AO off and on. AO must still darken beside the block, the plate must stay
clear beyond the radius, and the profile beside the block's straight side must have no
copy-sized steps (0.0144 before, 0.0026 after; limit 0.005). The test-only offscreen helper
gains an AO radius, like render_offscreen_ssaa.
Fixes#4167
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
At launch the app adds the solidcraft entry to each installed AI client off the window thread,
names newly connected clients once, and refreshes a stale path. Settings > Assistant has
"Make SolidCraft available to AI apps on this computer" (on by default); off removes the entries it
added. Entries the user changed are left alone; automation runs never touch the files.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #2926
* Edit Feature: a primitive in a moved component keeps its placement (#2926)
Edit Feature passes a primitive's placement (corner, base, axis, center)
through as saved, in its component's frame, as timeline.inspect reports it.
timeline.redefine read it as a world point and mapped it through the
component's placement, so in a turned component the edit moved the
primitive: in the Hinge Assembly, Leaf B's rod flipped and the revolute
joint put the leaf on the other side.
A placement equal to the saved one now stays as saved; a new one is still
mapped from the world.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TBKuBKHW3LfC7CMW1HYpwL
* Move the #2926 tests up in frames_tests.rs
They sat at the end of the file, where main has since added the #6000
test; moving them avoids the merge conflict.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: sascha2101 <221609842+sascha2101@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Every command in a design with placed components places their bodies
again on up to 16 threads, though nearly all of them are taken as they
were placed before (the same body by the same matrix). Those are now
taken on the calling thread and only the rest are placed on threads.
Warming the meshes (and display plans) of a state likewise starts
threads only for the bodies that have none yet.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
solidcraft-cli mcp setup (alias install) and Help > Connect AI assistants edit each installed
client's own configuration in place, with this program's absolute path: OpenCode, Claude Desktop,
Claude Code, Cursor, Codex, Gemini CLI, Copilot CLI, Windsurf, Zed and VS Code. Only the solidcraft
entries change; a stale path is refreshed and an entry the user changed is left alone.
Plain mcp keeps looking for the app while its headless session has no work, and says on every
tool result that it is headless when the app was not found.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #4094
* Sheet metal: Path Flange, a chain of bends on an existing sheet edge (#4094)
Path Flange (`sheet.path_flange`): sketch an open chain of lines across a sheet edge, pick the
edge and one line; each line becomes a flange as long as the line and each corner a bend, one
feature and one body. The edge flange takes further bends (`then`), each on the far edge of the
one before, and the Flange dialog shows them as rows with a bin each.
* Release: sign and notarize the macOS app with a Developer ID (#5749)
package.sh --sign signs the app, the CLI and the DMG with the hardened runtime and a secure
timestamp, notarizes the DMG and the CLI zip, staples the DMG and checks it with stapler and
spctl. The app carries entitlements.plist: com.apple.security.cs.disable-library-validation
lets the hardened runtime load 3Dconnexion's client framework (crates/threedx-client).
signing.sh --setup imports the certificate into a temporary keychain and writes the API key;
--cleanup removes both. The release workflow builds both architectures first (macos-build, no
secrets), then signs and packages in the join job. Without the APPLE_* secrets (forks, dry runs)
the app keeps its ad-hoc signature and the job summary says so.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* macOS release: staple the app before the DMG, retry stapler, document renewal (#5749)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* claim: #3271
* fix(ui): use document units across length fields (#3271)
* Format
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Adapt drag-extrude and joint tests to document-unit length helpers
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ui): preserve blank parameter drafts and cover bounded lengths
---------
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Selection fields: only the active one looks active
* Selection fields: a filled field keeps its tinted border where a theme fixes the control border's colour
* Select Other: Shift and Ctrl add the choice to the selection, as a click does
* Select Other: follow origin visibility rename in test
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Select Other test: click away from the lines' midpoints, which are picks of their own now
---------
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #2320
* Value fields step with the arrow keys and the mouse wheel (#2320)
Up/Down in a focused value field step a plain number by 1 in the unit it is written in
("20 mm" -> "21 mm", "2,5 mm" -> "3,5 mm"), 10 with Shift and 0.1 with Alt; the wheel over
the focused field does the same, a step a notch, instead of scrolling the panel under it.
Expressions such as `width / 2` are left as typed, and an open completion list keeps the
arrows. Every expression field gets this through `params_dialog::complete`: the value box
beside the geometry in every dialog, the dialog rows, sketch dimensions, the dimension
editor, Drive Joint and the Parameters window.
* The value box beside the geometry stays under the pointer
The box rides on the arrow's tip, so each wheel notch that stepped its value moved it, and
after a notch or two the box slid out from under the pointer and the wheel zoomed the view
instead. While the pointer is over it the box now keeps its place, and it goes back to the tip
when the pointer leaves.
* Value stepping is a preference, off by default
Preferences › General › "Step values with the arrow keys and the wheel" (`step_values`,
also through `ui.prefs`) turns on Up/Down and wheel stepping in value fields, and the value
box staying under the pointer while the wheel steps it. Off, as by default, value fields
behave as before: Up/Down move the text cursor, the wheel scrolls, and the box beside the
geometry follows its arrow.
* Value stepping test: Bend Radius is under Advanced now
* Value stepping test: the preference is on the Design page
* Value stepping test: draw the field straight into the test's frame (#2320)
The shared outer-host rule now covers tests too; the field needs no panel around it.
* claim: #4968
* Sheet metal rules: bend angles as the included angle (#4968)
A sheet metal rule's angle_display (bend, the default, or included) sets
how the DXF bend notes read each bend: a 58° bend is B1 UP 122° R1 under
an included rule. The bend table always gives included_angle_deg beside
angle_deg (CSV and JSON). Set in sheet.manage_rules and the Sheet Metal
Rules dialog, listed by sheet.rules; the flange angle stays the bend angle.
* Bend table: the included angle as the CSV's last column, so the columns before it keep their places (#4968)
A test also covers a body whose base flange names its rule, which keeps that rule's notes whichever rule is active, and a bend down.
* Sheet Metal Rules: Angle Display reads Bend or Included, so the choice fits its box (#4968)
The rule editor's row contract lists the new row and the key the dialog sends.
---------
Co-authored-by: begsaquib <saquibbeg4444@gmail.com>
* claim: #5580
* fix: follow named lid edges after parameter changes (#5580)
* fix: initialize viewport test frame tints
* Drop duplicate tint fields in gpu view tests (main already has them)
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: #5907
* Fix Manage Units to open the shared unit menu (#5907)
Open the browser's existing unit choices for interactive command starts
instead of executing document.units without its required parameter. Anchor
the menu below toolbar buttons and menu rows. Choices still run the normal
command, while programmatic calls require an explicit unit.
Cover error-free opening, unit choices, button anchoring and dismissal,
the current-unit marker, one-step undo/redo, and programmatic calls.
Fixes#5907
* Sketch Dimension: point or circle to the edge of a circle or arc (#4064)
Adds PointCircleDistance and CircleCircleDistance (edge-based, like #1863):
solver, constructive seed, drawing, command wiring, tests, UI scenarios and docs.
Saved designs gain two constraint kinds, in one pending format file.
Fixes#4064.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Sketch Dimension scenario: pick the circle at its top edge (#4064)
The bottom edge sits under the Dimension tool's status hint in the scenario window, so the second click never reached the tool.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* claim: #5439
* Agent chat: each design tab keeps its own conversation (#5439)
* Adapt chat-per-tab changes to the new Pending context and cursor tests
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>
solid.revolve already revolves a planar body face (`face`, `face_body`), but
the Revolve window's Profile input took sketch profiles only, so a face
couldn't be picked. The input now takes planar faces too, and a picked face
is sent as `face`; sketch profiles win when both are picked.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #5467
* Warn when a design is open elsewhere or changed on disk (#5467)
The desktop app keeps a lock file next to each open design
(.~<file>.lock: computer, user, version, opened and refreshed times),
refreshes it every minute and removes it on close; a lock not
refreshed for three minutes is stale and ignored. Opening a design
that another SolidCraft has open asks: Open Read-Only, Open Anyway or
Cancel. A read-only design saves only to another file.
A design remembers its file's size and modification time when it is
opened or saved. Save over a file another program or computer changed
since asks: Save a Copy (the default), Overwrite or Cancel.
Programmatic calls never ask: doc.open reports open_elsewhere and takes
read_only, and file.save over a changed file returns an error until it
is given overwrite: true. ui.documents answers both questions.
* claim: #5336
* Editing an Extrude or Revolve shows its sketch (#5336)
A finished sketch is hidden once a feature uses it, and it stayed hidden
while that feature's dialog was open for editing, so no other profiles
could be picked. The session now holds the sketch of the feature being
edited; the edit dialog sets it while it is open and clears it however
it closes, and that sketch is drawn without changing stored visibility.
* claim: #2063
* Windows OpenGL: use the system title bar so the picture lines up with clicks (#2063)
On Windows with only OpenGL to start, the window opens with the system
title bar instead of using the application bar as its title bar. The
window without OS decorations keeps its caption style while having no
frame, and some OpenGL drivers (older Intel HD/UHD) show the picture a
caption's height lower than the window, so clicks land above what they
point at. Direct3D 12 and Vulkan keep the custom title bar.
The choice is made before the window opens, from the backends wgpu
starts, so the window never changes its frame while OpenGL draws into it.
* claim: #5377
* Report a bug: title from the workspace and the reporter's words (#5377)
With no dialog open the report was always labelled "Modeling" and its
summary pre-filled as "unexpected result", so every such report arrived
titled "Modeling: unexpected result" and the fallback to the first line
of "What happened?" never applied.
Label the report from the active workspace (Part, Sketch, Surface, Sheet
Metal, Plastic, Mesh, or Render), leave the summary empty so the title
becomes "<workspace>: <first line of the description>", and keep
"operation failed" when an error was recorded. The summary field now
hints that a short title is optional.
The triage script that asks for an empty in-app report to be pasted
recognises the new titles.
New UI strings go in assets/locales/pending/<slug>.json and new ATTRIBUTION.md rows in
attribution/pending/<slug>.md. The app build, xtask lists/i18n/assets read them as part of the
catalogs, and the Generated files workflow folds and deletes them on main with
cargo xtask fold-pending.
Refs #6439
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #4031
* Triangulation: split a crossing boundary segment ourselves, never in spade's assert (#4031)
A trimmed face's boundary segment that crosses a constraint already in the
triangulation went to spade's add_constraint_and_split, which asserts
(cdt.rs:978, `!previous_region.is_empty() || from == final_vertex`) when a
split lands so near another vertex or constraint that the piece up to it is
blocked. The kernel guard caught it, but the whole operation failed and a
crash report was written; a STEP body imported empty or not at all (#4598,
also #4293, #4478, #5068, #5229).
PolyBoundary::add_constraints now adds every segment that can be added, then
splits each blocked one itself: a vertex at each crossing, inserted from the
crossed edge's own vertex, and the pieces between added where they can be. A
segment with crowded or very many crossings, or a face with more than four
blocked segments (a rim that runs back and forth across itself), leaves them
out, as truck did before #1653, so the face meshes slightly off instead of
not at all. Pinched faces (#1648) still split where they cross.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Agent chat: no per-message limit by default
The limit is optional now: an Unlimited checkbox on the AI page (checked by default), a
number field when unchecked. A saved 24, the old default, reads as unlimited. The API-key agent's
24-request and the gate's 400-request caps are gone; Stop still cancels.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Agent chat: add the new messages to the complete Japanese and Chinese catalogs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Viewport: lift selected and hovered edge highlights toward the eye
A selected edge's highlight (3 px core, 6 px halo) is a screen-space band that
was depth-tested at the edge's own depth. A face next to the edge seen at a
grazing angle is pushed back by only about a pixel of its slope, so it buried
part of the band, unevenly along the edge: the highlight showed as a thin
wedge instead of a line.
The band is now drawn like a thin cylinder around the edge: its depth is
pulled toward the eye by half its width plus a pixel, measured on screen so
it holds at every zoom. Plain model edges keep their depth (#1434 still
holds); the part of a highlighted edge the model really hides stays dimmed
through hidden_line.
Not yet compiled or tested.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Viewport: lift an edge highlight by how steeply its faces are seen
The first lift (half the band's width plus a pixel, the same on every edge)
was not tied to the faces beside the edge. Each highlight segment now carries
the normals of the two faces it borders (octahedral, packed with its width in
the line's width slot, read as u32 by vs_line): the band is pulled toward the
eye by its half width times how steeply those faces are seen, so a grazing
face no longer buries it, and an edge seen square to its faces is not lifted
at all.
Test: a_selected_edge_on_a_grazing_face_draws_whole (tilts 60 to 88 degrees,
small and large designs, orthographic and perspective; the band 5 mm behind
a face stays hidden). Capturer::rgba_highlighted draws a highlight scene as
the viewport does, for that test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #5802
* fix: repaint on a theme change and key the frame cache on the theme tints (#5802)
theme::apply asks for a repaint, so the new visuals are painted on the next pass instead of waiting for another event to schedule one. The gpu frame cache key now includes the theme's hover and selection tints, so a picture cached under the previous theme's colours is not reused.
* test(ui): settle egui's startup repaints before asserting the theme's repaint (#5802)
* test(ui): name the call site that asks for the theme's repaint (#5802)
* test(ui): apply the theme outside a pass so its repaint request stands alone (#5802)
The AppImage writes a ~/.local/bin/solidcraft-cli launcher from its desktop-integration step
(same opt-outs, never over a file it did not write). The macOS app links the CLI into
/opt/homebrew/bin or /usr/local/bin at launch when one is writable, and Help > Install
solidcraft-cli command does it with an administrator prompt otherwise. Snap and Flatpak are documented.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #5357
* A dragged command window keeps its place (#5357)
Command dialogs share one dragged offset per dock side for the session
instead of one per title, and closing a dialog no longer forgets it, so
the next command opens where the last one was left. The offset is still
kept inside the viewport, including after the window shrinks.
Choosing and swapping in a refinement rebuilds and uploads the whole model
scene. During an orbit of a large assembly that happened about ten times a
second, tens of milliseconds each. It now waits until the camera has stood
still for 200 ms.
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
A Home picture still being drawn by the detached saved-thumbnail thread
could be on the GPU as the process exited, and the process died with
SIGSEGV inside the graphics driver. capture::close stops new pictures
and waits for those in flight (the app calls it in on_exit), and
dropping SolveApp waits for the Home picture threads it started.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: post-merge Render review corrections (#5493)
* fix(render): gate bundled studio tests in isolated engine builds (#5493)
Addresses jyviko review comments on #5494: compile the two positive payload-dependent regressions only with bundled-render-library. Keep all assertions and the explicit feature-absent refusals. Reproduced 5 pass / 2 fail before the fix; isolated default, CPU without pack, and CPU with pack tests plus strict scoped Clippy and repository checks pass. No production or CI configuration changes.
* Test: a text on a path follows the path when a parameter moves it
A construction line placed by a dimension that reads a parameter, and a text laid along it: after
the parameter changes the line has moved and the text has not. It follows one evaluation late.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsiqyXWBFGTCX9J51bhysc
* Sketch text: a text on a path follows the path in the same evaluation
Problem: texts are laid out with the sketch's links, before the sketch is solved. A solve that
moves the curve a text runs along (a path placed by a parameter) left the text where the path had
been before the change, so it followed one evaluation late.
Solution: after a solve that keeps the sketch's orientation, every text that runs along a curve
of its sketch is laid out again along that curve as the solve left it; a text that no longer lays
out adds its warning.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsiqyXWBFGTCX9J51bhysc
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Test: an offset plane on a sheet flange's face follows the flange
An offset plane on the inner face of an edge flange, picked by body and index as model.faces
lists it. When the base gets wider the flange moves along x and the plane stays where the face
was picked; a sketch on the same face follows it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsiqyXWBFGTCX9J51bhysc
* Construction: a plane on a sheet's flat face follows the face
Problem: a construction plane on a flat face of a sheet body resolved the stored plane, which is
only where the face was when it was picked. When the sheet changed the plane stayed behind, while
a sketch on the same face follows it by its sheet face name.
Solution: a construction plane on a sheet's flat face resolves that face's plane as it is now, as a
sketch on the face does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsiqyXWBFGTCX9J51bhysc
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Test: a design keeps several hundred configuration rules
An assembly that switches a few hundred parts on and off, by two rules each, needs hundreds of
rules; the saved-rule budget refused more than 128.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsiqyXWBFGTCX9J51bhysc
* Configurations: a design keeps up to 2048 rules
Problem: a configuration table kept at most 128 rules. A rule drives one feature's suppression,
so an assembly that switches a few hundred parts on and off (by two rules each) could not keep
its rules. Copying rules for an independent component copy looked up each new id by scanning all
rules, which a larger table makes slow.
Solution: MAX_RULES is 2048; a copy looks its new ids up in a set of the ids taken. The docs say
the new limit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsiqyXWBFGTCX9J51bhysc
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The parts of one STEP assembly import share their face names, and each
part's body stays where the file has it, placed in its component by an
offset. A joint origin on a named face (a hole's centre, a face's
centre, an x reference by face, the end a round origin takes) looked the
name up on every body of the design and compared the pick, in the
joint's component coordinates, with each body as it lies. Another part's
face of that name could lie nearer, and the joint resolved on that
part's face: a pin jointed into one plate's hole landed where the other
plate's hole is relative to its plate.
named_face now looks only at the bodies of the joint's component, each
in its own coordinates, a face the name finds exactly before a piece of
it or a face it was merged into, then the nearest. The design-wide
lookup stays for a face whose body has gone to another component.
named_round, x_axis, pin_end and end_faces take the component, as
face_name_at and round_end_at already do.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A joint origin at a face's centre (`joint.origin {face}`, `Snap::FaceCenter`) was the face's
area centroid, holes included. A hole cut into that face, moved or suppressed therefore moved
every part held at the face's centre, though the face's outline stayed where it was: on a
200 x 100 plate with a 20 mm hole, 0.31 mm for each 20 mm the hole moves and 0.62 mm when the
hole, 40 mm off the middle, is suppressed.
On a planar face with holes the origin is now the middle of the area the face's outer boundary
encloses (`outline_centre`: the face's triangle sides that no other triangle of the face runs
back along, corners matched by place, chained into loops in the face's plane; the centroid of
the loop enclosing the most area). A named face (`NamedFace::plane`) and the planar face
nearest a point (`nearest_plane`) both use it. A face without holes, or one whose mesh gives no
clean outline, keeps its centroid as before.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A parameter change evaluates the model and solves the joints inside the
command (so that it can refuse a value that breaks a sketch), and
`Session::execute` refreshes again after the command and solves every joint a
second time, although nothing has moved since. On 40 small parts jointed
rigidly to a plate's holes (half of the joints lifted by an expression),
changing the lifts' parameter took 0.85 s on main and takes 0.45 s now;
changing one part's thickness, 1.96 s and 1.52 s (CLI, medians of 5 runs).
The session now keeps the design the joints were last solved for, as the
solve left it, and the model that solve read. A refresh that finds the same
model, and a design that differs from the solved one at most in its features,
leaves the joints as they are: the joints read features only through the
model, and a value edit stores the new solutions of the sketches it drove
after its solve, so its design is a new one by the time of the second
refresh. Placements and bounding boxes come out identical.
Test: `a_parameter_change_solves_the_joints_once` counts one solve per change
(two before), checks the jointed part still follows the hole, and that a
captured move of a jointed part is still solved back.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
An edge joint origin looked its edge and face up in the body's own coordinates with the pick in the component's,
and returned the frame in body coordinates. On a body placed by an offset (an imported part's bodies are) a pick on
the edge where it is shown found no edge ("joint origin: no edge there"), or the origin sat away from its edge by
the offset. Edge origins now take the picks into the body's coordinates through the inverse of its offset and place
the frame back by it, as named faces and round origins already do. A body without an offset is unchanged.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* CI: duplicate-PR check and a required 'Changes a default' line (Refs #6439)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PR checks: enforce the defaults line only on PRs opened from 2026-10-25 (Refs #6439)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Since #1768 the section's stencil marking draws every face with FACE_BIAS.
Where a back face seen through the cut meets a front face along an edge (a
pocket's wall and its side wall or floor, a fillet and the face it runs
into), the slope push decided which was nearer beside that edge; a front
face winning left those samples unmarked, the cap skipped them, and the
edge, drawn at its own depth, showed through the hatch. In perspective, and
while orbiting in orthographic too, edges popped in and out from frame to
frame.
A second marking pass draws the back faces only (front faces culled, depth
test <=, no depth write), pulled toward the eye as far as FACE_BIAS pushes
faces back, before the cap. The depth buffer keeps FACE_BIAS, so edges on
visible faces still draw whole (#1434).
Tests in crates/gpu/tests/depth.rs: a pocket 2 mm behind the cut, and a
filleted hub with a bore and a cross hole seen from twelve directions, both
depths, orthographic and perspective. On main the pocket shows 45 px of
edges through the cap in perspective and the hub 25 px over the views; with
this change 0 and at most 2.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #5352
* Sketch: circles and arcs honour construction (#5352)
sketch.arc.center_point, sketch.arc.three_point, sketch.circle.two_point,
and sketch.circle.three_point accepted construction: true but drew normal
geometry. They now mark the new curve as construction, like lines and
rectangles, and their parameter docs list construction?.
* claim: #5562
* Intersection curves: a point that won't project no longer panics (#5562)
A quarter disc at a box's corner revolved 360 deg about the box's edge and
cut (a hemisphere whose pole sits on that edge) panicked in truck at
intersection_curve.rs:133: dividing one of the cut's intersection edges
met a point that projects onto neither surface, and the hinted
projection's fallback called subs, which unwraps that same failed
projection. The guard turned it into a failed cut and a crash report.
Now the leader's point stands in, the division stops there, and the
failure is recorded (truck_geometry take_projection_failure). truck-shapeops'
face division checks it after dividing an edge and gives up, so the
Boolean attempt fails as quickly as the panic made it and the kernel goes
on to its other tries: carrying the approximate points on instead made one
kernel test (equal bores crossing) run for minutes. On this design the cut
still fails with an ordinary error, leaving the box whole; nothing panics.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #6000
* Sketch: explicit planes take a frame, world or component (#6000)
sketch.create and sketch.redefine take frame: "world" (the default,
as before) or "component" for an explicit {origin, x_dir, y_dir}
plane. The reply says which frame the plane was read in, with a note
when a world plane goes into a moved or turned component. frame on a
named plane or a face is refused: those are where they are.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
With Start set to From Object (or a start offset), the automatic
operation was still decided from the profile: the engine probed for
material just past the profile plane, and the dialog kept the operation
it had latched before the start changed. A profile on a plate's top,
extruded 14 mm down from the plate's bottom face, came up as Cut instead
of Join. The engine now probes from the extrude's start (and, to an
object, reaches from there), and the dialog decides the operation afresh
when the start changes.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #6145
* Windows: a stuck icacls no longer freezes SolidCraft when it writes the control token (#6145)
Writing the control token runs icacls to drop the file's inherited
permissions, and waited for it without a limit. On some computers the
new icacls process stays suspended, so connecting an agent froze the app
for good. It now waits at most 5 s; an icacls still running then is ended
and the token written anyway, into the user's own local app data folder.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Test: a plane through a construction axis follows it in a design with a component
Two offset planes, the axis where they meet and a plane through that axis. Once the design has a
component, a parameter change fails the last plane with "unknown construction `Ax`".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsiqyXWBFGTCX9J51bhysc
* Construction: the root's constructions are found once the design has a component
Problem: once a design has a component, the root's construction features are built in the model
placed in the world. That state carried each component's bodies, sketches and threads, but not
its construction planes, axes and points, so a construction built on another one failed on every
evaluation with "unknown construction".
Solution: the world state carries each component's constructions where the component is placed
(moved by its occurrence's placement, as the bodies are).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsiqyXWBFGTCX9J51bhysc
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Test: configuration rules given together are checked with all of them in place
Suppressing a box and the cut on it: the box's rule alone fails the row check (the cut has
nothing left to cut), so the two rules can't be added one at a time. Given together they pass,
in one undo step; a bad rule among them adds none.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsiqyXWBFGTCX9J51bhysc
* Configurations: config.rule takes several rules at once and checks the rows once
Problem: config.rule adds one rule and checks every saved row with it. Rules that only hold
together (suppressing a body and the cut on it) could not be added at all: the first one alone
fails the check. A design with many rules also rebuilt every row once per rule.
Solution: config.rule takes `rules: [...]`: each rule is added or replaced as with `rule`, then
the rows are checked once with all of them in place, in one undo step; a rule that fails
validation adds none. The docs describe it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsiqyXWBFGTCX9J51bhysc
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Test: joint origins take a long piece name
A face split by a cut whose edge runs in many segments is named after every face around its
piece, several hundred characters. joint.origin refused such a name, as model.faces gives it,
over 512 characters, both for a face centre and as a round end.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsiqyXWBFGTCX9J51bhysc
* Joints: an origin takes a face name as long as the face's piece name
Problem: joint.origin and joint.create took a face name (a face centre, a round end, an x
direction) only up to 512 characters. A face that a cut split is named after the faces around its
piece (`<face>#[<face>,…]`), which reaches several hundred characters when the cut's edge runs in
many segments (a slot with a zigzag edge, a cut of text), so a name model.faces gives was refused.
Solution: the face names a joint origin takes are capped at 16384 characters (MAX_FACE_NAME),
still a bound on hostile input.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsiqyXWBFGTCX9J51bhysc
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Sheet metal: test that an unfolded sheet's flat face is named after its base at every size
An unfold keeps the base in place and lays the flanges out beside it; the flat
face spanning them and the bend strips is the base's face. It was named after
the unfold whenever few of its sample points fell on the base (the bend strips
are many small triangles, the base two big ones), and that changed with the
sizes: a size change renamed every top and bottom face the refold made of it,
and joints and sketches on them lost their faces (40 of 85 joint ends of a
real part after one change). The test unfolds and refolds a base with three
flanges at two flange heights and wants the flat face and every refolded
panel face named after the base.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsiqyXWBFGTCX9J51bhysc
* Sheet metal: the panel an unfold or refold keeps in place names the face it is part of
A face a sheet feature made is named by where its sample points lie on the
input's faces: in place, else taken back through the folds. Only nine points
per face are sampled, triangle centroids picked by index. An unfolded sheet's
flat face spans the base, the flanges laid out beside it and the bend strips
between them; its bend strips are many small triangles, so most samples fell
on them. Those lie on no panel, and one of them made the fold fallback give
up for the whole face. The face was named after the unfold, and the refold
passed that name to every top and bottom face; which way it went changed with
the part's sizes.
For a sheet feature that moved panels, a face the plain vote leaves unnamed
is now looked at closer (up to 256 points): if a part of it lies in place on
the body's own face before (the panel the feature kept), that face names it.
Else the fold fallback votes over the points that can be taken back, when at
least half can, instead of none when one cannot. Other features, and faces
the plain vote names, are named as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsiqyXWBFGTCX9J51bhysc
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Test: a pattern copy's piece names match as the original's do
A copy's faces carry the instance after the name, after any piece qualifier
(`top#[left,slot:a]@F9.1`). A stored piece name of a copy whose bounding faces have changed since
finds no face of the copy at all (None at the first assertion), where the original's identical
name matches by its stem. On a real part: the joint ends on a patterned copy fall back to their
picks after a size change, the original's resolve by name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsiqyXWBFGTCX9J51bhysc
* Names: a pattern copy's piece names match as the original's do
Problem: a pattern's copy names its faces after the original's with the instance appended
after any piece qualifier (`top#[left,slot:a]@F9.1`). The piece parser expects a qualified name
to end with its qualifier, so a copy's name was its own stem: when the faces around a piece
changed, a stored name of a copy matched no face of the copy at all, where the original's
identical name still matched by its stem. Joint ends on a patterned part fell back to their picks
after a size change and flipped to the other end of their holes.
Solution: `history_names::matches` takes the pattern instances off the end of both names first
and compares the rest as before, within the same copy only (equal instances; an original's name
never matches a copy's face, nor one copy's another's).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsiqyXWBFGTCX9J51bhysc
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Test: a switch parameter alone switches the features its rules suppress
Configuration rules that switch features' suppression from a parameter (drive_suppression when
`on` = 0) ran only on config.activate: a plain parameters.change of the switch left the features
as they were, so a script had to activate a row for every change. Red until the fix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsiqyXWBFGTCX9J51bhysc
* Configurations: a parameter edit runs the suppression rules whose condition reads what changed
Problem: configuration rules ran only when a row was activated or a configuration edited. A
design that switches features on and off by a parameter (a rule that suppresses a feature when
the parameter is 0) did not follow a parameters.change of that parameter.
Solution: parameters.change (one parameter or several) runs, before the rebuild, the enabled
rules that switch a feature's suppression and whose condition reads a parameter whose value
changed (or a feature switched earlier in the same pass), in their order as activation applies
them, and lists them as `switched`. Driven parameters and restrictions still wait for a
configuration edit or activation; a rule whose condition reads nothing that changed leaves a
hand-set suppression alone; a rule that cannot be evaluated is skipped and never blocks the edit.
One undo step with the edit. docs/configurations.md says so.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsiqyXWBFGTCX9J51bhysc
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A live drag frame shares one work budget between its held solves, but
only their cluster decomposition was charged. The numerical solve (LM)
ran outside it, so in a larger sketch where the geometry can't follow
the cursor, one frame ran 64 failing held solves of about 150 ms each:
10 s per frame (68 points, 50 curves, 58 constraints, found by
randomised sketch editing). The frame budget now also counts the LM
work of its held solves, without stopping a solve: once a frame's share
is spent, the drag takes no more steps in that frame, keeps what it
reached and asks for the next frame (retry), as the viewport already
handles. The same drag now takes about 0.3 s per frame. Solves outside
live frames are unchanged.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Placing a design's bodies asked each body's owning component once per
placement, and each answer searched the features for the one that made
the body: placements x bodies x features on every command, twice (the
world state and the frames that map a command's picks into components).
Document::model_body_components answers for all bodies at once, and
world_state_reusing and body_frames_with_source use it. The engine's
frames now work out the per-body table only when a command picks a body
or names one; most commands pick none.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Every evaluation fingerprints every feature by the parameters its
expressions name, and works out every parameter, and most of them (named
feature inputs) are literal values such as "10 mm". In a design built
command by command both were lexed and parsed again on every command.
references() now keeps its answers per thread (bounded), eval_params
keeps its last few results for identical definitions (compared in full),
and a parameter that names no other is evaluated once per expression,
unit and design length unit.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A standard part (a nut, washer, bolt, screw, pin or bearing) drew its sketches without
constraints, so each was left with free degrees: 3 for a hex, 8 to 12 for a revolved outline.
A check of a design that holds parts listed every one of them as not fully defined. The part
builder now fixes each sketch's points and curves before finishing it: the outline is the
part's size, and a resize builds it again the same way.
Claude-Session: https://claude.ai/code/session_01XhgPofrmFDeLvMhar6Wsqw
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A new feature is named after the first free number of its stem
(Cylinder1, Cylinder2 …), and every number tried searched all features'
names: the thousandth cylinder of a design tried a thousand names against
a thousand features. A new input's dN name likewise wrote out every d1,
d2 … it tried. Both now gather the numbers taken after their stem once
(params::numbers_after, which counts only names written as the number
is) and take the first number not among them. The names are the same.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Every evaluation fingerprints every feature by its JSON, so a design
built command by command wrote all of its features out as JSON on every
command. The text is now kept (per thread, by feature id) with the
feature it was written from, and written again only when the feature
differs; the text hashed is the same, so the fingerprints are too.
Imports, whose text depends on their STEP digest, are written each time
as before.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
document.inspect with measure (what inspect_design and the
solidcraft://design resource ask for) measured every body one after
the other, and again on every call: on a 1,280-body design each call
took 25 to 60 s. It now measures them on all cores, as
inspect.measure does, and each body keeps its measurement in a cell
shared by its clones (with the body it was measured on, as the outline
box is kept), so a design inspected again measures only the bodies that
changed. Placements taken as before keep their placed body's cell too.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Every joint whose offset, angle or values are expressions is worked out again
on each solve, twice per solve (re-expressing the joints, then the check that
leaves out one whose expression no longer works out), and each expression that
names a parameter evaluated every parameter of the design anew
(`Document::eval`). The cost grows with the joints with expressions times the
design's parameters: on 40 parts jointed to a plate, each joint lifted by an
expression and each part carrying 13 parameters (522 in all), a joint solve
took 0.55 s on main and takes 0.41 s now, a change of the lifts' parameter
1.06 s and 0.81 s (CLI, medians of 5 runs).
`joints::Params` keeps the design's parameter values once the first
expression names one, and `joints::expressed` takes it, so one pass evaluates
the parameters once. Values and error messages are those of `Document::eval`.
Test: `kept_parameters_evaluate_as_the_design_does` compares values and
errors with `Document::eval`.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Test: a label on a path follows its source far
A rectangle of construction lines placed on three points projected from another sketch, with a
centred text on its far side: moved 270 away by the parameters that place the source, its solve
from the stored sketch failed ("the sketch constraints conflict").
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsiqyXWBFGTCX9J51bhysc
* Sketch: a sketch whose links moved far is walked there and keeps the stored sides and turns
Problem: a change upstream moves what a sketch projects (or the points its parameters place)
while its own values stay. The solve from the stored state then either failed ("the sketch
constraints conflict") or landed on the mirror image of a side or turn and reported it solved,
because a solve after a link move checks no orientation when no value changed: a line tangent to
a moving circle hopped a diameter to the circle's other side. And a sketch moved so was stored
only when its own values changed, so every later evaluation solved it from its first stored state
again.
Solution:
- flips_from_stored: the orientations of the stored sketch (unsigned offsets, distance
triangles, and a line's side of a circle it is tangent to) that a solved sketch turned over,
each read where the stored sketch has its points.
- solve_after_link_move: the new values solved where the links were stored, then the moved
points walked to where they are now (predictor-corrector: each step starts from the last two
solutions carried along their secant; steps halve on a failure or a flip and grow while they
solve).
- Document::solve_sketch: when a linked or fixed point moved and the jump fails or flips, the
walk; a flip the walk cannot avoid either is reported as one.
- Model::edited_sketches: a sketch whose linked points a value edit moved is stored as solved, so
the next evaluation starts where this one ended.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsiqyXWBFGTCX9J51bhysc
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #1796
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SBN24y2TcqRcm2zn8Gkj27
* Sketch Symmetry: centre a line between a face's edges (#1796)
Symmetry of two edges of a face (its projected outline, whose sides run
in opposite directions) about a construction line between them was
refused with "neither entity can move". The pairing of the line ends by
mirror image landed on main in #3029; this keeps the tool side and the
tests for #1796:
- The Symmetry tool and a three-line preselection take the only
construction line or centerline as the symmetry line, whatever the
pick order (picking it first used to mirror it out past the other
line). Two points and a line take the line. Otherwise the last pick,
as before.
- Tests: fixed lines drawn the opposite way, pairs by mirror image (a V,
end to end along the axis), every pick order, a three-line
preselection, and a scenario clicking a face's edges.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SBN24y2TcqRcm2zn8Gkj27
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #4813
* Sketch text: measure a line by the rule that draws it (#4813)
`TextMetrics`, made once from a font and a style, parses the face once and gives the scale for a capital height, the
distance between lines, a line's width and the characters the font has no glyph for (in order, each time, including
those its character map sends to `.notdef`). `text_geometry_styled` lays its lines out through it: one rule for
measured and drawn text, no kerning, a missing character still drawn as `?`. Measuring builds no outlines, so a text
too large to draw (`MAX_LINK_ENTITIES`, `MAX_TEXT_CHARS`) can still be measured. Every outline stays bit for bit the
same: a test pins five styled texts, captured before the change. The size and spacing checks are shared, and still
run before the font is looked for; a text whose place or angle is not finite now says so instead of naming its
height.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #5523
* Windows resize smoke test: wait for a settled window before hit testing
Startup placement can move the window between CheckBorder reading its rect
and the hit tests, so correct answers for the new position failed against
stale coordinates. Wait for two reads 200 ms apart to agree before the first
pass, compare the rect after each pass, and retry the pass (up to three tries)
when it moved. A pass with a stable rect fails exactly as before.
Fixes#5523
* claim: #5302
* Extrude: a picked profile takes the whole drawn profile (#5302)
A sketch on a solid's side face holds the face's outline as projected
reference geometry, and that outline cuts the drawn profile into pieces
of the sketch's graph. A pick by index or by point resolved against those
pieces, so extruding a rectangle that straddles the face's edge took
only the piece under the cursor.
The pick is now expanded to the drawn profile it belongs to, so it takes
every region of it. The drawn profiles are the disjoint faces of the
drawn graph, so an explicit pick of a drawn hole still fills only that
hole (#5176).
* Extrude: a pick expands across the reference geometry that cuts the drawn profile (#5302)
The expansion crossed every region of the drawn profile, so a projected curve that bounds a region inside the profile (the rim of a counterbore) was crossed too and the cut took more material than intended; now the expansion stops at that boundary. Fixes the regression in cmd::features::prism_local_tests::a_ring_cut_round_a_counterbored_hole_in_a_chamfered_disc, and keeps the behaviour of #5302 and of the #5176 invariant.
* claim: #2722
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Sketch on the sides of a thin extrude (#2722)
A thin wall's sides lie off the sketch curves (half the thickness either
side, say), so no curve was under them: its side faces had no role, no
exact history name, and Create Sketch refused them as "can't keep an
attachment". The roles of a thin wall's sides are now
`wall:<curve>:left|right` (the side of the curve it runs beside, in the
direction the curve is drawn), and its open ends `wall:<curve>:start|end`.
They follow the thickness and side settings and survive a save.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* claim: #5822
* Batch: consecutive parameter edits can rebuild the design once (#5822)
An agent's batch of parameter edits, one parameter per step, evaluated the
design once per edit: an edit early in the timeline rebuilds nearly all of
it, and 21 such edits ran for minutes.
engine.batch and the MCP batch tool take coalesce_parameters (default
false). With it, each run of consecutive plain parameter edits
(parameters.change with only a name and an expression, or with changes) is
set as one parameters.change {changes}: one rebuild for the run. Any other
step ends a run, so later steps see the design up to date. Every step
still gets its own result (coalesced: the run's length); the run is one
undo step. A run whose combined edit is refused (a new parameter, which
changes can't make; a bad expression) runs step by step, so a refused edit
is still named by its own step. The app's off-thread evaluation computes
the run the same way. The grouping is solidcraft_engine::batch_params,
shared by both runners.
21 parameter edits on a plate with 21 extruded bosses (in-process MCP,
ci profile): 40.1 s step by step, 3.5 s coalesced, the same volume.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Batch coalescing: say a run skips the states between its edits; test it against step by step (#5822)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #5881
* Snap points: the centre, middle and ends an edge or face offers (#5881)
Phase 1 of #5881. `solidcraft_engine::snap::snap_points` gives the points
a picked item offers as picks, from Measure's exact shapes: a round edge
its centre (an arc also its ends and exact middle, joined across pieces
the kernel split it into), a straight or other edge its ends and middle,
a flat face the centre of its area, a cylinder or cone the centres of its
ends, a sphere or torus its centre, a vertex itself. Nothing uses them yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* doc: persistent names read as their structure; joint.inspect splits an edge with a blend face first
`persistent_name::{FaceName, EdgeName}` parse a face or edge name once (blends, inner walls,
copies, pieces, numbered pieces, `~k`) and write it back as the same text, bounded against
hostile input. A suffix after a blend's edge naming a later feature belongs to the blend face.
joint.inspect split an edge at its first `|`, so an edge whose first face is a blend
(`F4:blend:F3:+x|F3:+z#[…]|F5:-y`) came back as a cut-off blend name and a string that is no
face. It now uses `EdgeName::parse`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* doc: persistent names as typed values: features, roles, suffixes, a lexer and errors that say where
`FaceName` is a base (made by a feature in a role, a blend and the edge it rounds, an inner
wall, a body's face by index, other text) and its suffixes (copy, piece, numbered piece).
Roles are typed where the vocabulary is known (top, side:<curve>, +x, face:<k>, …) and kept as
written otherwise. The text form is lexed into tokens and read by the grammar in the module's
doc; a name that doesn't read says where and what was expected.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* names: read what made a face, its kind and its feature ids through persistent_name
The places that took a name apart by hand to find the feature that made a face, whether it is
a blend, a copy, a numbered piece or a wall, or to remap feature ids on copy, now ask the typed
name (`made_by`, `split_feature`, `is_blend`, `is_copy`, `is_numbered_piece`, `is_wall`,
`features`, `map`). A name that doesn't read falls back to the old text check.
- An offset of a face that only names a blend inside it (`F2:top#[F5:blend:…]`) is no longer
taken for a fillet's.
- Copying a component remaps feature ids inside a piece's `#[…]` too.
- A shell's inner wall split by a later cut keeps the qualifier on the wall.
- joint.inspect finds the sketch curve of a copied side (`side:l1@F7.2`).
Finding a face by name (strip_piece, parent_piece) is unchanged: it reads a trailing
qualifier as the whole name's and moves once the text form says where it belongs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* engine: regression tests for press-pull on a wall that only names a fillet and joint.inspect on a copied side
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* names: an edge holds one face or more; made_by reads only the leading token
An open boundary's edge has one face and an edge several faces meet at has more, so an edge name
is a list of faces (a blend's own edge still takes two at most, as before). `made_by` reads just
the leading `F<n>:`, which every name a feature made starts with, so asking it of every face
each frame stays cheap. Names are read up to 1 MiB: a face engraved with text has piece names
past 4 KiB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* persistent_name: split into lexer, parser, write, text and tests
No code changed; callers' paths stay the same (`pub use text::*`).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #5962
* Timeline: reorder anyway (allow_errors) and report the features the move breaks (#5962)
timeline.reorder and timeline.reorder_many refuse a move after which a
feature that worked would fail. Sometimes breaking a later feature is the
point: an agent restructuring a design wants the move made, then the list
of what broke so it can repair those features next.
Both take allow_errors (default false: refused as before). With it the
move is made and the reply lists broken: [{feature, error}] (empty when
nothing broke); one undo puts the order back. timeline.reorder_many also
takes active, as timeline.reorder does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A pattern count such as (L-2*Post)/210 is a length when the 210 is unit-less, and was refused with only "expected a unit-less number, got a length". The message now says to divide by a length, e.g. floor(L / (210 mm)).
Co-authored-by: Nathalie Schönwetter <nathalie@cfw-ag.ch>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
render.settings.set stores exposure_ev and the environment controls, but
render.preflight and render.start refused any setup with them because the
CPU image executor doesn't apply them, so such a setup could never render.
The CPU executor now renders it and the replies list the left-out
controls in ignored_controls.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #4230
* Ribbon: an active contextual tab looks selected (#4230)
Contextual tabs (Sketch while editing, Sheet Metal for a sheet body) chose
their solid accent fill before checking whether they were active, so
selected and unselected looked the same.
The selected contextual tab keeps the solid accent pill. An unselected one
now looks like any unselected tab with its accent as an outline, so it still
marks the context without outshining the tab that is selected. Ordinary tabs
are unchanged.
* claim: #4363
* A straight 3D sketch line can be picked as an axis (#4363)
3D sketch wires were always offered as curved, and axis inputs resolved
only 2D sketch lines, so a straight 3D line couldn't be an axis (Plane at
Angle and others). A single-segment wire is now offered as straight and
resolves to its endpoints in model space; splines are still refused.
Combine, Loft, Mirror, Circular pattern, Rectangular pattern and Sweep get the
same two-sentence help as the first six (en, fr, ja, zh). Text only.
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Removing a design from the Start page's recent list only changed memory, and on the web sync_recents() rebuilt the list from stored designs on each launch, adding it back. Track removed designs in a persisted 'forgotten' set that sync_recents honours; opening or saving a design, or deleting/renaming its file, clears it.
* Viewport: show the running tool's icon beside the pointer
While an interactive tool or a command's dialog runs, its toolbar icon
rides above and right of the pointer in the viewport, so the next click's
command is visible without looking up at the ribbon. It sits clear of the
system arrow and of the sketch tools' coordinate readout below the
pointer, moves left at the right edge and below the readout at the top.
It shows only where a click reaches the model: not over panels, dialogs,
menus or the navigation bar, and not while the camera moves. The icon is
the command's own (the one the ribbon and the dialog header show), so
there is no new artwork. On by default; "Show tool at cursor" in View
settings (`ui.set {"toolCursor": false}`) turns it off.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Viewport: the tool at the cursor is off by default
As asked in review: the badge starts off, and "Show tool at cursor" in
View settings (or `ui.set {"toolCursor": true}`) turns it on. The test
checks the default and turns it on through the View settings checkbox.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #5244
* Sketch Create menu: family rows show their icons
The Line, Rectangle, Circle, Arc, Slot, Polygon, and Spline entries in a
sketch's Create drop-down were drawn as plain text buttons. Draw each as
the shared menu row with its first variant's icon and a submenu arrow,
keeping the same popup handling for its variants.
* claim: #5304
* Sketch: undo and Backspace remove the last pending spline point (#5304)
While a spline tool has points placed, Cmd/Ctrl+Z and Backspace take back
the last point instead of undoing sketch history or deleting the selection.
Undo reaches sketch history again once no spline points remain.
* claim: #5288
* Mirror keeps the preselected feature when the object type changes (#5288)
The pre-selection moves into the objects input when a pattern window
opens, and switching Object Type cleared that input, so switching to
Bodies and back lost the selected feature. The window now keeps the
objects of the type not shown and restores them when it is chosen
again. A plain timeline click on a feature already picked keeps it;
Ctrl+click takes it out.
* claim: #5415
* Floating history: the last done feature is no longer drawn as selected (#5415)
The floating history strip passed the last feature before the marker to
HistoryChip as selected, so it looked selected with nothing selected.
Pass only the real selection, and mark the last done feature with a small
tab-accent dot under its chip instead.
* claim: #5546
* Agent chat: hand WSL paths to agents started through wsl.exe (#5546)
An ACP agent started through wsl.exe was handed the session's working
folder and the solidcraft-app bridge's command as Windows paths, which it
can't use: the Claude adapter refuses the session, and the bridge never
starts. When the agent's program is wsl or wsl.exe, convert both to their
WSL form (C:\Users\... -> /mnt/c/Users/...) before session/new, in the
chat and in the Test button. The bridge still runs on Windows and reaches
the app at 127.0.0.1.
The Preferences > Navigation overall speed slider stopped at 3.00x, but
`space_mouse::frame` clamps the applied speed to 0.1..=5.0, so the top of
the range the engine honours could not be reached from the UI. Raise the
slider's ceiling to 5.0x to match that clamp; the step, format and lower
bound are unchanged.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* claim: #5680
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Preferences: draw the Z up orientation figure right-handed (#5680)
Z up showed Y pointing toward the viewer on the left, a mirrored
(left-handed) figure. Y now points away on the right, like the axis
gizmo. The axis tips move into orientation_axes and a test checks that
X to Y and Y to Z turn counter-clockwise in both figures.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* claim: #5488
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Link the version number in the title bar to its release notes (#5488)
The version at the right of the title bar opens the release page of this
build (v<version>, which exists for stable, candidate and nightly
releases) in the browser; a development build opens the release history.
Its click area stops at the report button, which the long version
strings of candidates and nightlies overlap.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* claim: #5828
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Hem: the handle runs along the hem to its tip (#5828)
The hem's on-canvas handle started on the picked edge and pointed square
to the sheet (#4794 only flipped it), so it neither sat on the hem nor
moved the way the Length grows. Length is measured from the outer face
of the fold to the tip, so the handle now starts on the centre line of
the hem layer at that face and points along the hem toward the tip.
The test builds the hem for both Flip states and checks the handle's
direction, its start on the layer's centre line, and that start plus
Length lands on the built tip. It replaces the arrow-side test of #4836,
whose rule (the arrow points to the side the hem builds on) no longer
applies.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* claim: #5850
* Find in Timeline scrolls both timeline layouts to the found feature (#5850)
Find in Timeline selected the feature but neither the bottom History strip
nor the history on the left scrolled to it. The handler now leaves a one-shot
reveal request (TreeState::timeline_reveal); whichever layout draws its items
next scrolls the found item into the middle of its scroll area and drops it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #5855
* Ignore middle-click clipboard paste during navigation (#5855)
Firefox on Linux sends a primary-selection paste event when the middle
button orbits or pans. Paste events on frames where the middle button is
pressed, held, or released no longer reach the design; Ctrl/Cmd+V still
pastes.
* claim: #5905
* Drive Joints: a Show ghost checkbox (#5905)
Driving a joint draws the moved parts' old place as a translucent ghost,
which gets in the way when driving joints over and over. Drive Joints has
a Show ghost box under its value rows: on by default (nothing changes),
off draws only the moved parts at their new place. The stopped-short
warning stays. The choice is a preference (drive_joints_ghost), so it is
remembered between uses; it affects Drive Joints only.
The preview's Colors carry the choice (ghost), so toggling it recomputes
the preview (its key hashes the colours) and world_preview leaves the
ghost out.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #5868
* Design tabs: the tooltip says where the file is saved (#5868)
Hovering a design tab showed the design's name, the same text as the tab.
The tooltip now says where the file is saved, with a path in the home
folder shortened to ~/ (home::friendly_path, as the start page does), or
"Not saved yet" for a design never saved. When the tab is too narrow for
its name, the full name comes first, then the place.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Look At already has a key (Ctrl+8, Cmd+8 on a Mac), but the button's
tooltip didn't say so, as Fit's does (F6).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
desktop_path_can_start_an_agent_and_its_runtime gave the version probe
3 s and the first reply 5 s. Under a full test run on a loaded macOS
machine the probe came back empty (version None, twice in a row), while
the test alone passes in 0.4 s. Both limits are now 20 s; a working
agent still answers at once, so the test takes no longer.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Agent chat: a Mammouth AI preset for OpenAI-compatible endpoints (#5815)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Agent chat: the key test names the Mammouth AI URL too (#5815)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
With the Thickness box cleared, the dialog sent thickness: "" and the
command refused it ("`thickness` must be a number or an expression"),
shown as a red error with Create Issue. The dialog now makes no command
until a thickness is entered.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Where a body's face lies flush with another body's face, the face point
alone could resolve to the other body, and Shell then refused with
"select at least one face to remove". The dialog now passes the body of
the picked faces, as the command already accepts.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
With a sketch profile and a body face picked together, OK ran the
profile's extrude first and then looked the face up by its point. Where
the profile's extrude covered the face, it was gone: "no face of Body1
at face". A new extrude now runs its face picks first; an edit still
rebuilds the profile's extrude first.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Plane at Angle's OK with an empty Plane or Axis input showed "select a
plane first" or "pick an axis" as a red error with a Create Issue
button, and people filed it as a bug (#3292, #3773). A refusal that only
names what to pick is now shown as a plain hint, with no issue button,
in every dialog (Sketch Modify dialogs already hid it).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
sheet.export_dxf always wrote the dashed bend lines (BEND_UP,
BEND_DOWN) and split the flat pattern over a layer for each role. A
cutter that cuts every line it is given cut the bend lines too.
bend_lines: false now leaves them out, and layers: single writes every
entity on layer 0. The defaults keep today's output.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Test: a saved model keeps each sheet body once
A sheet-metal design wrote its sheet bodies again with every later feature's state in the saved
model: each feature that leaves the sheet alone (a box beside it) adds more than the whole sheet
to the records. A design of many features after its sheet metal passed the cache's 32 MiB
manifest limit that way and kept only some of its states (#3220). The test also opens the saved
file and wants the sheets taken from it as evaluated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsiqyXWBFGTCX9J51bhysc
* Model cache: each sheet body is written once, not with every state
Problem: a saved model writes every feature's state, and each state wrote its sheet bodies in
full; most features leave the sheets as they were, so a sheet-metal design of many features
repeated them over and over (a 280-feature part: 30.5 MB of manifest, 29.6 MB of it 69 distinct
sheet records written 243 times). Past the manifest's 32 MiB limit the save thinned the cache
(#3220) and opening evaluated the left-out features again, although the distinct data was small.
Solution: the records keep a list of sheet bodies, each written once; a state names its sheets by
their place in it, as it already does for bodies and sketches. A thinned model keeps the whole
list and counts it once against its budget; the reader counts sheets with the bodies and sketches
a state holds; the cache format is now /4 (an older cache is a miss).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsiqyXWBFGTCX9J51bhysc
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #6161
* Preferences: the theme strip shows arrows where more themes are (#6161)
The strip scrolls sideways with a hidden scrollbar, so nothing showed that
more themes were past its end. An end with themes past it now shows a
chevron button over the tiles there; a click scrolls most of a page.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #6304
* view.pick: map screenshot pixels to exact geometry (#6304)
A read-only command that answers, for screenshot pixels (window or model
frame) or world-space rays: body, face index, exact world point and
outward normal, depth, occurrence/component for placed parts, and the
nearest edge/vertex within edge_px. Faces are raycast through the same
per-body pick index the mouse uses; flat faces are solved exactly
against their plane, curved faces take the mesh hit with the normal
interpolated from the mesh's surface normals. Section-aware: faces on
the removed side do not stop a pick and the cut cap answers with
section_cap. Never touches the selection, camera, document or undo.
Exposed as control ui.pick and as view.pick through engine.execute and
engine.batch (and through them MCP execute and batch).
---------
Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
* claim: #320
* Sketch: toggle a selected dimension between driving and driven (#320)
A selected sketch dimension now offers 'Toggle Driven Dimension' in the
viewport right-click menu, running the existing engine command
sketch.toggle_driven. The over-constrain -> driven placement half landed
in #366; this exposes the engine's toggle in the UI, the remaining gap
from the retest notes on #320.
- context_menu: sketch_entity_items collects selected SketchConstraint
ids that are dimensions (kind.value() is Some) and, for a single
selected dimension, prepends the toggle item aimed at that constraint.
Non-dimension constraints and plain entities get no item.
- Label reuses the existing catalog entry cmd.sketch_toggle_driven, so
every locale translates it with no catalog change.
- Test: the menu offers the item for a selected dimension, running it
round-trips driving -> driven -> driving, and a horizontal constraint
or a plain curve gets no item.
---------
Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
* Test: a long configuration table builds the cells of the rows on screen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Configurations: the table builds the rows on screen and its cells once per change
A table of hundreds of configurations was slow to open and to use: every frame cloned the
whole config.table answer, rebuilt every cell's text, looked each parameter's unit up once
per cell and built an editor for every cell of every row, on screen or not.
The answer and its cells (with units, looked up once per column) are now kept per design
revision, and rows off screen keep their space without building their cells, as the
parameters table already does. A row with an edit in progress stays live, so leaving its
cell still commits the edit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The taskbar asks a window for its icon once, when the window appears. eframe
sets the icon while the window is still hidden and shows it after the first
frame; when that frame is slow (safe mode's software renderer) or startup kept
the UI thread busy long enough for Windows to mark the window as not
responding (building the viewport pipelines), the request isn't answered in
time, the taskbar falls back to the window class's icon, and winit's class has
none: the button showed Windows' generic icon for as long as the window was
open.
Give the class the exe's embedded icon (resource 1) before the window is shown.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* inspect.parameter_sweep: check a design across a parameter's range
Rebuilds a copy of the design at each value of a parameter and reports what
no longer holds compared with the design as it is: features that fail or
warn, joint origins that are lost or turn over on their own part, sketch
planes that turn over, and sketch solves that fail or fold. The design is
not changed; each value's rebuild time is reported too.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* inspect.parameter_sweep: params doc matches what it takes and returns
`steps` takes at most 199 (200 values), as the check says; failed and
warned entries carry the feature's name; a lost origin says which one; a
value the parameter refuses gives {value, ms, ok: false, error}. The
refusal test covers steps 200.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* inspect.parameter_sweep: Japanese and Chinese labels
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aac7mP9rthK3WsPpPRvCsF
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* claim: #3988 (3D mouse: follow the device that moves, not the first one found)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* 3D mouse: follow the device that moves, not the first one found (#3988)
On Windows and macOS the reader opened the first 3D-mouse interface it
could and read it until it failed. A 3Dconnexion Universal Receiver with
nothing paired opens fine, has a valid layout and never fails, so it kept
the reader for good. A SpaceMouse on a cable next to it was never read
("Not calibrated (hid 256f:c652)", every axis at zero).
The reader now opens every candidate and follows the one in use. The
first is followed from the start, as before. Another takes over once it
shows use (an axis off zero or a button down) while the followed one has
shown none for STALE_AFTER, so two devices moved together don't trade
the reading. A candidate that fails is dropped; if it was the followed
one the reading resets and the next is read. With one device the reader
behaves as before, down to the 100 ms read timeout. Several take 10 ms
turns, and reports queue in the system meanwhile.
The selection is `follow()`, written against a small `ReportSource`
trait, so it is tested with simulated devices on every platform: an idle
receiver next to a SpaceMouse, two devices in use at once, and devices
failing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* 3D mouse: a press that makes a device the one in use counts as a press
Since #3825 a HID device's first button report after it connects is its
baseline: a button down in it is held, not pressed. A device taking over
from another one was connected at that moment, so the press that took
over was swallowed. With an idle Universal Receiver found first, that is
the first button press on the SpaceMouse.
follow() now keeps the buttons each candidate last reported while another
was read, and the reader hands them to the reading as the baseline when
that candidate takes over. A device that reported no buttons before it
took over keeps the held-not-pressed rule.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Tyler Chiriboga <18270301+Tyler-Chiriboga@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The ribbon's command-search field had its own area at `Order::Foreground`, so
it painted above every system window: a Preferences card moved under the top
right corner showed the search field floating on top of it, while the rest of
the ribbon went under the card as it should.
Closed, the field is ribbon chrome and now draws at `Order::Background` with
the rest of the ribbon; open (or animating open or shut), it keeps
`Order::Foreground` so the results sheet still floats over every window.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
On 0.21.0 the facts test failed on rectangle symmetry, which now works
on two sides. Other statements in the modeling and mechanisms skills
also no longer match: edge picks have a distance limit, concentric
circles extrude as a ring, a hole sketch below a part drills into it,
slot ids count on from earlier curves, shell refuses curved blends and
lofts, measure and inspect_design grow with the body count, and
joint.sweep needs occurrences with against.
Each correction has a test in test_facts.py.
* Fillet/chamfer: find each edge on its own body
A blend picked over edges of several bodies was evaluated on one body,
the one nearest the first pick, and every edge was named on that body
when the feature was made. Picks on the other bodies were named after
that body's nearest edge and rounded it instead (or moved to it by
position once an upstream edit moved them). With two stacked boxes, a
height change put the upper box's picks on the lower box's rounded
corners: the upper box's fillet resolved there and its own edges lost
their rounds.
Now each edge is named on the body it was picked on, and evaluation
finds each edge's body by its stored name (a gone name stays on the
feature's other bodies), groups the edges by body and blends each body.
A name an older build recorded on the wrong body (the pick lies outside
the box its reference recorded) is not used; the pick finds the edge.
A pick an upstream edit left at an end of its named edge goes to the
edge's middle, so the kernel cannot take the neighbouring edge.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Fillet/chamfer: skip the per-body lookup when there is one body; name each body's picks in one pass
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Release 0.22.0: version bump, design format 35
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Release 0.22.0: tests no longer expect the folded render and material changes to be pending
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Performance overlay: frame-time graph and histogram, triangles, draw calls (#2548)
Net change of the perf-overlay branch, rebased onto main as one commit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Document frame_rate_details in ui.prefs
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: #4971
* Thread: the refusal for a fine-only size says what to type (#4971)
M28 and other sizes without an ISO coarse pitch were refused as an
unknown thread, with examples of other sizes. Thread and threaded Hole
now say that the size has no coarse pitch and suggest two to give, such
as M28x2 or M28x1.5. The coarse table is unchanged.
* Thread: automatic sizing past M56 says what to type (#5055)
Automatic thread sizing only knows the ISO coarse sizes up to M56, so a
75 mm shaft was refused with "no metric thread fits a 75.00 mm
cylinder", although M75x2 and M75x3 work when typed. Past the end of
the coarse range, the refusal now says so and suggests two sizes with
their pitch, scaled to the diameter (a hole's size adds one pitch to its
bore). The coarse table is unchanged.
MidPoint on a point and a line keeps the first pick still and moves the second.
With the point picked first (an end of one line, then the line it should sit in
the middle of), the line turned about its far end to bring its middle to the
point: a line from (0,0) to (20,0) went to (0,0)-(10,10) for a point at (5,5).
A point and a line now put the line first, so the point moves and the line
stays; the line still comes to the point when the point can't move.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #5009
* Assistant: a request timeout per endpoint for slow models (#5009)
An OpenAI-compatible endpoint now stores how long one model request may
take (request_timeout_s, 300 s by default, clamped to 30 s ..= 1 h). It
is set in the Add agent dialog and in the endpoint's entry under Agents
(a running conversation uses a change from its next request), through
ui.agent_chat add_agent, and reported in the chat's state. A request
that runs out of time says the limit and where to raise it instead of
"timeout: global". The desktop HTTP cap goes from 900 s to an hour.
The tool-call timeout stays 330 s: it covers SolidCraft's own tool work
and the user's Allow, never the model.
* Assistant settings: line up an endpoint's Request timeout with Permissions
The label column of an agent's entry is wide enough for "Request timeout",
so the label isn't cut and both rows share one column.
* Undo: what is hidden lives in the snapshot, so hiding is a step (#2219)
`Session::execute` records a step only when the document `Arc` changes, and what is
hidden lives in `Session::visibility` — saved with the design, but outside the
document. `browser.visibility` was also declared `.noundo()`, and `Snapshot` had no
field for it, so even a recorded step could not put a hidden body back: undo went to
the command before the hide and left it hidden (#2219).
`Snapshot` and `Checkpoint` now carry the `Visibility`, `go_to` restores it, and
`Session::execute` treats a visibility change as a change of its own. That change is
display-only, so it refreshes the view with a revision bump (`refresh_display`)
instead of evaluating a model a hide does not touch.
The command also takes `folder: "all"` (every body, sketch, construction item, origin
item and joint, plus the browser folders and occurrence hides) so Show All can be one
step, and asking for the state an item already has no longer reorders the hidden
lists: a redundant hide is not a step and leaves no unsaved changes.
* Show/Hide: the menus run browser.visibility, so the history has it (#2219)
The hide and show actions wrote `session.visibility` directly instead of running the
command: the body and face menus (`ui.hide` / `ui.show`), the `V` key
(`context_menu::toggle_visibility`), the browser rows' eyes, the Origin rows and
`ui.originAll` / `ui.originPart`. The engine could record a step and there was
nothing to record — which is why the release still answered "nothing to undo" after
hiding from the Browser.
They all run `browser.visibility` now (the repo rule that the UI acts through
commands), and `ui.showAll` is that one `folder: "all"` call, so Show All is one undo
step rather than three. `docs/occurrence-workflows.md` says what visibility does in
the history.
assets/locales/ko.json had 298 ids that en.json has and it lacks (the
help.solid.* texts, the whole Preferences page, the issue-report dialog,
history names, ribs, mouse gestures and navigation presets). Add Korean for
297 of them; "Of" is left untranslated because its context is unclear.
Wording follows the existing ko.json (필렛, 모따기, 돌출, 쉘, 바디, 피처,
비활성화, 환경 설정, 어시스턴트, 에이전트). The file stays in the canonical
sorted one-message-per-line form; no existing message changed.
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* claim: #4337
* Measure picks projected and intersection sketch curves (#4337)
Dialog inputs turned away every curve or point of a sketch that is not the
active one, which includes projected and intersection curves, so Measure
took the nearest item it did accept instead. Give Measure explicit
acceptance for these references, keep the source sketch on a click, and
accept them from a pre-selection and a box selection. Other inputs keep
their restrictions.
* claim: #3892
* Browser tree hover stays fast on a nested STEP assembly (#3892)
Hovering a placement row lit its whole subtree by walking it again
(Session::occurrence_bodies) on every frame, and opening a STEP file
makes its top placement active, so every highlight rebuild walked the
whole assembly again for the active placement's bodies. On a nested
assembly each walk resolves every placement below it, which cost
0.5 s or more per walk.
- Session::occurrence_bodies remembers its answers per placement until
the document, the evaluated model or the pending moves change
(occurrence_bodies_shared hands out the shared list).
- The highlight looks hovered bodies and faces up by name instead of
scanning the hovered list once per body.
* claim: #3222
* Sketch drag: project a face sketch's links once per live gesture (#3222)
Every live sketch.drag frame refreshed the sketch's links before solving
the drag. For a sketch on a face that projects all of the face's edges
again: 115-460 ms per frame on the top face of a 600 x 500 mm plate with
120 holes imported from STEP, against about 1 ms for the drag's own solve.
The model the links come from cannot change during the gesture.
The session now remembers the sketch, document and revision a live frame
left. The gesture's next frame reuses the links while nothing else has
run: any other command (undo included) or any other change to the design
makes the next frame project them again. A test counts the projections.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Sketch drag test: drop an unread assignment (#3222)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #3883
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Find in Browser finds the body; Find in Timeline opens the feature's folders (#3883)
A right-click on a body in the viewport picks a face, and the face menu's
Find in Browser selected the face's creating feature in the Features folder
instead of the body. It now reveals and selects the body, as the edge and
body menus already did.
Find in Timeline now opens any collapsed history folders around the feature,
and opens the Browser's Features folder for the picked occurrence, scrolled
to the feature (the reveal formerly behind Find in Browser).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Find in Browser opens collapsed rows above the body and finds later placements (#3883)
Open the document, placement, Bodies and group rows when the command runs, not while the
Bodies folder is drawn, so a collapsed ancestor no longer hides the body. Match the scroll
target by the placed body's world name, and fix the group row key ({scope}/g{id}).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Find in Browser picks the body's own placement; Find in Timeline leaves the Browser alone from rows (#3883)
Every component's first placement keeps the definition's body name, so the root placement
matched before the body's own one and Find in Browser on a first copy opened nothing above it:
the placement must belong to the body's component too.
Find in Timeline from a sketch, construction plane or feature row has no body to say which copy
was meant, so it opened the Features folder of whichever placement came first (the other copy),
or one that never lists sketches. From those rows it now only reveals the feature in the
History; from a face, edge or body it still opens that body's copy's Features folder.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
With an opened STEP assembly's imported root active (it owns no body), a
Join revolve, sweep or loft from a sketch on a nested part's face made a
separate body, and a hole at the points of such a sketch found no body to
cut. A Join with no targets now takes the body its profile sketch is on,
through the same active scope as an extrude, and a hole counts as a cut
for the rule that a cut from a component with no body works on every body.
Refs #3223
* claim: #4236
Co-Authored-By: Claude Code <noreply@anthropic.com>
* Coil: store an optional sketch and profile selection
Co-Authored-By: Claude Code <noreply@anthropic.com>
* Coil: sweep a sketch profile along the helix
Co-Authored-By: Claude Code <noreply@anthropic.com>
* Coil: accept a sketch and profile selection
Co-Authored-By: Claude Code <noreply@anthropic.com>
* Coil dialog: pick a sketch profile for the section
Co-Authored-By: Claude Code <noreply@anthropic.com>
* Coil: cargo fmt
Co-Authored-By: Claude Code <noreply@anthropic.com>
* Coil: refuse a loaded sketch coil that also sets the primitive section
A saved design could add section, section_position or start_angle to a
coil that already has a sketch; eval ignored them instead of refusing the
contradiction as the command does.
Co-Authored-By: Claude Code <noreply@anthropic.com>
* Coil: assert the kernel refusal and exercise hostile profile selections
Co-Authored-By: Claude Code <noreply@anthropic.com>
* Coil dialog: keep the sketch-profile check after rebase
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Format
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
* feat: extrude a face or profile by pressing on it and pulling
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* feat: pull-to-extrude keeps the window open and sets the step by sideways motion
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Pull to extrude: coarser step changes, a step ladder below the pointer, the step in the distance box
The step label sat on top of the distance box at the arrow tip. It is now a ladder of the
step in use between its neighbours, below the pointer, and the distance box also names the
step ("Step 1 mm"). One step change takes 44 px of sideways travel instead of 22.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Pull to extrude: a setting, off by default (Preferences > Navigation)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Rename the pull setting to Direct interaction
It will also cover other direct gestures in the viewport later (grabbing an edge to round it).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Nathalie Schönwetter <nathalie@cfw-ag.ch>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* claim: #4853
* Sketch on a turned occurrence: reuse moved bodies across edits (#4853)
A sketch with a face anchor is evaluated in its component's frame.
state_in_frame_at moved every body placed differently from that frame
with a fresh transform on every evaluation, and the face-anchor frame
moved the anchored body again, so each sketch edit on a turned or moved
part transformed and re-meshed it.
The model now keeps the bodies it moved into a feature's frame, keyed on
the source body and the exact matrix (and checked against the source's
mesh, geometry, colour, paint and split lines, as world placement does).
An unchanged body under an unchanged placement is moved and meshed once.
Each evaluation starts a new generation and drops entries that neither
it nor the one before used, so the cache holds only placements in use.
Model states hold a weak reference, so the cache never keeps earlier
states alive.
Fixes#4853
A circular or rectangular pattern of a hole and the chamfers on its edges failed with
"no edge near ..." when a chamfer was picked before the hole: the chamfer was copied
first, onto a body that had no hole there yet.
Co-authored-by: Nathalie Schönwetter <nathalie@cfw-ag.ch>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
projection() reads the model's box for the scene's depth, several times a
frame, and model_bounds() sampled every sketch's curves for it each time,
hidden sketches too. A 533-feature design with 445 sketches spent about 45 ms
of every frame there (stack samples: model_bounds -> Sketch::bounds -> segs ->
spline polylines), so orbiting ran near 16 fps. The sketches' box is now kept
for the world state it was made from, as the bodies' boxes already are.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #4099
* Measure: a face or edge picked by its point alone (#4099)
inspect.measure (and the MCP measure tool) refused a face or edge item
without an index ("missing field `index`"). Face and edge items are now
read as body + point with an optional index: the point finds the face
(the same lookup face picking uses) or the nearest edge within the pick
tolerance, before the selection is built. A missing body or a point on
no face or edge is a clear error. Explicit indices still work.
* claim: #4098
* Extrude: document which way a positive taper goes (#4098)
A positive taper slopes the walls outward (the far end is larger along the
extrusion direction), a negative one inward. Say so in the params doc, the
JSON schema and the guide, and pin the sign with a test.
* Offset of free-form curves: trim folds, round chain corners
Offsetting a spline by more than its tightest bend failed with "offset spline
has no interior certificate correspondence": the plain offset folds over itself
there and a single linked spline cannot follow it. A chain of Bézier curves (any
imported SVG outline) was refused outright.
Such an offset is now the parallel boundary: the plain offset of every piece,
joined round each corner by an arc about it, trimmed where it folds or crosses
itself (a sharp corner is left), each kept run fitted with an open spline and
certified to the offset tolerance, and the whole proven free of crossings. Tiny
kinks are joined directly; zero-length Bézier handles (infinite curvature at
the end) are replaced by their chord within a quarter of the tolerance. A
single curve whose plain offset is smooth still gets the linked offset; the
trimmed or chained result is ordinary curves, not linked to the source, so the
command adds no distance parameter for it.
* Offset chains mixing lines or arcs with free-form curves
A chain offset read every curve with Sketch::exact_segs, which only covers free-form
curves, so a chain with straight or circular pieces (a text glyph, an SVG path with
L commands) failed with "offset source changed shape". Read them with exact_pieces,
which gives lines and arcs from their analytic form; the trimming already handles
those pieces.
The solver takes at most 4000 unknowns in one connected group, and a
pattern joins all its copies into one, so a hexagon patterned 17 × 20
(4000+ unknowns) failed as "that would over-constrain the sketch
(conflicts with )", naming no constraint. The solve report now carries
the size of the largest group it could not take, and the failure says
how many unknowns are joined, the limit, and that fewer copies or
several patterns or sketches stay within it.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #3786
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Sketch: the first dimension scales only a connected drawing (#3786)
The first length dimension scaled the whole sketch about its origin, so a
second rectangle that nothing links to the first was resized and moved too.
Now the sketch scales only when its curves form one chain, joined by shared
points or constraints (the origin counts as a point); separate shapes keep
the previous behaviour: the dimension sizes the shape it measures.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Dragging one end of a line tangent to a circle or arc let the solver move the
line's other end too, sometimes far ("goes flying"): only the dragged point was
held, and the least-squares step spread the change over the free points. The
drag now first tries holding the line's far end as well, and falls back to the
old solve only when that can't be met. Dragging the end of a tangent line to
(5, 12) moved its other end 0.399 mm on 0.19.0; it now stays put.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A cut starting on a face of its target runs its tool on behind the start plane with the
tool's slope. A taper that widens the pocket narrows the tool there, and a corner arc
smaller than the run on times tan(taper) closed: "the taper closes the profile", though
the pocket itself only grows. The run on is halved, quartered, then dropped (the tool
starts on the face with its straight foot) until the tool builds.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The fastener tables stopped at M3 and M20. The socket head cap screw (ISO
4762), hex bolt (ISO 4014), hex nut (ISO 4032) and plain washer (ISO 7089)
tables, and the coarse pitches they use, now have M2, M2.5, M14, M24 and M30
too, in size order. Set screws and dowel pins are unchanged.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #5202
* History: show the sketch at the marker while it's the current step (#5202)
A sketch a later feature uses is hidden, so moving the History marker
onto it changed nothing in the view. The sketch immediately before the
marker is now drawn even when consumed or hidden; its stored visibility
is untouched, and earlier sketches, hidden components, groups and the
Sketches folder still apply.
* claim: #4717
* Canvas: a plane seen edge-on doesn't take the image (#4717)
Placed with a click at a sketch's origin, the canvas went onto the origin's XZ square, which
is under the cursor there too and was the first plane hit. From the Top view that plane is
edge-on, so the image was out of sight and Canvas seemed to do nothing. A plane hit counts only
when the view shows it (its normal more than ~11.5 degrees off the view's edge-on direction); the
sketch plane, or another plane, takes the image instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
With Sketch Dimension on and nothing picked yet, a click on a dimension
that is already in the sketch opens its value box, the same in-place
editor a double-click opens without a tool. Enter applies, Esc cancels,
and the tool stays on for the next dimension.
The dimension's text always takes the click; its lines only away from
sketch points and curves, which still start a new dimension (the rule
plain selection uses). Once a new dimension is under way, clicks keep
their meaning. Dimensions off in the selection filter are left alone.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #3032
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Create Sketch leads the right-click menu on empty space too (#3032)
A right-click on empty space in the viewport now starts with Create Sketch, in
the same place as a right-click on a plane (#1354): first, then a separator.
Over a plane it sketches on that plane, as before; on empty space it is the
toolbar's Create Sketch, so the next click on a plane or planar face starts the
sketch there. The menu offers it once.
Tests: empty_space_leads_with_create_sketch (the list) and
create_sketch_from_empty_space_sketches_on_the_plane_clicked_next (the harness:
right-click empty space, pick it, click XY).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Slightly fix the extend tool
* Extend: add the line the preview draws, and stop an arc on another curve
sketch.extend reported "nothing to extend to" whenever nothing stood in the
line's way, even though the viewport previewed a line past its end; with
nothing in the way the extension now runs as far as the preview shows, on the
same measurement, so the click adds what was drawn.
An arc extension that ran into another circle or arc refused the target when
its crossing already belonged to another curve's end (a shared crossing point
of two circles): that point is now joined instead, so the extension closes onto
the geometry that is there.
---------
Co-authored-by: SolidCraft Dev <dev@solidcraft.local>
Co-authored-by: n0tfr3shm1lk <n0tfr3shm1lk@ukr.net>
* claim: #4854
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Sketch palette: the Tie button shows "Tie", not its catalog key (#4854)
The button passed the key itself to ui_kit::button with Button::Normal,
which draws its label as given; it now passes the translated label. The
loose-ends test checks the button by its label.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A move changes no shape, but its preview copied the session, evaluated
the move and meshed the bodies again for every change of the values. While
the triad was dragged the part trailed the arrow by one of those
evaluations, and longer for a larger part. The preview now takes the
listed bodies' world meshes once and draws them moved by the move's
transform (about the world origin, as the Move feature turns bodies), the
original shown as removed and hidden as replaced; a copy keeps the
original. Anything it can't take (values that don't evaluate, a body not
shown, over 2 M triangles) goes to the scratch-design preview as before.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #5020
* Insert SVG: refuse geometry scaled to infinity, and bound the profile grid (#5020)
A transform such as scale(1e308) turned a circle's radius into infinity.
The SVG reader now refuses non-finite transforms and geometry with a clear
error, and Grid::cells in the profile finder uses checked arithmetic, so an
infinite or huge box is checked against all shapes instead of wrapping the
cell count and allocating until memory runs out.
* Insert SVG: refuse lines with infinite ends; fix the grid test's cell count
A line whose transformed ends are infinite was dropped silently, because the
distance between them is NaN. It is now refused as out of range, like curves.
The new grid test expected one cell for a box spanning its own one-box grid;
such a box touches the 2 x 2 cells at its corners, so it expects 4.
* Ctrl+1…7 turn the view to the standard views (#5057)
Front, back, left, right, top, bottom and isometric, handled with the
other view keys beside Ctrl+8: animated like the view cube, reserved so
no command can take them, and ignored on the flat pattern page.
* Front … Isometric View are commands, Ctrl+1…7 their default keys (#5057)
The standard views become seven commands (view.front, view.back,
view.left, view.right, view.top, view.bottom, view.isometric) whose
default shortcuts are Ctrl+1…7, instead of keys the window handles
itself. They show in Keyboard Shortcuts, can be moved to other keys or
cleared, and the digits can be given to other commands. In the app they
turn the view as the view cube does; headless they set the session
camera, keeping target, distance and projection.
A design file keeps its evaluated model so that it opens without
evaluating the timeline. A feature whose boolean needed a retry records
it, and the kernel's BooleanDiagnostics leaves `rejected` out when it is
empty but has no default for it when read, so reading those records
failed ("saved model: diagnostics"), the saved model was refused, and
the design was evaluated on every open (17.6 s for the reporter's
17-feature design, 1.2 ms with this).
The reading side now takes an absent list as an empty one, without a
change to the kernel crate.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Open or close every Browser folder from the design or a component menu,
including folders that start closed. Display only: not saved, not an undo step.
A face orthogonal to the sketch plane projected every edge onto the same
line: coincident and overlapping pieces, one per notch, that the face
boundary check then refused on the next evaluation ("boundaries cross,
touch or have ambiguous topology") and marked lost.
A link layout whose straight pieces all fall on one line is now one line
between its two farthest points; vertices along it can be projected one
by one. The face-boundary and loop-identity checks skip such a silhouette,
which encloses nothing. Coincident lines are laid out once.
* Orbit about the cursor: over empty space, turn about the part in view (#2616)
With Orbit around → Point under the cursor, an orbit begun over empty space
turned about the middle of everything shown. For a part far from the origin and
anything at the origin (a small body, a sketch), that middle is halfway between,
far off screen, and the part flew out of view: a box at (50000, 50000, 0) with a
10 mm box at the origin went from (872, 535) on screen to (-277084, -279746) after
a 180 px orbit. It now turns about the middle of the bodies the view shows part
of, and only falls back to everything shown when no body is in view.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Restore LF line endings in the files this change edits
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #5851
* Add a contribution skill for first-time contributors (#5851)
skills/solidcraft-contributing walks a new contributor from checking their
tools through fork setup, choosing and claiming an issue, a focused change
with checks and screenshots, and review. The skills index lists it and
the install commands point at tangency-labs/solidcraft.
* Skills install: copy the modelling skills only
The contributing skill is read from its link when someone wants to
contribute, so the install commands no longer put it in every project's
global skills folder.
#4349 fixed the start page treating a sample's shipped picture, which comes back under the
sample's key without a design, as its build failing. That race dropped the request with "the
sample could not be built" and made the s15_start_page_sample_save_reopen scenario fail
depending on timing. This adds the test that sends the picture while the sample is wanted and
checks that the sample still opens.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #3034
* Pattern of features: show the direction arrow on the patterned feature (#3034)
A rectangular pattern whose Object Type is Features had no drag arrow
and no value box: the canvas anchor knew bodies, faces, edges and
sketch geometry but not a whole feature, so it returned nothing.
A picked feature now anchors at the middle of the geometry it made
(its bodies, or its faces on a body later features changed). The
pattern's preview stands in for the body the feature lies on, so the
anchor reads the feature's geometry including replaced bodies, as a
body pick reads its body.
* claim: #3713
* Exploded views export as an animation (#3713)
motion.export_animation takes an exploded view (view: id|name) as well as a
study or a joint: the parts come apart at a steady pace from the assembled
placement to where the view shows them, and back together with back: true
(exploded at the middle frame). 41 frames by default, 81 with back. The home
view frames the parts both assembled and exploded. As with explode.show, the
design keeps its placements, its history and its saved state.
explode.show and the export share the moves; moves a design file gives with
no finite translation are left out.
* Mechanisms skill: an exploded view's default frame count
---------
Co-authored-by: MateuszSychXpl <151755914+MateuszSychXpl@users.noreply.github.com>
* claim: #2873
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Press Pull: route curved faces to Offset Face, with an arrow (#2873)
Q sent every non-edge selection to Extrude, which takes only profiles and
planar faces, so a hole's wall or a boss's side was refused. The engine's
solid.press_pull already offsets curved faces; the window now does too:
- Q on a curved face opens Offset Face; its Faces input takes curved faces.
- Q with nothing selected opens Extrude as before, and a first pick of a
curved face or an edge reopens Press Pull on it (Offset Face or Fillet).
- Offset Face's arrow follows a curved face's outward normal where it was
picked, so it can be dragged like an extrude's.
Fixes#2873
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #4582
* Keyboard Shortcuts: a bound Shift key is named "Shift", not "Shift+ShiftLeft" (#4582)
The shortcut's name was the modifiers held plus the key itself, so binding the
Shift key alone showed "Shift+ShiftLeft": the modifier named twice, and the
left and right keys as two different shortcuts.
A modifier key is now the shortcut itself, and its two sides are the same one
(the app's modifiers collapse them), so the page shows what the user pressed.
A shortcut bound before the change ("Shift+ShiftLeft") keeps working: the
stored name is migrated when the preferences or a shortcuts file are read.
* Keymap: the bound-modifier test passes the new repeat flag to press
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* claim: #4664
* Control: name the token file and spot a stale one when a call is unauthorized (#4664)
When the app refuses a token that the MCP client read from a token file,
the error now names that file, when it was written and how long ago,
without printing the token. The app also writes `<port>.pid` beside the
token file (its process id and, on Linux, its start time) and removes
both at exit; when that process has exited, the error says the token
file is stale. The process check runs only after a refusal, and the
token file itself stays one line, so existing clients keep working.
Document force-quit leftovers and the MSIX AppData redirection.
* Bench assistant: read the token out of the new ClientToken
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
The Origin folder's eye, a finished sketch's Show Profile and Show Dimensions,
and body locks move from UiState into the design's saved visibility
(origin_hidden, hidden_profiles, shown_dimensions, locked_bodies), so each tab
keeps its own and they survive closing the design. The Sketches folder uses the
saved browser folder (c0/sketches in hidden_folders); ui.set/ui.inspect keep
showOrigin, showSketches, hiddenProfiles, shownDims and lockedBodies.
Preferences > Design > "Hide origin by default": a design that never toggled
its Origin eye follows the preference. New fields are a pending format change
(format/pending/browser-view-state.rs).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #3862
* Coil: its parameters show and can be edited (#3862)
The Coil dialog now shows every setting solid.coil takes: the section
(circular or square), where it sits on the diameter (inside, center or
outside), the start angle and a Clockwise option. Edit Feature on a coil
reopens the dialog filled in from the feature, and OK redefines it;
before, it fell back to a timeline.edit line in the command search.
Settings left at their default are not sent, so OK on an unchanged edit
keeps the coil as it was.
* claim: #2224
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Agent chat: keep Send in view for a long message, open "View attached" (#2224)
The message box sat in a fixed 92-point reserve, so a long message grew it
past the panel and took Send, Stop and Attach view with it. The box is now a
panel of its own at the bottom: it grows upwards up to a cap, then scrolls,
and the conversation takes what is left.
The picture sent with a message is kept on it (once the agent starts on the
message), and the "View attached" caption opens it in a window.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Agent chat: a message's pictures as thumbnails with a hover preview and a viewer (#2224)
Instead of a window on the far side of the screen: the pictures of a message
show as small thumbnails under it, hovering one shows it bigger for as long as
the pointer stays, and a click opens a viewer over the app with the message's
pictures one at a time (previous and next, arrow keys, Esc). A picture carries
a note shown with it, so attachments of other kinds can use the same viewer.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* claim: #2286
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Agent chat: Attach selection sends what is selected in the model with the message (#2286)
A checkbox beside Attach view. The selection is read when the message is
sent (also for a queued one) and described: faces and edges with the names,
indices and points the tools take plus their size and place, features by name
and kind, bodies by name; at most 64, the rest counted. The agent gets it as
text after the message; the conversation shows the message as typed with the
things as grouped chips (hover for what the agent was told).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* WIP: the chat's context picker dialog (#2286)
* Agent chat: Pick context, a picker for elements, features and bodies (#2286)
A button beside Attach selection opens the picker: a dialog with Elements,
Features and Bodies inputs, so clicks in the model, the timeline and the
browser pick into the active one (clicking again takes one out), with the
usual highlight. OK puts the picks as chips with an x above the message box;
they go with the next message like an attached selection (text for the agent,
chips in the conversation) and the tray is empty again afterwards. Cancel
keeps what was picked before. The control channel opens it with pick.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Agent chat: Attach selection counts and describes what was clicked, not the pieces of a rim (#2286)
A click on a circular rim or a split surface selects all its pieces for the
commands. The checkbox counted them one by one and the agent got one entry
per piece (reported on macOS). They are now grouped back into the click they
came from: one chip, one entry with the whole size, and the indices of every
piece in its detail. Edge details are looked up by position as well as
index.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Agent chat: a message's picture is kept on exactly that message (#2224)
The turn now carries its message's place in the conversation (also through a
queue and a send again after a sign-in), so its picture is kept there, once,
and never on an older message that also wanted one (review of #2225).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Agent chat: use the UI kit's checkbox and button and the font tokens (guard tests)
Main's UI-kit guard tests reject raw egui checkboxes, buttons and font sizes in the
chat panel; Attach selection, Pick context, the chip remove button and the chip fonts
now come from ui_kit and Tokens.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
* wip(render): reconcile completed material and image pipeline source with 0.20 (#5493)
Source-only integration reference for staged review, not a merge-ready aggregate.
Donor: 0xlillith/solidcraft@aea20b0115.
Upstream: 858e5995d6.
Retain original CPU/material contracts and explicit GPU donor provenance in
docs/render-materials-lab.md (#2067, #2191, #2780, #3306). Preserve released
format migrations and current navigation/startup/shell behavior. New saved
records use pending descriptors. The physical-card descriptor follows the
maintainer's material-engineering-cards naming request, with a legacy-lab
format-29 regression. Replace raw new panel hosts with shared containers.
Repository checks, complete workspace/all-target Clippy, and the independent
physical/resource authoring checks pass for this source. The fresh full local
CI invocation has reached test compilation and is not yet complete. Do not
transfer donor native output or prior test totals to this integration.
Generated review screenshots, video, package ZIP and machine logs remain at
the immutable personal evidence pin. Uncommitted geometric displacement is
backed up separately, not silently included. No kernel/vendor change, direct
main write, release or independent contributor-branch rewrite.
Co-authored-by: Christian Svensson <blue@cmd.nu>
* test(cli): pin the Compact camera-sample admission diagnostic (#5493)
The bounded request still refuses 1024x1024x64 samples before host initialization and writes no output. Update only the stale expected diagnostic to the existing Compact budget wording; production limits and code are unchanged. Exact-tree CLI regression suite: 14 passed, 0 failed. The full repository gate has been invoked separately and is not yet complete.
* Normalize UI container violation paths across platforms
Report repository-relative paths with forward slashes so diagnostics and the existing nested-package regression assertions agree on Windows and Unix. Keep the container detection rules unchanged.
Validation: official ui-containers gate and all seven focused xtask tests passed; rustfmt and diff checks passed. Only xtask/dependencies were compiled; no application build or heavy tests.
(cherry picked from commit 44b58d8e10)
* fix(render): preserve camera contracts and correct acceptance regressions (#5493)
Preserve Design view.apply semantics with a separate strict Render camera command. Retain existing translations when English-only aliases collide; add material-axis entries and camera-specific pitch labels. Compare complete validated engineering cards and retain job handles before cancellation.
Focused and native acceptance is pinned to tree f8a423b21f. Full CI has been invoked separately and is not claimed green. Landing follows the render integration policy in #5493.
* fix(render): resolve showcase licence and upstream resource hygiene (#5493)
Record SychPL's explicit MIT OR Apache-2.0 grant, retaining source/model bytes and the Ubuntu lettering notice. Remove 80 absent attribution rows, personal fork records, dead delivery attributes and three fork/resource-only workflows. Existing xtask assets now performs bounded offline hash checks and rejects orphan render attribution; no parallel draft CI or package installation. Original authoring reproduction remains explicit. Scoped xtask tests, assets, Clippy, formatting and prepared SELENE validation pass; no application or renderer-wide result is inferred.
* fix(render): align saved bounds and progressive sample resolution (#5493)
Address review r4364211669/r4364211677/r4364211681/r4364211691/r4364211695. Persisted light/frame domains respect the existing 1e9 document bound; related LED/rectangle/direction cases use the same bound. Workstation primary resolve shares the 4096 admission cap and film counters use checked conversions. Exact candidate: 485 CPU render tests, 21 render document tests and strict scoped Clippy pass, including positive boundary round trips and 2048/4096 end-to-end checkpoints. No released format/migration edits, relaxed test limits or full-stack acceptance claim.
* wip(render): isolate optional resources and harden sample handling (#5493)
Review work in progress: opt-in encoded render library with reproducible runtime catalog exports; native-only robust dependency through the workspace; checked bloom slices; per-sample point-light diagnostics; independent area-light spatial/film keys. Resource-module binaries in both configurations, generation, Cargo production feature graphs and attribution checks pass. Full engine/render regression and combined acceptance are pending: do not merge this donor as a constituent or treat scoped resource proof as runtime acceptance.
* fix(gpu): preserve donor allocations through the scope-safety repair (#5722)
Port the native-validated #5723 scope-cleanup correction into the experimental donor, retaining its adaptive, volume, aging, photographic and bloom allocations. Same reverse-drain helper and regression cases; the donor combination still needs its own compile/test gate and is not covered retroactively by render-base results.
* claim: #1369
* Reject unknown pending concepts after format folding (#1369)
* test(render): close bounded sampling and bloom review regressions (#5493)
Validate the optional-resource/lighting/dependency donor at its exact tree. Required Vulkan GPU 127/0/3; CPU renderer 487/0; engine Render 72/0; document render 21/0; lean library 7/0; strict scoped Clippy, assets/layers/lists/containers and lean-engine WASM pass. Preserve original failed controller/typing attempts. This is the draft donor/disposition source, not a replacement for constituent render PRs or final main acceptance.
* wip(render): preserve stable aging target validation candidate
Exact staged source from prior review work on donor 3b892eab5e. This is a recovery/review checkpoint, NOT accepted delivery. An actual command-path regression currently fails because the selected executor refuses ordered surface aging. No assertion, capability or admission limit is weakened. Native/UI/full combination acceptance remains pending. Saved/runtime split adds one pending descriptor and retains original file scope; no main/render branch is changed.
* test(render): preserve numeric setup intent at the admitted coordinate bound
Follow up jyviko review on #5494 without weakening the corrected saved-coordinate contract. The positive clicked fixture uses 1e9 instead of inadmissible 2e9; explicit out-of-range cases still refuse, and the full saved setup round-trips through JSON and the native container. Exact-source Render workspace 95/0 and strict all-target UI Clippy pass. This does not incorporate the separately failing stable-aging candidate or claim full current-render acceptance.
* test(render): use the real GPU executor for saved aging
Move successful native cold-reopen Check/Start coverage from the CPU-backed protocol fixture into the production GPU executor tests. Verify capture-index changes preserve the stable target and that enabling aging changes both PNG and raw EXR output. Retain explicit, allocation-free refusal coverage for executors without aging support. Production capabilities and admission checks are unchanged.
* fix(render): persist stable identities for targeted aging (#5493)
Separate saved RenderBodyTarget identities from captured mesh ordinals. Reject legacy ambiguous targets, resolve Check/Start after visibility changes and preserve native reopening, copies and undo. Unsupported executors still refuse; real AMD Vulkan regression completes saved aging after reordered capture.
Source tree 6b84c8ad passes document 23, engine Render 75, GPU effects 5 and Render UI 95 with no failures; scoped strict Clippy, WASM and repository gates pass. Fresh CLI/native reopen and clean/aged GPU exports pass with identical physical totals and inspected actual images. Whole current-render integration is separate.
* fix(render): reconcile combined CPU/GPU contracts on current upstream
Unify captured resources/status and CPU-vs-GPU request schemas after the real two-parent integration; remove obsolete duplicate Vulkan selector and retain combined platform-filter regression. Deduplicate only identical or superseded test definitions, retaining the stronger card metadata and reserved-executor assertions. No assertions, admission limits or feature refusals are weakened.
All targets of render dependencies, GPU, engine, UI, CLI and native app pass cargo check with raytrace-cpu and bundled-render-library at tree c70d00d1. Native executable tests and full current-source acceptance remain pending. This is a private integration rehearsal for subsequent constituent PRs, not an aggregate official render/main merge.
* test(render): retain one complete host lifetime regression set
Deduplicate byte-identical DropProbe/task-lifetime tests and retain the adapted environment/texture ownership regressions with every assertion and complete memory estimate. Keep the registered-geometry admission regression once; only struct field order differed. Replace a one-element diagnostic loop with its equivalent block, preserving all wrap/width assertions.
Test-only integration repair. Formatting and diff checks pass; refreshed executable validation is required. No production behavior, limits, assertions or ignores changed.
* fix(doc): recognize the proven format-29 card descriptor alias
Normalize only the historical format-29 physical-material-cards spelling, and only when material-engineering-cards is registered. Admission and migration de-duplication use the same identity. Unknown concepts, wrong versions and incapable readers still refuse before mutation. Six controlled-history Rust regressions and actual CLI migration/no-clobber acceptance pass. No version, released migration or CI changes. Refs #5793, #1369, #5493.
* test(render): preserve grounding refusal before explicit instance moves
Update the two Render binding fixtures for upstream first-occurrence grounding. Verify grounded movement refuses without changing document/history, then explicitly unground through the public command before the intended move. Retain every target, resource, copy and persistence assertion. Refs #5493.
---------
Co-authored-by: Christian Svensson <blue@cmd.nu>
Co-authored-by: Victor Santos Oliveira <56417388+Vittuu@users.noreply.github.com>
Co-authored-by: cfaulkingham <colin.faulkingham@gmail.com>
* Normalize UI container violation paths across platforms (#5599)
Report repository-relative paths with forward slashes so diagnostics and the existing nested-package regression assertions agree on Windows and Unix. Keep the container detection rules unchanged.
Validation: official ui-containers gate and all seven focused xtask tests passed; rustfmt and diff checks passed. Only xtask/dependencies were compiled; no application build or heavy tests.
(cherry picked from commit 44b58d8e10)
Scoped render-branch prerequisite from #4507, coordinated under #5493. No unrelated translation/kernel changes are included. Fresh Windows fmt, seven ui_containers regressions and the production container gate pass on render base 3a6153d18f. Full CI is reserved for final render-to-main acceptance under the maintainer policy.
Co-authored-by: Victor Santos Oliveira <56417388+Vittuu@users.noreply.github.com>
* Preserve translated source-label aliases (#5619)
Render integration prerequisite for #5493. Fixes#5618. Retain stable-ID fallback and translated legacy captions; include controlled regression coverage.
* Persist validated engineering cards independently of mass and optics (#1060)
Render constituent under #5493. Fixes#1048. One pending material-engineering-cards descriptor, immutable released migrations, independent assigned density and source conditions. Nine persistence and ten physical-card regressions plus the fresh quick gate pass. Additional native follow-up is reported separately; no full-stack acceptance is implied.
* Render: progressive GPU path-tracing executor (#2067)
Land the unchanged contributor GPU foundation into render under the maintainer policy in #5493. Fresh quick gate 39993339605 passed. Original author and co-author attribution retained; no workflow file changes. Final render-to-main CI and jyviko sign-off remain separate.
Co-Authored-By: Christian Svensson <blue@cmd.nu>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Render: asynchronous GPU setup and completed-image panel (#2780)
Integrate the original asynchronous setup, progress and completed-image UI after #2067. Refreshed source passes quick gate 40009901424. Original attribution retained. Final render-to-main full CI and jyviko sign-off remain separate.
Co-Authored-By: Christian Svensson <blue@cmd.nu>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Render: studio lighting, clear coat and glass on GPU (#2191)
Integrate the original studio renderer after the GPU executor and async UI foundations, under #5493. Fresh render-target quick run 40011408339 passes. Preserve attribution, bounded resource admission and documented glass/denoising limits. Historical native benchmarks retain their source pins. Full renderer-to-main CI and jyviko sign-off remain separate.
Co-Authored-By: Christian Svensson <blue@cmd.nu>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Codex <noreply@openai.com>
* GPU path tracing: use compatible Vulkan adapters (#3306)
Land the final Vulkan-only path-executor policy after #2191 under the #5493 constituent route. Fixes#3217. Fresh quick run 40012565783 passed. Preserve adapter/software policy and CPU fallback; no DXC dependency or workflow changes. Full native/hardware acceptance and final jyviko sign-off remain separate.
Co-Authored-By: Christian Svensson <blue@cmd.nu>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(material): initialize optional cards in upstream density regression
Fix the E0063 found by quick run 40015318093 while combining main #4785 with render #1060. The new upstream fixture explicitly has no engineering card. Keep every density/name/default-material assertion and production implementation unchanged.
* feat(render): expose independent anisotropic GGX core (#1694) (#1699)
Extract the original numerical source and analytic tests from published #1698 at 463a3e0d into its existing #1699 constituent. Keep current GPU/shared jobs, format and kernel untouched. Add scoped numerical-contract documentation. Full render library 158 passed, strict scoped Clippy and wasm pass, as do assets/layers/lists/format; source tree pinned in the acceptance record. No runtime consumer or final combined renderer acceptance is claimed.
* Sync current main into render, preserving release and viewport changes (#5705)
* Regenerate THIRD-PARTY-LICENSES.txt and docs/features.md [skip ci]
* Viewport: whole-body highlights reuse the model's own triangles (#4554) (#5109)
Hovering or selecting whole bodies -- a browser row, a History group, a body
under the pointer in body-pick mode -- built a CPU copy of every body's
triangles into the highlight scene. On a 40-body 4.3 MB STEP assembly that was
147 bodies, 70 MB of triangle copies and a 519 ms frame.
The model already has the same geometry on the GPU. The highlight now names it
and the renderer draws it again in the highlight colour:
- bodies drawn from a display plan: the model's evaluated plan chunks, with a
hover/selection tint uniform (the chunks stay resident in the model scene);
- bodies drawn as placements of shared triangles: the same placements of the
model's shared triangles, coloured per instance;
- bodies the model builds on the CPU: their vertex range in the model scene's
block.
See-through and faded bodies keep the CPU feedback, which colours them; faces
and edges highlighted on their own are unchanged. The highlight looks the same
(verified side by side).
On the assembly the highlight build drops from 519 ms to 9 ms and no longer
copies any triangles; selecting the whole assembly takes 9-20 ms.
Co-authored-by: Claude Code <noreply@anthropic.com>
* Offer to stand up SolidWorks files instead of turning them silently; add Turn Up Axis to the component menu (#5655)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* Sketch on a picked origin plane uses the plane shown after standing an import up (#5659)
* Sketch on a picked origin plane uses the plane shown, also in a moved component
After a STEP file opens its component stays the active one, and standing it up
turns that component a quarter about X. Starting a sketch on XY picked in the
viewport then sent `{"plane": "XY"}`, which names the active component's own
XY: upright after the stand-up. The sketch landed on the world XZ, Look At
turned to face Y and the extrude went along Y.
The viewport and browser only show the design's origin planes, so a picked
origin plane now goes to sketch.create written out as that world plane while
the active component is moved; sketch.create maps it into the component's
frame once. The flip toward the viewer uses the same plane.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Stand-up axes tests: stand the file up through the offer when one is waiting
#5655 offers to stand up SolidWorks files instead of turning them silently.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* gpu tests: give the two test frames the new hover and selection tints (#5698)
#5109 added Frame::hover_tint and selection_tint; two test frames that landed alongside it didn't set them, so solidcraft-gpu's tests stopped compiling on main.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Release 0.21.0-rc.2 (#5691)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jackson Oursland <jacob.oursland@gmail.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
* GPU: preserve error scopes on cancelled preparation (#5722) (#5723)
* claim: #5722
* fix(gpu): unwind preparation error scopes on cancellation (#5722)
Move fallible CPU HDR packing before GPU scopes and drain the remaining scope stack in reverse order before propagating denoiser/resource allocation failures. Native regressions cover cancellation and allocation-error cleanup plus subsequent scope reuse. Exact candidate Vulkan-required GPU suite: 101 passed, 0 failed, 3 existing ignored; strict GPU all-target Clippy and repository assets/layers/lists/container checks pass. No driver exception, shader transport or test limit is suppressed.
* library: transport PBR and engineering cards without lossy copies (#1086)
* wip(library): reconcile exact rich-catalog replacement semantics (#1062)
Source-only scoped extraction from original 0f801962 / #1086 on the refreshed #1060 card prerequisite. Preserve current asynchronous picker guards and source-ID handles. Scalar v1 and rich-v2-without-PBR clear existing PBR; supplied rich PBR is retained. No new saved concept or duplicate card descriptor. Formatting and source review completed; compile/native acceptance is still pending.
* test(library): preserve explicit downgrade and fallible UI regressions (#1062)
Request scalar v1 explicitly in the card downgrade refusal regression; rich v2 remains the default lossless writer. Propagate fallible harness actions with ? in the shared clicked-catalog helper, preserving all assertions. No production code, limits or ignore changes. Fresh grouped IO/engine/UI tests, scoped strict Clippy and WASM pass on this exact tree.
* Render: defer path-host startup until first use (#5742) (#5743)
* claim: #5742
* fix(render): defer GPU path initialization until first use (#5742)
Retain the selected adapter factory during Design/safe-mode startup; instantiate once on first Render entry, including late viewport installation. Preserve the deferred/preparing/ready/failed state across document changes and retain selected fallback adapter policy. Exact tree passes 32 render-workspace and 1966 full UI tests (0 failures; 7 existing ignored), strict native all-target Clippy, wasm check and repository gates. Three wasm warnings remain in untouched upstream files. Fresh native desktop acceptance is separate and in progress; no whole-renderer/main sign-off implied.
* GPU: adapt dispatches and retire lost devices (#5754) (#5755)
* claim: #5754
* fix(gpu): adapt dispatch size and retire lost path devices (#5754)
Start with a 64-item quantum and adapt bounded work toward 20 ms from observed completion, including denoiser tiles. Never exceed 16384 work items; hardware-specific shader cost means this is not a universal TDR guarantee. Publish device loss/uncaptured failure/poll failure through live executor availability without reinitializing automatically. Source-pinned required-Vulkan GPU suite: 105 passed, zero failed, three existing ignores, including actual device.destroy retirement and session availability. Strict GPU/engine all-target Clippy passes. Broader engine and repository/WASM continuation remains separately recorded. No geometry baseline, CI rule or published format history change.
* fix(render): wrap complete backend diagnostics (#5714) (#5715)
Preserve full backend/job/decoded-image diagnostics in the existing shared panel without ellipsis. The painted-galley regression covers Floating/Compact and light/dark, complete text, wrapping and fixed panel width. Exact-tree Render UI 33/0, strict UI Clippy, WASM and repository gates pass. Real safe-mode window captures, camera restore, CPU export and normal exit are recorded; no backend behavior or unrelated UI policy changes.
* Sync pinned main into render (#5869)
* Viewport: show reference geometry through bodies (Preferences, off by default) (#2697)
A new Graphics preference, "Show reference geometry through bodies", draws
inactive sketch contours, origin and construction planes and their axes over
shaded bodies. It is off by default, so the viewport looks as before: reference
geometry is depth-tested and a section leaves it whole (#1229); Create Sketch
and dialogs picking a plane still draw the planes through the model.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* ui-egui tests: offscreen GPU tests start their devices one at a time (#3989) (#3990)
* claim: #3989
* ui-egui tests: offscreen GPU tests start their devices one at a time
Every offscreen test started a wgpu instance of its own, and starting one
makes the Vulkan loader scan its drivers. With several test threads two
tests doing that at once crashed the test process with SIGSEGV inside the
loader (loader_icd_scan). offscreen_device now holds a process-wide lock
while it starts the instance, adapter and device; each test keeps its own
device and the rendering still runs side by side.
* DXF: write and read a Bézier at its own degree (#5269) (#5281)
* claim: #5269
* DXF: write and read a Bézier at its own degree (#5269)
The DXF writer and reader held a spline's degree to 7, the value MAX_BEZIER_DEGREE had before
#1642 raised it to 9. A G4 blend (one Bézier of degree 9) was written as a degree-7 SPLINE over
its first 8 control points, so a flat pattern's cut-out stopped 4.444 mm short in the file.
- seg_shape takes its limit from MAX_BEZIER_DEGREE: a Bézier is written whole, at its own degree.
- read_dxf reads SPLINE degrees 1 to MAX_BEZIER_DEGREE, the degrees a sketch spline takes.
- Tests: every Bézier degree 4 to 9, a degree-9 round trip, the reader's degrees, hostile SPLINE
degrees and too few control points, the G4 cut-out exported and read back with its areas, and a
degree-9 SPLINE with 2 to 10 points inserted into a sketch.
Fixes#5269
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Renaming a plane keeps its projections, canvases and hidden state (#4353) (#4354)
* claim: #4353
* Renaming a plane keeps its projections, canvases and hidden state (#4353)
timeline.rename only renamed the feature in other features' references. A sketch's
projection of a construction plane, a canvas placed on it and the hidden list keep it
by name too, so they lost it: the sketch warned that the plane no longer exists, the
canvas left the view and a hidden plane showed again. Document::rename_feature_keys
renames it in sketch links and canvases, as rename_body_keys does for bodies; the
selection follows in the command, and the hidden list in Session::refresh_from, so a
hidden plane stays hidden through undo and redo too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Browser: add Rename and Delete to feature menus (#3839, #4343) (#4342)
* claim: #3839
* fix(ui): add Rename and Delete to browser feature menus
* Browser: a second click on a Document Settings entry closes its window (#4150) (#4152)
* claim: #4150
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Browser: a second click on a Document Settings entry closes its window (#4150)
Each entry under Document Settings only opened its window. A second click on
Units closed the menu on the press (the row is outside it) and opened it again
on the release; Precision left Preferences open; Parameters and Design
Properties came back as a fresh copy. Now a click on an entry whose window is
open closes it the way the window's own x does:
- Units: context_menu::toggle_for with the row as the opener, the fix#1522
made for the workspace button's menu. A right-click still opens it at the
pointer.
- Precision: prefs::close(app, true), the Cancel/x path that puts back what
changed since the window opened (now pub(crate)).
- Parameters and Design Properties: the window is dropped, as its x does. A
Parameters value typed but not applied yet is dropped with it, as today's
second click already drops it.
Fixes#4150
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Preserve expression-positioned sketch fillet corners (#4097) (#4106)
* claim: #4097
* Sketch fillet keeps a corner placed by an expression (#4097)
Filleting a corner whose position comes from parameter expressions deleted
the point and its expressions, so the sketch gained two degrees of freedom
and later parameter changes distorted the outline. keep_corner now also
keeps fixed or expression-positioned points as virtual sharp corners.
* Fix feature editor picks after upstream sketch resizing (#4083) (#4084)
* claim: #4083
* Feature editors refill named edges and faces where they are now (#4083)
After an upstream sketch change, Fillet, Chamfer, Shell and Draft evaluated
correctly, but their editors refilled the originally clicked points and
could then fail with "no edge at ...". Resolve the stored names through the
document's naming helpers first, keep the resolved index, and map the pick
into the displayed body's frame; the point lookup stays as the fallback.
* ui-egui tests: clear the texture deltas of the frames they run (#4039)
Nine tests dropped an egui frame's output with its texture delta still
unapplied (the expression completion editor's frames, the slot preview's
three, the browser's feature rows). epaint asserts against that in debug
builds, so they failed under the dev profile and passed only where debug
assertions are off. They clear the delta as the other tests do; one more
hover test that dropped its output the same way does too.
* Frames: profile library, members along frame sketch lines, and the Frame Member dialog (#4003) (#4004)
* claim: #4003
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Frames: the profile library (#4003)
The first slice of frames and weldments: the cross-sections structural members
will be made of, each SolidCraft's own parametric definition built in code
(no vendor or standard size tables are shipped).
- solidcraft_doc::frame: bar, round bar, rectangular and round tube, angle,
channel, I-beam and a generic T-slot extrusion as exact line/arc regions,
with defaults worked out from the profile's size, refusals that name the
dimension that does not fit, anchors and placement, and exact section
properties (area, centroid, Ixx/Iyy/Ixy by Green's theorem).
- frame.profile.list, frame.profile.properties (with mass per metre for a
density) and frame.profile.insert (draws a profile into a sketch, locked by
default, at a point, anchor and angle).
- Tests against hand-computed values (rectangles, circles, composite
rectangles by the parallel-axis theorem, fillet spandrels), hostile input,
extrusion volume, undo; catalog entries in every locale; docs/frames.md;
ROADMAP M15.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Frames: drop the built-in T-slot profile (#4003)
Aluminium extrusions and other vendor sections come from the user's own
sketch (drawn or inserted from the vendor's DXF), so the library keeps the
plain steel shapes only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Frames: members along frame sketch lines (#4003)
frame.member lays one body per straight line of a frame sketch (2D lines or
3D sketch lines), its cross-section square to the line from end to end:
- profile: a built-in profile, or profile_sketch, a sketch whose material
(nested loops alternating, so two circles make a tube) is the section in
its own x and y around its origin; profiles picks override it.
- anchor (origin, centre, centroid or a bounding-box corner or edge middle),
angle (an angle expression, so a parameter), mirror.
- Orientation: the profile's y as near world up as the line allows, x
square to both; a vertical member's x is world X (frame::member_axes).
- Members follow sketch and parameter edits, undo as one step, save and
reopen (format: pending/frame-members.rs), and are named after the profile
(Rect Tube 40x40x3, (2)…), numbering on past names already taken.
Tests against hand-computed volumes and bounding boxes (rectangle, 3D
tower lines, parameter edit, sketch tube profile and picks, anchor/angle/
mirror), refusals, undo/redo and reopen; walk fixtures and goldens; docs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Frames: fixes from the adversarial review (#4003)
- Default member lines are what was drawn: projected and other linked curves
and 3D wires (a face sketch's edges) and lines of no length are left out.
- A profile sketch's material starts from its drawn profiles, as
solid.extrude's "all" does, so a section sketched on a face works; the
even-odd test uses outlines and boxes worked out once, capped at 256.
- Member names are pinned to their lines (`names`, by line id; `body_name`
is the base): deleting or adding a line, or deleting another feature, no
longer renames other members, and frame.member stores every name it gives.
body.rename renames a member on its line (also for later member features);
a copy whose body_names were set still names its members by position.
- Built-in profile dimensions are expressions (ProfileSpec): they follow
parameters, bare numbers take the design's units, and they are feature
inputs. frame.profile.properties/insert read them the same way.
- Default root/toe radii shrink to the room a thick section leaves; radii
given are checked by name; a radius must be 0 or at least 0.01 mm; error
messages report the wall or flange value given, not twice it.
- A line within 1e-6 rad of vertical orients as vertical, so rounding never
turns a post a quarter turn.
- A 2D frame sketch in a differently placed component is mapped into the
member's frame, and member features recompute when placements change.
- Caps before lookups for a design's `lines`; a line listed twice is
refused; a 3D wire that runs back on itself is not a line.
Regression tests for each, from the review's scenarios.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Frames: the Frame Member dialog on the ribbon (#4003)
- Frame Member (MODEL > Create) opens a dialog: the frame sketch's lines
picked in the view (or All Lines), a built-in profile and its dimensions or
a section picked in another sketch, anchor, angle, mirror and name, with a
live preview. Edit Feature reopens it filled in.
- Lines picked in the view keep their sketch (a SKETCH_LINES input picks
ExternalSketchCurve): line ids are each sketch's own, so two 3D sketches
both have a `w2`, and the members went on the newer sketch's lines.
- timeline.redefine takes frame.member. Members keep their names through an
edit unless it gives them a new body_name; frame.member takes `names`.
- A member whose section is a whole profile sketch keeps it when edited
(the dialog shows the sketch instead of picking every region, which would
fill a tube's hole).
- An original frame_member icon, open for review, on the ribbon, timeline
and browser.
- The angle profile is listed as L-Angle, and web and flange as Web/Flange
thickness, so they translate apart from the existing Angle, Web and Flange;
the dialog's labels are in en, ja and zh. en-GB no longer repeats the frame
command labels it spells the same.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Rectangular and circular pattern: copies that miss the body are reported (#3042) (#3919)
* claim: #3042
* Warn when cut pattern copies remove no material
A rectangular or circular pattern whose copies miss the body succeeded silently and those copies did nothing. The pattern now reports how many of its cut copies removed no material, and keeps the copies that land.
* Delete in a rolled-back history warns about the features after the marker (#3471) (#3698)
* claim: #3471
* Delete in a rolled-back history warns about the features after the marker (#3471)
With the timeline rolled back, the checks a delete runs first (the
confirmation's "these features will fail", timeline.dependents and
selection.delete's would_fail) compared models evaluated only up to the
marker. Features after it, broken by the delete, were not listed, so their
error marks turned up only once the timeline moved to the end.
newly_broken_in_history evaluates both sides over the whole history when
either has a marker (on copies; the model and document are unchanged) and is
used by the delete's report and by timeline.dependents.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* macOS: Fusion navigation preset does not support Shift + two-finger orbit (#3446)
* Macos: keep touchpad/key modifiers true to fusion
* fix: remove obsolete orbit snap reset from trackpad navigation
Upstream removed ViewportState::orbit_snap. The new trackpad orbit path retained one reference, causing E0609 in the PR merge build.
Validation: all seven trackpad tests pass; cargo check -p solidcraft-ui-egui --all-targets passes on the exact failing CI merge 66a2791b with this fix applied; rustfmt and diff checks pass.
Co-Authored-By: GitHub Copilot <noreply@github.com>
* fix: retain Fusion trackpad help in refactored preferences
Preserve upstream's new preferences pages and apply the translated Fusion trackpad tooltip in prefs/navigation.rs.
Validation: cargo build -p solidcraft passed. Tests and full CI were not run, as requested.
Co-Authored-By: GitHub Copilot <noreply@github.com>
* style: format Fusion trackpad preferences tooltip
Apply rustfmt's required wrapping for the relocated tooltip. No behavior changes.
Validation: cargo fmt --all -- --check and git diff --check passed.
Co-Authored-By: GitHub Copilot <noreply@github.com>
---------
Co-authored-by: GitHub Copilot <noreply@github.com>
* 2-point circle takes a typed diameter (#3278) (#3335)
* claim: #3278
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* 2-point circle takes a typed diameter (#3278)
After its first point the 2-point circle tool showed no value box, so the
circle could only be sized with the cursor. It now shows a Diameter box,
like the centre circle: the typed value puts the second point that far
along the cursor's direction, and Enter draws the circle.
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>
* Reorder design tabs by dragging them (#3187) (#3188)
* claim: #3187
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Reorder design tabs by dragging them (#3187)
A tab dragged along the strip follows the pointer while the others make
room, and lands where it is released; Escape, or releasing down the
window, leaves the order as it was. With the custom title bar a drag that
starts mostly up or down (or on the only tab) still moves the window.
The control channel's ui.documents gains a move action (from, to).
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>
* Renaming a body keeps the evaluated timeline, off the window thread (#2467) (#2468)
* Renaming a body that later features change evaluates nothing (#2467)
Model::rename_body now keeps the results when later features change the
body (a pattern, a chamfer, a join). It still evaluates when a body of
that name comes and goes or isn't the same body throughout, when a later
body's name is worked out from it (X (2)), or when a message quotes it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Rename box: rename off the window thread, and locked bodies follow a rename (#2467)
The rename box commits body.rename through run_heavy, so a rename that
still evaluates later features (one names the body in its targets)
computes beside the window instead of freezing it. Locked bodies now
follow a body.rename from anywhere (the rename box, the control channel,
scripts), not only the rename box.
---------
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Toolbar: a constraint dragged out of the small box becomes a full button (#5254) (#5456)
* claim: #5254
* Toolbar: a constraint dragged out of the small box becomes a full button (#5254)
The Constrain panel's small box of constraint icons was a fixed list, so
dragging one onto the full-size buttons saved a new order but it was still
drawn in the box. The toolbar settings now remember, per tab, which box
commands were moved out; those are drawn as full buttons, the box closes
up, and dropping one back on a box icon returns it. Reset Toolbar restores
the default box, and settings saved before this load unchanged.
* Sketch offset keeps its source lines free to turn (#4464) (#4466)
* claim: #4464
* Sketch offset keeps its source lines free to turn (#4464)
An offset adds a Parallel between each source line and its offset line.
Since offsets became associative, the offset's points are held during a
solve, so each Parallel pinned its source line to the offset line's
current direction: a closed chain lost its turning freedom (8 degrees of
freedom became 6), and Horizontal or an angle edit on a source line was
refused as conflicting.
The solver and the constructive reading now leave out a Parallel that
ties an offset line to the source it follows while the association lives;
the association already keeps them parallel. After Break the stored
Parallel drives again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Flat Pattern view draws perforations (#4440) (#4441)
* claim: #4440
* Flat Pattern view: draw perforation holes (#4440)
The flat view drew the outline, cut-outs, pockets and bend lines but not
perforation fields, though the flat-pattern data and the DXF export list
their holes. The view now reads the hole centres from the sheet through
SheetBody::perforation_holes, caches them per model state and body, and
draws them as circles in the cut-out style. Holes outside the viewport or
smaller than one pixel are skipped, and the cache is capped at one million
holes.
* "Don't Save" looks like a button in the Save changes prompt (#4380) (#4400)
* claim: #4380
* "Don't Save" looks like a button in the Save changes prompt (#4380)
The prompt's third action used the kit's low-emphasis text style, so it
didn't read as clickable and people thought Cancel was the only way out.
Add an opt-in secondary_left style to the confirm footer (a framed
secondary button from the theme tokens) and use it for Don't Save.
* Save prompt test: clear each frame's texture delta once it is applied
egui 0.36 panics when a TexturesDelta with unapplied deltas is dropped;
the test kept only the last frame's output and dropped the earlier ones.
* 3D mouse test: clear the frame's texture changes, so it passes in debug builds (#4382)
each_frame_hands_the_view_its_motion_through_the_drivers_mapping dropped
the output of a frame it ran (`let _ = ctx.run_ui(...)`). epaint panics
in debug builds when a TexturesDelta is dropped with deltas left ("Dropped
TexturesDelta with 1 unapplied deltas"), so the test failed under a plain
`cargo test` on every platform while the ci profile passed. It now clears
them, as the other UI tests do since #60.
Co-authored-by: Tyler Chiriboga <18270301+Tyler-Chiriboga@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Spanish (es-ES): catch up with en.json (#4292)
Co-authored-by: carlymx <24634598+carlymx@users.noreply.github.com>
* Esc ends the navigation bar's Pan, Orbit and Zoom modes (#4226) (#4233)
* claim: #4226
* Esc ends the navigation bar's Pan, Orbit and Zoom modes (#4226)
The nav bar toggled a mode on, but nothing cleared it on Esc, so a left drag
kept panning, orbiting or zooming. Esc now ends an active nav mode first;
the next Esc reaches the tool or dialog as before.
* Keep long Select Other labels inside the popup (#4209) (#4225)
* claim: #4209
* fix(ui): bound Select Other labels and expose full names (#4209)
* Deleting an inserted file removes its components, placements and joints (#4100) (#4113)
* claim: #4100
* Deleting an inserted file removes its components, placements and joints (#4100)
A STEP or mesh file inserted into a design makes a component tree. Deleting
its feature left that tree behind: the components, their occurrences, the
rigid joints that hold them and the bodies' component assignments. A later
body that took an imported body's name was then placed in the stale component.
Document::remove_component_tree removes a component subtree and everything
that refers to it: the features made inside it, its occurrences, the joints
on them with their motion links, joint origins, contact set members, motion
study keys and exploded moves, body component and offset entries, standard
part and link records, and configuration columns. Deleting an inserted
file's feature and component.delete both use it; component.delete now also
removes joints left pointing at a deleted occurrence. The body transfer
refusal moves with it, unchanged.
* Only an inserted file's feature takes its component tree when deleted
Mark the feature a STEP, IGES or mesh insert makes (`inserted`), and remove
the component tree on delete only for those and for a linked design's
feature. Deleting a copied component's independent base deletes just that
feature, as before. Older designs get the mark as they open (pending format
change), except copied components' independent bases.
* Rigid joint saved with the wrong occurrence when two occurrences share a component (#3857) (#3939)
* claim: #3857
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Joint dialog: a body of a further instance picks that occurrence, not the component's first (#3857)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* Cancel Sketch: leave a sketch without keeping its changes (#3838) (#3938)
* claim: #3838
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SBN24y2TcqRcm2zn8Gkj27
* Cancel Sketch: leave a sketch without keeping its changes (#3838)
sketch.cancel (Cancel Sketch, ribbon Exit group and the sketch's
right-click menu) puts the design back as it was when the open sketch
was created or opened for editing: a new sketch is removed with its
dimension parameters, an edited one returns to how it was. It is one
undo step, so Undo goes back into the sketch.
The session remembers the design and the undo history at the point a
sketch is entered. Cancel uses that only while the history still runs
through it (the same steps before, every step since taken in this
sketch); otherwise it refuses and points to Undo, so a stale entry can
never throw away later work.
The icon is the approved close glyph in the sketch accent (alias
sketch_cancel); ja and zh carry the new label.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SBN24y2TcqRcm2zn8Gkj27
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* 3MF import: name a Bambu Studio project's bodies after its objects (#3858) (#3859)
* claim: #3858 (3MF import: names from a Bambu Studio project)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* 3MF import: name a Bambu Studio project's bodies after its objects (#3858)
Bambu Studio leaves the name off the root model's objects and keeps it in
Metadata/model_settings.config (<config><object id="N"><metadata key="name">),
so every object of a project came in as Mesh1, Mesh2, ... A build item whose
root object has no name of its own now takes the object's name from those
settings. A name in the model still comes first, and part names, other
settings and objects of other model parts are left alone.
The settings are read only when a body needs a name, within what the model
parts left of the 1 GB budget. Settings that can't be read (malformed, nested
too deeply, too large) never fail the import: the bodies keep the generic
names, with a warning saying why.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Tyler Chiriboga <18270301+Tyler-Chiriboga@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Viewport: right-click keeps a multi-selection of faces and edges (#3733) (#3788)
* claim: #3733
* Viewport: right-click keeps a multi-selection of faces and edges (#3733)
---------
Co-authored-by: AhmetV <344941595+ahmetv10@users.noreply.github.com>
* Parameter import and image pickers offer their own file types (#3216) (#3314)
* claim: #3216
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Parameter import and canvas image pickers offer their file types (#3216)
Both opened the picker with no file type of their own, so it listed only
designs, CAD files and meshes (and All files). macOS lets you pick only
the types a filter names: CSV parameters, and PNG or JPEG images for a
canvas, were greyed out. They now ask for csv/json and png/jpg/jpeg,
which the picker lists first (#1801).
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>
* Return to the originating tab after finishing a sketch (#3250) (#3264)
* claim: #3250
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Return to the originating toolbar tab after finishing a sketch (#3250)
A sketch started from SHEET METAL or PLASTIC always ended on the SOLID
tab. Remember the tab that was active when the sketch began
(UiState::sketch_return_tab, not saved) and go back to it when the
sketch ends. Falls back to SOLID when that is not one of the normal
toolbar tabs. A sketch started from SOLID behaves as before.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Test: no one-element loop in the return-tab test (clippy single_element_loop)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* STEP import: let a cancelled import release the import slot (#3171) (#3189)
* claim: #3171
* Release cancelled file import slots (#3171)
* Dragging a part along its joint no longer highlights what's under the pointer (#5137) (#5140)
* claim: #5137
* Dragging a part along its joint no longer highlights what's under the pointer (#5137)
The viewport's hover pick was skipped during a box selection and camera
moves, but not while a jointed part was dragged, so faces and edges under
the pointer lit up on every frame of the drag. The pick now waits until
the joint drag ends.
* Browser: a body picked in the viewport is opened to and scrolled to (#3005) (#3081)
* claim: #3005
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Browser: a body picked in the viewport is shown in the Browser (#3005)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Merge main; viewport::select takes a toggle flag now
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>
* Line tool snaps a diameter through the circle centre (#4326) (#4327)
A line drawn across a circle only snapped its end onto the curve, so a
diameter could miss the centre at any angle. Near the centre the line now
locks onto the radial line: on the rim it stays on the circle and the centre
is kept on the line, and on the centre point the end lands there.
* Flatpak: find the host's fonts for CJK text and the font list (#5124) (#5127)
* claim: #5124
* Flatpak: find the host's fonts for CJK text and the font list (#5124)
Inside the Flatpak sandbox the host's fonts are mounted at /run/host/fonts
and /run/host/user-fonts, which no font search looked at. Japanese and
Chinese text fell back to boxes, and the font list missed the host's
families.
- Search both folders for the CJK and Chinese fallbacks and for the
installed-font list.
- The CJK fallback reads SOLIDCRAFT_FONT_DIRS (a path list, as the font
list already does), with CRAFT_FONTS_DIR kept as a fallback.
* Crash reports: keep component.activate in the journal (#5079) (#5121)
* claim: #5079
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Crash journal keeps component.activate (#5079)
Activating a component changes only the session's active component and
occurrence path, not the design, so the journal dropped the command and a
crash report's replay built later parts under the previously active
component. The journal now also keeps a command that changes either.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Control channel and MCP on by default, with a setting to turn it off (#5561)
The app opens its localhost control channel on port 27878 (or the next free
one) unless Preferences > Assistant turns it off; --control and
SOLIDCRAFT_CONTROL_PORT still win. A current.port discovery file lets
solidcraft-cli mcp find the running app with no arguments.
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* Tests: moving an occurrence recomputes only the cuts and joins it touches (#4965) (#4982)
* claim: #4965
* Joins and cuts follow only the placements they depend on (#4965)
Tests that turning a joint between parts cuts no hole again, and that a
tool cutting another part cuts again when that part moves relative to it,
while an unrelated part moving keeps the result.
Booleans and Combine now hash only the placements of the bodies they take
as inputs, relative to the frame that evaluates them (#3397), so these
pin that behaviour for joins and cuts with named targets across parts.
* fix(ui): preview shortcut chords and wait for complete modifier input (#1810)
Keep physical modifier events out of the ordinary-key slot, preview held keys immediately, and confirm after release and a one-second settling interval. Preserve legacy control-channel modifier behavior while allowing explicit physical Control/Command input. Add regression coverage, a UI scenario, localized guidance and documentation.
Fixes#1809.
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* Move: scope box selection to the active component (#4459) (#4527)
* claim: #4459
* fix: scope Move box selection to active component (#4459)
* Delete removes a selected construction axis (#4692) (#4695)
* claim: #4692
* Delete removes a selected construction axis (#4692)
Delete on a construction axis skipped it with "the origin axes can't be
deleted". It now deletes the construction axis feature, like construction
planes; the origin axes X, Y and Z are still refused.
* Agent chat: end the agent's process group where `kill` is busybox' (#4587) (#4588)
* claim: #4587
* Agent chat: end the agent's process group with the system call (#4587)
The group was signalled with a `kill` program: `kill -s TERM -- -<pgid>`.
That works with procps' `kill` and with BSD's, but busybox' `kill` takes no
`--`: it prints its usage, exits 1 and signals nothing. The agent's helpers
(the CLI an adapter wraps, the MCP servers it starts) then stay alive after
the agent is ended.
The group is signalled with the system call instead, so no `kill` program and
no shell are needed and the behaviour is the same everywhere. `nix` (MIT) is
already in the tree through zip, and only on Unix: the browser build does not
take it.
The two tests that cover this now pass where they failed before:
`agent_chat::process::tests::dropping_the_process_ends_its_group` and
`agent_chat::process::tests::stopping_the_agent_leaves_no_helpers_running`.
* Extrude: picking a face, vertex or parallel plane off the profile switches the extent to To Object (#2855)
* Extrude: picking a face, vertex or parallel plane off the profile turns the extent to To Object
With profiles picked, a face off their plane (or curved), a vertex off it, or a
construction plane parallel to it is taken as the To Object target instead of
another profile: the extent switches without the Extent Type choice, and the
distance shows how far the target is. Coplanar faces still join the profiles.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Extrude: automatic To Object is a Preferences setting, off by default
Preferences > Design > "Extrude to a picked target" (`extrude_auto_to_object`, default
false) turns the automatic To Object on. Off, picks in the Extrude dialog only add
profiles and the distance is not synced to a target, as before this change.
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
* Regenerate THIRD-PARTY-LICENSES.txt and docs/features.md [skip ci]
* FreeCAD/Rhino import: decode caps with room for detailed parts (#1783) (#3209)
* claim: #1783
* FreeCAD/Rhino import: decode caps with room for detailed parts (#1783)
cadmpeg decoded FreeCAD and Rhino files under its service profile, whose
million collection items and 2.56e8 work units a detailed part (a sprocket,
a threaded rod, a part with a few hundred faces) runs past: the import was
refused with "resource limit on CollectionItems: BudgetExceeded". Allow
8 million items and 2e9 work units, half of cadmpeg's desktop profile; the
input-size cap stays SolidCraft's STEP limit and every other cap stays at
the service profile's.
decoding_is_capped checks the two raised caps (and that they stay below the
desktop profile's) and that every other cap is still the service one.
Fixes#1783
* A spherical shell revolves into a solid with a void, and cuts and joins with hollow tools work (#451) (#4182)
* Assembly: ground the first part; drag free joint chains (#2804) (#2805)
* assembly(feat): ground first parts and drag free joint chains
Ground the first top-level part when it is created and let face dragging reposition free occurrences or any member of a floating joint chain in one undo step. Preserve exact repeated-placement frames, joint values, modifier selection and Escape cancellation. Keep the accumulated fixture, viewport layout and documentation integration fixes from the existing PR.
Signed-off-by: Karl Von Richter<karl@embeint.com>
* assembly(fix): update movable fixtures for first-part grounding
Accept the documented grounded override in component creation, explicitly free movement fixtures, and keep linked clipboard pastes movable even in an empty document. Cover first and reused linked pastes, default insertion grounding, and single-step undo in the existing link tests.
Signed-off-by: Karl Von Richter<karl@embeint.com>
* assembly(test): keep the motion-cost crank free
Unground the first component in the document-level joint-face measurement fixture so its motion samples can rotate after first-part grounding. Preserve the rotation and shared-measurement assertions that caught the Linux CI failure.
Signed-off-by: Karl Von Richter<karl@embeint.com>
* assembly(test): free the frame placement fixture
Create the frame-member placement fixture ungrounded before moving it above its root sketch. Keep its world-space member assertions while respecting first-part grounding in the frame tests added on main.
Signed-off-by: Karl Von Richter<karl@embeint.com>
---------
Signed-off-by: Karl Von Richter<karl@embeint.com>
* UI: integrate theme radii, borders, palettes and viewport refresh (#5045) (#5046)
* claim: #4922
* UI: let themes own corner radii
* UI: use four-point panel and window radii in SolidCraft 95
* UI: align the 95 radius assertion with four-point panels
* claim: #4953
* UI: let themes own structural and control borders
* UI: single Blueprint outlines and dotted secondary rules
* UI: separate container outlines and add theme-owned 95 bevels
* claim: #5007
* UI: apply theme palettes to icons, workspace tabs and Gruvbox settings
* claim: #5045
* Viewport: invalidate cached geometry colours when the theme palette changes
* UI: preserve legacy theme logo colours with the borderless mark
* Section Analysis: OK with no plane picked turns the section off (#2410) (#2411)
While a section is on, Section Analysis opened with no plane picked now
says "No plane picked: OK turns the section off." and OK runs
inspect.section {"clear": true}. With no section on, OK with no plane
still asks for a plane.
Fixes#2410.
* Regenerate THIRD-PARTY-LICENSES.txt and docs/features.md [skip ci]
* Close Change Parameters with Escape (#3372) (#3399)
* claim: #3372
* Fix Escape cancellation in Change Parameters
Consume Escape before parameter editors can commit pending text on focus loss, while preserving autocomplete and shortcut capture priority. Add 11 UI regressions covering draft cancellation, preview cleanup, document and history preservation, and reopening saved values.
* Preserve construction references when axes and points are renamed (#4419) (#4449)
* claim: #4419
* fix: preserve renamed construction references (#4419)
* Browser: route body picks into open command dialogs (#2560) (#4371)
* claim: #2560
* fix(ui): route Browser body picks into active dialogs
* Fix midpoint picks in sketch dimensions (#4872)
Retain MID snaps across both picks and materialize reusable midpoint
constraints for point-to-line and point-to-point dimensions. Let midpoint
pairs choose horizontal or vertical spans with pointer placement before
entering a value.
Cover projected model edges, overlapping geometry, helper reuse,
undo/cancel/reopen, and existing angular and line-circle workflows.
* Sketch slots: provide typed dimension inputs for every variant (#2423) (#4318)
* claim: #2423
* fix(sketch): add typed dimensions to every slot tool (#2423)
* Version 0.21.0 on main (released from rc.2's code) (#5774)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* docs(mcp): Windows setup for Claude Desktop (#3794)
* docs(mcp): Windows setup for Claude Desktop (paths, two servers, Store config file)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(mcp): say the Store build can keep the config elsewhere, not that the other path fails
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Regenerate THIRD-PARTY-LICENSES.txt and docs/features.md [skip ci]
* MCP: wait out long requests, honest batch outcomes, batch tool aliases (#2901)
Rebased onto main as one commit (was 120d26f7, df488f9a, db9d1073 plus
merges of main).
- A request the app reports as still running (#1618) is waited for on the
same connection through `control.pending`; past the wait the error says it
was not cancelled and will still apply (or only write its file), later calls
are not sent behind it, and the new read-only `pending` tool returns its
result once it lands.
- A failed batch says what really happened: `rolled_back` only when the
design is back where the batch started (as #3288 / #3242 on main: the app's
`rolled_back` is believed; for an older app, every completed step undone;
only a rollback asked for is reported as one), plus `applied`, `not_run` and
`failed.still_running`.
- A batch step may name a tool (set_parameter, list_commands,
describe_command, execute, measure, inspect_design, undo, redo, export, save,
open, new_design, crash_reports); screenshot, body_topology, batch and
pending are refused before anything is sent.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* 3D mouse: driver choice, guided calibration, button mapping and a picture of the device (#1463) (#3825)
* Add a small test boat to steer around with the 3D mouse
A little hull in its own camera, drawn by the UI itself and not the CAD
viewport, so a 3D mouse can be tried out without opening a design. It
brings the model (generated by assets/models/make_test_boat.py) and the
tests for how the camera moves it.
Nothing uses it yet; Preferences picks it up with the rest of the 3D mouse
work.
* UI kit: keep a choice row from flipping with the scrollbar
A choice row picks segments or a dropdown by whether its labels fit. A page
scrollbar that comes and goes narrows the row by its own width, so the row
could flip back and forth between the two on a page that sits right at the
edge. It is now judged on the whole row and through a small helper,
fits_steadily, that only changes its answer once the miss is wider than the
scrollbar.
Also tests for that, for typed numbers in a slider's value box, and a
test hook to put up the issue-report input barrier by hand.
* 3D mouse: device model, guided calibration, buttons and a picture
The 3D mouse becomes a device we know something about instead of six raw
numbers.
The reader picks between spacenavd and the direct hidraw driver, falls back
from one to the other, and says so when it can't open the device. Guided
calibration asks whether the device is a spring cap or a trackball, adds a
touch step, and uses a settle filter so a ball let go of stops reading as
motion. Buttons get jobs (keys, held modifiers, commands), saved per
device, with defaults for the SpaceMouse Pro.
Preferences gets a picture of the device that moves as it does, next to the
test boat, and the same picture can sit in the 3D view at Left Viewport,
Right Viewport or Left Window, above the floating History card like the
legend and readouts.
These ride together because the crate's calibration types, the Preferences
page, the Linux reader and the button hook all use each other.
* 3D mouse: offer to calibrate an uncalibrated device
When a 3D mouse shows up that has not been calibrated, a card in the 3D
view offers to calibrate it, to swap drivers when the other one would work
better, to close, or to never ask again. It is made once per device, goes
away after 30 seconds in view, and gives way to a status or computing
toast instead of stacking on it.
The card is the shared toast, which now takes buttons: they group by
purpose, wrap inside the card at narrow widths, and do nothing while an
issue report holds CAD input. The picture of the device in the 3D view
keeps clear of the card.
* 3D mouse: translations and attribution
The 3D mouse strings (ui.3d_mouse_*, the calibration steps and the offer
card) in every shipped language, the Japanese catalog note, and the credit
for the test boat model in ATTRIBUTION.md.
This is last because the code before it only looks keys up: it builds and
runs without these, falling back to English until they land.
* 3D mouse: draw its preferences with the settings kit like the rest of Navigation
The 3D mouse section was a folded header with its own layout. It now
uses the same kit as the sections above it: a settings_section card,
settings_form rows with the shared label column, dividers between the
device, axes, picture and buttons groups, Text::Hint for the helper
lines, and settings_control_width for the dropdowns.
The section is open like every other one (the header still folds it),
so the device badge on the folded header is gone; the calibration row
already names the device. Every setting, string and key is unchanged.
Tests that unfolded the section no longer click it, and the hint lines
are looked up as hints instead of notes.
* 3D mouse: show each axis's live tilt in Preferences, like main's axis rows
Each view axis row is main's axis row again: a meter that follows the tilt of the motion driving it, with the dead zone marked, next to Invert, Speed and the "driven by" menu. The meter reads the calibrated travel when the device has a calibration and the raw reading over full scale when it does not, and asks for a frame whenever the reading changes. The helpers are back as they are on main, and the calibration state is a badge by the enable toggle.
* 3D mouse: line its preferences up with the rest of Navigation
- The two checkboxes sit side by side in the same two-column grid as Wheel and trackpad.
- Row labels stop repeating "3D mouse" and use sentence case: Calibration, Driver, Restore defaults, Reset view, Left viewport, Right viewport, Left window.
- Hints line up under the control column instead of the left edge.
- The six view axes are named Pan X, Pan Y, Zoom, Orbit X, Orbit Y and Roll so they fit the name column.
- Buttons are a divider and rows in a form like the other groups, without the bold sub-heading.
- The driver, dead zone, axes, picture and buttons sit behind a collapsed Advanced section, since most people never touch them.
* 3D mouse: tidy the Advanced test for clippy
* Timeline: suppressing a feature keeps later features' body names (#4302) (#4311)
* claim: #4302
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Timeline: suppressing a feature keeps later features' body names (#4302)
Bodies numbered by the counter (`BodyN`) take the next free number on every
recompute, so suppressing a feature that made one moved every later counted
body down one (`Body2` became `Body1`) and broke what named it, such as a
Move of `Body2`. timeline.suppress now stores the current names of later
features' counted bodies in their `body_names` first, as body.rename already
does for a renamed counted body (#1109). Engine only: no evaluation or
design-format change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Timeline: a body made while a feature is suppressed keeps its name (#4302)
Suppressing a feature freed its counted name (`Body1`), so a body made
meanwhile took it and became `Body3` once the feature was back. Suppress
now also stores the suppressed feature's own counted names, and the
evaluator adds the names stored on earlier suppressed features to the
state's `reserved_body_names` (the names a rename freed, #4957), which the
`BodyN` counter passes by. They are taken from the design before each
feature is computed and are part of the state digest, so a cached result
is only reused when they match.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* cadmpeg 0.6.3: FreeCAD and Rhino import in the browser too (#4482)
cadmpeg 0.6.3 is pure Rust (ruzstd instead of the zstd C library, zip 9), so
solidcraft-io depends on it on every target and the wasm build reads .FCStd
and .3dm files like the desktop app. The browser's file pickers offer them.
0.6.3 replaces DecodeOptions::container_only with a decode depth; ask for
Geometry, what its STEP writer reads. zip 8 leaves the tree, and with it the
three cargo-deny advisory exceptions kept for it.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Pattern dialogs: Quantity fields get up/down step arrows (#4260) (#4264)
A new kit control, `ui_kit::count_field` / `count_row`, is an expression field for a
whole-number count with stacked step arrows at its right edge. An arrow steps the
evaluated count by one within the field's limits: a plain number is replaced, an
expression keeps its parameters and gains or adjusts a trailing `+ n`. The text
changes as typing changes it, so the live preview follows.
Used by the Quantity rows of the sketch Rectangular (1-500, both directions together
at most 500) and Circular (2-500) patterns, and the solid Rectangular, Circular and
Path patterns (1-1000).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Regenerate THIRD-PARTY-LICENSES.txt and docs/features.md [skip ci]
* Join: say so when the joined piece doesn't touch the body (#4434) (#4436)
* claim: #4434
* Join: say so when the joined piece doesn't touch the body (#4434)
A Join (extrude, revolve, sweep, loft, pipe, coil, primitives, thicken,
boundary fill, surface network) whose material meets none of its targets
still joins it to the first target as a separate piece. The result stays
as it was; the feature now warns: "the joined piece doesn't touch `Body1`,
so `Body1` is now in 2 separate pieces (New Body makes it a body of its
own)". History marks it and the command reply carries it.
The check compares pieces (outer shells, not voids) of the bodies the
feature changed or absorbed before and after it, so it holds for the
batched multi-profile path too, stays quiet for a join that bridges
pieces or closes a cavity, and leaves Combine (which joins the bodies it
is given) alone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Hole: snap to face and round-edge centres, drag placed holes, section in the dialog (#2171) (#2912)
Placing a Single hole put it exactly where the click landed, with nothing to line it up by and no
way to move it but taking it out and picking again; the dialog was a list of size fields.
- Preferences › Design › Create Hole has two settings, both off by default, so a new installation
places holes as before:
- Snap to face and edge centres (`hole_snaps`): the planar face under the cursor offers its
middle and the centres of its round edges (a boss, a rounded corner, a hole already there) as
marks; a pick near one, or on or beside such an edge, takes that centre and the mark is named.
Sketch points on the face still come first; Alt or Ctrl held places it where clicked.
- Move holes by clicking or dragging (`hole_moves`): each placed hole has a handle that drags it
(snapping when snaps are on) and takes it out when clicked, and a pick elsewhere on its face
moves it there. Edit Feature shows the handle too, so a hole can be moved later (#1047).
- The Hole dialog draws a section of the hole as its options make it, with each size in a field
beside the dimension it sets; a hovered or focused field lights its dimension. The layout is a
new kit element, `ui_kit::figure_rows` (a caller-painted figure beside captioned fields).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Hole: place a Single hole by distances from reference edges (#37) (#2931)
A Single hole can take up to two straight edges of its face as references,
each with the distance of the hole's centre from that edge's line, measured
in the face's plane. `solid.hole` takes them as `references: [{edge, distance}]`;
the edges are found again by their persistent names, else their points, so
the hole follows them when the part changes, and each distance is a
parameter. A bad reference (no edge there, a curved edge, one square to the
face, parallel edges, a negative distance, a centre off the face) is refused.
In the Hole dialog, Reference 1 and 2 follow the face, each with its Distance
row; the canvas shows each distance as a dimension from its edge to the hole,
with a box just past the edge to type it in. Picking an edge measures its
distance (and its box takes the keyboard), typing one moves the hole, and
moving the hole measures them again. Edit Feature picks the edges again with
their distances.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
---------
Signed-off-by: Karl Von Richter<karl@embeint.com>
Co-authored-by: Frank <58932224+plexi32@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: raykholo <raykholo@users.noreply.github.com>
Co-authored-by: CamWight <91156642+CamWight@users.noreply.github.com>
Co-authored-by: SS-EN <84559909+SS-EN@users.noreply.github.com>
Co-authored-by: Colin Faulkingham <colin.faulkingham@gmail.com>
Co-authored-by: Karl Von Richter <karl@von-richter.net>
Co-authored-by: HiroKenEz <59478073+HiroKenEz@users.noreply.github.com>
Co-authored-by: Peppe Bergqvist <p@bergqvi.st>
Co-authored-by: GitHub Copilot <noreply@github.com>
Co-authored-by: phanoob <lesur.Stephane@wanadoo.fr>
Co-authored-by: Phanoob <phanoob@gmail.com>
Co-authored-by: Kai Evans <nemshilov@gmail.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Tyler Chiriboga <tylerchiriboga.apps@gmail.com>
Co-authored-by: Tyler Chiriboga <18270301+Tyler-Chiriboga@users.noreply.github.com>
Co-authored-by: CaRLymx <carlymx@gmail.com>
Co-authored-by: carlymx <24634598+carlymx@users.noreply.github.com>
Co-authored-by: Loohi <78334654+Loohi@users.noreply.github.com>
Co-authored-by: leValdemar19 <valdemarnord@hotmail.com>
Co-authored-by: AhmetV <mihendiz10@gmail.com>
Co-authored-by: AhmetV <344941595+ahmetv10@users.noreply.github.com>
Co-authored-by: rlartot <94693724+rlartot@users.noreply.github.com>
Co-authored-by: Christian Svensson <blue@cmd.nu>
Co-authored-by: Thodoris Tsagkogeorgas <thodoristsagko@gmail.com>
Co-authored-by: ahmetalpcelik <80778880+ahmetalpcelik@users.noreply.github.com>
Co-authored-by: ealitt <ealitt@gmail.com>
Co-authored-by: Homero Thompson <vdirienzo@gmail.com>
Co-authored-by: Diogo Soares Rodrigues <dsrodrigues20@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Artem Sushchev <Suschev.Artem+github@gmail.com>
Co-authored-by: Nathan Dane <nathandane.dev@gmail.com>
Co-authored-by: Kai Evans <hi@kaievans.co>
Co-authored-by: Victor Santos Oliveira <56417388+Vittuu@users.noreply.github.com>
Co-authored-by: cronos <crono2250@users.noreply.github.com>
Co-authored-by: rbousigues88 <rbousigues@gmail.com>
Co-authored-by: The Nomad <nomad@positron3d.com>
Co-authored-by: Jackson Oursland <jacob.oursland@gmail.com>
Co-authored-by: Jacques <59315319+Jacquesvdberg92@users.noreply.github.com>
* feat(render): reconcile definition-local visual mapping (#1214) (#1223)
Retain the original mapping implementation, all eight analytic/hostile-input tests and mapping contract byte-for-byte from #1223. Reconcile only module registration and the current roadmap on render. Actual current render library 166/0; strict all-target render Clippy, wasm32 and repository gates pass. No persistence, sampler, new capability, kernel/vendor change or external code is added. Historical mixed-base full-CI failures are not relabelled.
* feat(render): add bounded linear film accumulation (#1696) (#1697)
Extract the independent FilmAccumulator from owned donor 3b892eab5e. Keep the existing job cancellation and caller-owned admission/work contracts. PNG resolution, primary camera coverage and progressive consumer wiring remain separate. Add analytic statistics, atomic refusal, allocation/range boundaries and real 4096/1048576-count tests. No kernel, document schema, workflow or capability change.
* Sync main into render and preserve GPU CLI help (#5976)
* CLI: --help on each subcommand prints its usage and exits 0 (#4538) (#4545)
* claim: #4538
* CLI: --help on each subcommand prints its usage and exits 0 (#4538)
Every command now answers `--help` and `-h` with its own usage on stdout and
exit 0, the way the top level has since #1828. Before, each subcommand
answered differently: `fuzz --help` started a fuzz run with the default seeds
and ran until it was stopped, `recipe --help` took the flag for a file name,
and `snapshot`, `render`, `oracle`, `run`, `eval`, `script` and `crashes`
answered with an error on stderr and exit 1.
The help is read before the command runs, so `--help` never starts work: no
fuzz run, no MCP server, no file read.
`fuzz` also refuses an argument it doesn't take, as `snapshot` already did: a
flag it doesn't know and a stray word are errors, not arguments to ignore
while a hundred seeds run.
The usage lives in one place now: each command's lines are a constant, and the
top-level usage is composed from them, so the two cannot drift apart. The
top-level usage is unchanged, byte for byte.
# Conflicts:
# apps/solidcraft-cli/src/main.rs
* Renaming a parameter follows it into sheet-metal and plastic rules and canvases (#4420) (#4423)
* claim: #4420
* Renaming a parameter follows it into sheet-metal and plastic rules and canvases (#4420)
Document::rename_param rewrote other parameters, feature inputs, dimensions,
configurations and joints, but not the expressions of sheet metal and plastic rules or
the plane a canvas lies on, so after a rename the sheet metal and plastic features
using them failed with "unknown parameter" and the canvas left the view. It renames
them there now, and param_users lists them (a rule's field, a canvas's input), so a
parameter they use can't be deleted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Parameters: rules' features in drives and the graph; shared rule field lists (#4420)
A sheet metal or plastic rule that uses a parameter makes the sheet metal (or plastic)
features rebuild when it changes, so parameters.users lists them in drives and
parameters.graph links them. SheetRule::fields/fields_mut and PlasticRule::fields_mut
replace the hand-written copies of the rules' fields in params.rs, eval.rs and
sheet.manage_rules. Suggested by @CamWight.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fmt (#4420)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Mirror and pattern: a pattern or mirror feature as the source (#395) (#3998)
* claim: #395
* Mirror and pattern: a pattern or mirror feature as the source (#395)
A pattern or mirror of features refused another pattern or mirror as a
source ("Pat cannot be patterned or mirrored yet"). Such a source now
stands for what it copied: its own sources are replayed at each of its
transforms moved by each of ours, to any depth up to eight. Copies of
a feature picked directly keep the instance numbers they had, so
existing designs keep their face names; copies made through a nested
pattern or mirror are numbered after them, so one feature reached two
ways never names two copies alike. A pattern or mirror of bodies as
the source is still refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Expression completion: keep longer names visible when the typed word is a unit (#3663) (#3664)
* claim: #3663
* Expression completion: keep longer names listed when the typed word is a unit (#3663)
Typing `rad` into an expression box showed nothing because `rad` is the radians unit and
a word that is itself a known token closed the popup. The popup now stays open on such a
word. A complete name (`width` with `width_extra` defined) opens it unarmed: the longer
names are listed with no row selected, Enter submits what was typed and Tab moves on; Up
or Down picks a row, after which Enter or Tab complete it as before. A unit means
something only after a number, so a unit typed anywhere else (`rad` on the way to
`radius`) is a prefix like any other: the popup is armed and Enter completes `radius`
instead of submitting one radian.
A complete token is extended only by names that start with it (`in` lists `inner`, not
`rounding`), a typed unit offers no other units (`in` does not list `inch`), and a unit
right after a number (`2 in`, the form every dialog starts its sizes in) opens no popup at
all, so nothing sits over a dialog's OK button.
Fixes#3663
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Homero Thompson <vdirienzo@gmail.com>
Co-authored-by: SS-EN <84559909+SS-EN@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Martin Pålsson <martin.palsson@outlook.com>
Co-authored-by: Daniel <daniel.owsianski@gmail.com>
* Render: isolate matched GGX visible-normal sampling (#1654)
Independent numerical constituent of #5493. Part of #1654; the CPU reflection consumer remains #1698, so this does not close its image acceptance. Original sampler, matched PDF and analytic/hostile-input tests are preserved from the reviewed donor. 189 render tests, strict Clippy, wasm32 and repository gates pass; fresh quick run 40086624771 passes. No CI configuration, saved-data or kernel change.
* GPU: match index-matched Fresnel in path and viewport (#6036)
Fixes#6035. Preserve actual GPU/f64 material-oracle checks in both translations. Scoped Vulkan, render, Clippy, WASM and fresh quick checks pass. No CI configuration or gate changes.
* feat(render): expose bounded original appearance recipes (#1722)
* claim: resolve the main/render synchronization conflict for #6050
* Settle appearance panel scrolling before targeting Search (#6079)
* claim: #6076
* test(render): settle appearance scroll before resolving Search (#6076)
Expanded starter swatches moved under the scenario click while the existing scroll animation was active. Advance 20 deterministic harness frames before resolving the Search handle. No wall-clock sleep, timeout change, assertion deletion or production change. Original native reproduction fails at the empty-state assertion; all 124 scenarios now pass with one pre-existing ignored adhoc entry.
---------
Signed-off-by: Karl Von Richter<karl@embeint.com>
Co-authored-by: Victor Santos Oliveira <56417388+Vittuu@users.noreply.github.com>
Co-authored-by: Christian Svensson <blue@cmd.nu>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jackson Oursland <jacob.oursland@gmail.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Frank <58932224+plexi32@users.noreply.github.com>
Co-authored-by: raykholo <raykholo@users.noreply.github.com>
Co-authored-by: CamWight <91156642+CamWight@users.noreply.github.com>
Co-authored-by: SS-EN <84559909+SS-EN@users.noreply.github.com>
Co-authored-by: Colin Faulkingham <colin.faulkingham@gmail.com>
Co-authored-by: Karl Von Richter <karl@von-richter.net>
Co-authored-by: HiroKenEz <59478073+HiroKenEz@users.noreply.github.com>
Co-authored-by: Peppe Bergqvist <p@bergqvi.st>
Co-authored-by: GitHub Copilot <noreply@github.com>
Co-authored-by: phanoob <lesur.Stephane@wanadoo.fr>
Co-authored-by: Phanoob <phanoob@gmail.com>
Co-authored-by: Kai Evans <nemshilov@gmail.com>
Co-authored-by: Tyler Chiriboga <tylerchiriboga.apps@gmail.com>
Co-authored-by: Tyler Chiriboga <18270301+Tyler-Chiriboga@users.noreply.github.com>
Co-authored-by: CaRLymx <carlymx@gmail.com>
Co-authored-by: carlymx <24634598+carlymx@users.noreply.github.com>
Co-authored-by: Loohi <78334654+Loohi@users.noreply.github.com>
Co-authored-by: leValdemar19 <valdemarnord@hotmail.com>
Co-authored-by: AhmetV <mihendiz10@gmail.com>
Co-authored-by: AhmetV <344941595+ahmetv10@users.noreply.github.com>
Co-authored-by: rlartot <94693724+rlartot@users.noreply.github.com>
Co-authored-by: Thodoris Tsagkogeorgas <thodoristsagko@gmail.com>
Co-authored-by: ahmetalpcelik <80778880+ahmetalpcelik@users.noreply.github.com>
Co-authored-by: ealitt <ealitt@gmail.com>
Co-authored-by: Homero Thompson <vdirienzo@gmail.com>
Co-authored-by: Diogo Soares Rodrigues <dsrodrigues20@gmail.com>
Co-authored-by: Artem Sushchev <Suschev.Artem+github@gmail.com>
Co-authored-by: Nathan Dane <nathandane.dev@gmail.com>
Co-authored-by: Kai Evans <hi@kaievans.co>
Co-authored-by: cronos <crono2250@users.noreply.github.com>
Co-authored-by: rbousigues88 <rbousigues@gmail.com>
Co-authored-by: The Nomad <nomad@positron3d.com>
Co-authored-by: Jacques <59315319+Jacquesvdberg92@users.noreply.github.com>
Co-authored-by: Martin Pålsson <martin.palsson@outlook.com>
Co-authored-by: Daniel <daniel.owsianski@gmail.com>
* claim: #3083
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* WIP: choose the window system on Linux in Preferences (#3083)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Preferences: a window system set over the control channel is saved for the next start again
The merge kept main's check in ui.prefs, which only looked at the anti-aliasing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Preferences: the window system section drops the restart badge its note already states
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #2459
* Sketch tools: pressing a tool's shortcut again cycles its creation modes (#2459)
R starts 2-Point Rectangle; R again, before the first corner, switches to
3-Point, Center, Rounded, then back to 2-Point (likewise for Circle,
Line, Arc, Slot, Polygon and Spline keys). A held key's auto-repeat
doesn't cycle, the first press always starts the bound mode, and once a
point is placed the key keeps the tool and its points.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Sketch tools: the mode cycling is a Sketch preference, off by default (#2459)
A tool's shortcut pressed again keeps restarting the tool, as before;
Preferences > Sketch > "Pressing a tool's shortcut again switches its
mode" turns the cycling on.
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>
* ci: repair local nextest scheduling and Windows fixtures
* test: refresh MSVC robustness references and isolate GNU policy
Integrate the Windows reference records from #3361 and the target-environment
selection and recording policy from #4159. Keep shared records and comparison
tolerances intact, and retain seed 36 as an existing failure tracked by #3583.
Co-authored-by: Ray <raykholo@users.noreply.github.com>
Co-authored-by: chris <christopher.nourse@gmail.com>
* test: make CI subprocess and revolve fixtures deterministic
---------
Co-authored-by: Ray <raykholo@users.noreply.github.com>
Co-authored-by: chris <christopher.nourse@gmail.com>
* claim: #4985
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Equal with three or more curves selected makes them all equal (#4985)
With more than two curves selected, Equal fell back to the pick tool and
applied nothing. sketch.constraint.equal also takes curves: [2..200],
each made equal to the first in one step (strict, so a conflict changes
nothing), and the toolbar sends a selection of three or more curves as
that list.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #5074
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Trim cuts a line at its tangent point on a circle (#5074)
A line the solver made tangent to a circle touches it within rounding
noise, and the exact intersection missed the touch whenever the noise
fell outside, so trim found no cut there and deleted the whole line.
Trim takes its cut points from intersections_within at its own merge
distance, as profile detection does (#588).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #3330
* Linked components: joints keep their face after Update Links (#3330)
A linked design comes in as STEP text, which keeps no face names, so the
faces of its bodies were named by their place in the file (`face:<k>`).
An edit of the part that adds a face earlier in the file (a second hole
of the same size) gave that place to another face, and a joint on the
linked part moved there silently.
When a link is made or updated, each imported face is matched to the
part face it was written from (the same geometry), and the Import
feature keeps that face's name in the part (`Feature::link_faces`, a new
pending format change). The linked face is named
`F<import>🔗<digest of that name>`, pieces keeping their qualifiers,
so references follow the part's own history through Update Links as they
do inside the part, and a face the part no longer has is reported.
Linked faces still answer to `face:<k>`, so older references keep
resolving; Update Links warns about joints that use them and reports
joints that no longer solve.
* Linked faces: a solid's twin keeps its own names; renamed faces count as an update
Two solids of a part alike in one place each take the names of the solid
the file names them after. Update Links reports a link whose part named
its faces differently since as updated (a joint may follow that name to
another face), not as unchanged; only a link getting its first names
stays unchanged.
* Linked faces: Extract to File carries the design's face names too
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: MateuszSychXpl <151755914+MateuszSychXpl@users.noreply.github.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* claim: #3663
* Expression completion: keep longer names listed when the typed word is a unit (#3663)
Typing `rad` into an expression box showed nothing because `rad` is the radians unit and
a word that is itself a known token closed the popup. The popup now stays open on such a
word. A complete name (`width` with `width_extra` defined) opens it unarmed: the longer
names are listed with no row selected, Enter submits what was typed and Tab moves on; Up
or Down picks a row, after which Enter or Tab complete it as before. A unit means
something only after a number, so a unit typed anywhere else (`rad` on the way to
`radius`) is a prefix like any other: the popup is armed and Enter completes `radius`
instead of submitting one radian.
A complete token is extended only by names that start with it (`in` lists `inner`, not
`rounding`), a typed unit offers no other units (`in` does not list `inch`), and a unit
right after a number (`2 in`, the form every dialog starts its sizes in) opens no popup at
all, so nothing sits over a dialog's OK button.
Fixes#3663
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* claim: #395
* Mirror and pattern: a pattern or mirror feature as the source (#395)
A pattern or mirror of features refused another pattern or mirror as a
source ("Pat cannot be patterned or mirrored yet"). Such a source now
stands for what it copied: its own sources are replayed at each of its
transforms moved by each of ours, to any depth up to eight. Copies of
a feature picked directly keep the instance numbers they had, so
existing designs keep their face names; copies made through a nested
pattern or mirror are numbered after them, so one feature reached two
ways never names two copies alike. A pattern or mirror of bodies as
the source is still refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #4420
* Renaming a parameter follows it into sheet-metal and plastic rules and canvases (#4420)
Document::rename_param rewrote other parameters, feature inputs, dimensions,
configurations and joints, but not the expressions of sheet metal and plastic rules or
the plane a canvas lies on, so after a rename the sheet metal and plastic features
using them failed with "unknown parameter" and the canvas left the view. It renames
them there now, and param_users lists them (a rule's field, a canvas's input), so a
parameter they use can't be deleted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Parameters: rules' features in drives and the graph; shared rule field lists (#4420)
A sheet metal or plastic rule that uses a parameter makes the sheet metal (or plastic)
features rebuild when it changes, so parameters.users lists them in drives and
parameters.graph links them. SheetRule::fields/fields_mut and PlasticRule::fields_mut
replace the hand-written copies of the rules' fields in params.rs, eval.rs and
sheet.manage_rules. Suggested by @CamWight.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fmt (#4420)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #4538
* CLI: --help on each subcommand prints its usage and exits 0 (#4538)
Every command now answers `--help` and `-h` with its own usage on stdout and
exit 0, the way the top level has since #1828. Before, each subcommand
answered differently: `fuzz --help` started a fuzz run with the default seeds
and ran until it was stopped, `recipe --help` took the flag for a file name,
and `snapshot`, `render`, `oracle`, `run`, `eval`, `script` and `crashes`
answered with an error on stderr and exit 1.
The help is read before the command runs, so `--help` never starts work: no
fuzz run, no MCP server, no file read.
`fuzz` also refuses an argument it doesn't take, as `snapshot` already did: a
flag it doesn't know and a stray word are errors, not arguments to ignore
while a hundred seeds run.
The usage lives in one place now: each command's lines are a constant, and the
top-level usage is composed from them, so the two cannot drift apart. The
top-level usage is unchanged, byte for byte.
# Conflicts:
# apps/solidcraft-cli/src/main.rs
A Single hole can take up to two straight edges of its face as references,
each with the distance of the hole's centre from that edge's line, measured
in the face's plane. `solid.hole` takes them as `references: [{edge, distance}]`;
the edges are found again by their persistent names, else their points, so
the hole follows them when the part changes, and each distance is a
parameter. A bad reference (no edge there, a curved edge, one square to the
face, parallel edges, a negative distance, a centre off the face) is refused.
In the Hole dialog, Reference 1 and 2 follow the face, each with its Distance
row; the canvas shows each distance as a dimension from its edge to the hole,
with a box just past the edge to type it in. Picking an edge measures its
distance (and its box takes the keyboard), typing one moves the hole, and
moving the hole measures them again. Edit Feature picks the edges again with
their distances.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Placing a Single hole put it exactly where the click landed, with nothing to line it up by and no
way to move it but taking it out and picking again; the dialog was a list of size fields.
- Preferences › Design › Create Hole has two settings, both off by default, so a new installation
places holes as before:
- Snap to face and edge centres (`hole_snaps`): the planar face under the cursor offers its
middle and the centres of its round edges (a boss, a rounded corner, a hole already there) as
marks; a pick near one, or on or beside such an edge, takes that centre and the mark is named.
Sketch points on the face still come first; Alt or Ctrl held places it where clicked.
- Move holes by clicking or dragging (`hole_moves`): each placed hole has a handle that drags it
(snapping when snaps are on) and takes it out when clicked, and a pick elsewhere on its face
moves it there. Edit Feature shows the handle too, so a hole can be moved later (#1047).
- The Hole dialog draws a section of the hole as its options make it, with each size in a field
beside the dimension it sets; a hovered or focused field lights its dimension. The layout is a
new kit element, `ui_kit::figure_rows` (a caller-painted figure beside captioned fields).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A new kit control, `ui_kit::count_field` / `count_row`, is an expression field for a
whole-number count with stacked step arrows at its right edge. An arrow steps the
evaluated count by one within the field's limits: a plain number is replaced, an
expression keeps its parameters and gains or adjusts a trailing `+ n`. The text
changes as typing changes it, so the live preview follows.
Used by the Quantity rows of the sketch Rectangular (1-500, both directions together
at most 500) and Circular (2-500) patterns, and the solid Rectangular, Circular and
Path patterns (1-1000).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #4434
* Join: say so when the joined piece doesn't touch the body (#4434)
A Join (extrude, revolve, sweep, loft, pipe, coil, primitives, thicken,
boundary fill, surface network) whose material meets none of its targets
still joins it to the first target as a separate piece. The result stays
as it was; the feature now warns: "the joined piece doesn't touch `Body1`,
so `Body1` is now in 2 separate pieces (New Body makes it a body of its
own)". History marks it and the command reply carries it.
The check compares pieces (outer shells, not voids) of the bodies the
feature changed or absorbed before and after it, so it holds for the
batched multi-profile path too, stays quiet for a join that bridges
pieces or closes a cavity, and leaves Combine (which joins the bodies it
is given) alone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
cadmpeg 0.6.3 is pure Rust (ruzstd instead of the zstd C library, zip 9), so
solidcraft-io depends on it on every target and the wasm build reads .FCStd
and .3dm files like the desktop app. The browser's file pickers offer them.
0.6.3 replaces DecodeOptions::container_only with a decode depth; ask for
Geometry, what its STEP writer reads. zip 8 leaves the tree, and with it the
three cargo-deny advisory exceptions kept for it.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* claim: #4302
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Timeline: suppressing a feature keeps later features' body names (#4302)
Bodies numbered by the counter (`BodyN`) take the next free number on every
recompute, so suppressing a feature that made one moved every later counted
body down one (`Body2` became `Body1`) and broke what named it, such as a
Move of `Body2`. timeline.suppress now stores the current names of later
features' counted bodies in their `body_names` first, as body.rename already
does for a renamed counted body (#1109). Engine only: no evaluation or
design-format change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Timeline: a body made while a feature is suppressed keeps its name (#4302)
Suppressing a feature freed its counted name (`Body1`), so a body made
meanwhile took it and became `Body3` once the feature was back. Suppress
now also stores the suppressed feature's own counted names, and the
evaluator adds the names stored on earlier suppressed features to the
state's `reserved_body_names` (the names a rename freed, #4957), which the
`BodyN` counter passes by. They are taken from the design before each
feature is computed and are part of the state digest, so a cached result
is only reused when they match.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Add a small test boat to steer around with the 3D mouse
A little hull in its own camera, drawn by the UI itself and not the CAD
viewport, so a 3D mouse can be tried out without opening a design. It
brings the model (generated by assets/models/make_test_boat.py) and the
tests for how the camera moves it.
Nothing uses it yet; Preferences picks it up with the rest of the 3D mouse
work.
* UI kit: keep a choice row from flipping with the scrollbar
A choice row picks segments or a dropdown by whether its labels fit. A page
scrollbar that comes and goes narrows the row by its own width, so the row
could flip back and forth between the two on a page that sits right at the
edge. It is now judged on the whole row and through a small helper,
fits_steadily, that only changes its answer once the miss is wider than the
scrollbar.
Also tests for that, for typed numbers in a slider's value box, and a
test hook to put up the issue-report input barrier by hand.
* 3D mouse: device model, guided calibration, buttons and a picture
The 3D mouse becomes a device we know something about instead of six raw
numbers.
The reader picks between spacenavd and the direct hidraw driver, falls back
from one to the other, and says so when it can't open the device. Guided
calibration asks whether the device is a spring cap or a trackball, adds a
touch step, and uses a settle filter so a ball let go of stops reading as
motion. Buttons get jobs (keys, held modifiers, commands), saved per
device, with defaults for the SpaceMouse Pro.
Preferences gets a picture of the device that moves as it does, next to the
test boat, and the same picture can sit in the 3D view at Left Viewport,
Right Viewport or Left Window, above the floating History card like the
legend and readouts.
These ride together because the crate's calibration types, the Preferences
page, the Linux reader and the button hook all use each other.
* 3D mouse: offer to calibrate an uncalibrated device
When a 3D mouse shows up that has not been calibrated, a card in the 3D
view offers to calibrate it, to swap drivers when the other one would work
better, to close, or to never ask again. It is made once per device, goes
away after 30 seconds in view, and gives way to a status or computing
toast instead of stacking on it.
The card is the shared toast, which now takes buttons: they group by
purpose, wrap inside the card at narrow widths, and do nothing while an
issue report holds CAD input. The picture of the device in the 3D view
keeps clear of the card.
* 3D mouse: translations and attribution
The 3D mouse strings (ui.3d_mouse_*, the calibration steps and the offer
card) in every shipped language, the Japanese catalog note, and the credit
for the test boat model in ATTRIBUTION.md.
This is last because the code before it only looks keys up: it builds and
runs without these, falling back to English until they land.
* 3D mouse: draw its preferences with the settings kit like the rest of Navigation
The 3D mouse section was a folded header with its own layout. It now
uses the same kit as the sections above it: a settings_section card,
settings_form rows with the shared label column, dividers between the
device, axes, picture and buttons groups, Text::Hint for the helper
lines, and settings_control_width for the dropdowns.
The section is open like every other one (the header still folds it),
so the device badge on the folded header is gone; the calibration row
already names the device. Every setting, string and key is unchanged.
Tests that unfolded the section no longer click it, and the hint lines
are looked up as hints instead of notes.
* 3D mouse: show each axis's live tilt in Preferences, like main's axis rows
Each view axis row is main's axis row again: a meter that follows the tilt of the motion driving it, with the dead zone marked, next to Invert, Speed and the "driven by" menu. The meter reads the calibrated travel when the device has a calibration and the raw reading over full scale when it does not, and asks for a frame whenever the reading changes. The helpers are back as they are on main, and the calibration state is a badge by the enable toggle.
* 3D mouse: line its preferences up with the rest of Navigation
- The two checkboxes sit side by side in the same two-column grid as Wheel and trackpad.
- Row labels stop repeating "3D mouse" and use sentence case: Calibration, Driver, Restore defaults, Reset view, Left viewport, Right viewport, Left window.
- Hints line up under the control column instead of the left edge.
- The six view axes are named Pan X, Pan Y, Zoom, Orbit X, Orbit Y and Roll so they fit the name column.
- Buttons are a divider and rows in a form like the other groups, without the bold sub-heading.
- The driver, dead zone, axes, picture and buttons sit behind a collapsed Advanced section, since most people never touch them.
* 3D mouse: tidy the Advanced test for clippy
Rebased onto main as one commit (was 120d26f7, df488f9a, db9d1073 plus
merges of main).
- A request the app reports as still running (#1618) is waited for on the
same connection through `control.pending`; past the wait the error says it
was not cancelled and will still apply (or only write its file), later calls
are not sent behind it, and the new read-only `pending` tool returns its
result once it lands.
- A failed batch says what really happened: `rolled_back` only when the
design is back where the batch started (as #3288 / #3242 on main: the app's
`rolled_back` is believed; for an older app, every completed step undone;
only a rollback asked for is reported as one), plus `applied`, `not_run` and
`failed.still_running`.
- A batch step may name a tool (set_parameter, list_commands,
describe_command, execute, measure, inspect_design, undo, redo, export, save,
open, new_design, crash_reports); screenshot, body_topology, batch and
pending are refused before anything is sent.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* docs(mcp): Windows setup for Claude Desktop (paths, two servers, Store config file)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(mcp): say the Store build can keep the config elsewhere, not that the other path fails
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Retain MID snaps across both picks and materialize reusable midpoint
constraints for point-to-line and point-to-point dimensions. Let midpoint
pairs choose horizontal or vertical spans with pointer placement before
entering a value.
Cover projected model edges, overlapping geometry, helper reuse,
undo/cancel/reopen, and existing angular and line-circle workflows.
While a section is on, Section Analysis opened with no plane picked now
says "No plane picked: OK turns the section off." and OK runs
inspect.section {"clear": true}. With no section on, OK with no plane
still asks for a plane.
Fixes#2410.
* claim: #4922
* UI: let themes own corner radii
* UI: use four-point panel and window radii in SolidCraft 95
* UI: align the 95 radius assertion with four-point panels
* claim: #4953
* UI: let themes own structural and control borders
* UI: single Blueprint outlines and dotted secondary rules
* UI: separate container outlines and add theme-owned 95 bevels
* claim: #5007
* UI: apply theme palettes to icons, workspace tabs and Gruvbox settings
* claim: #5045
* Viewport: invalidate cached geometry colours when the theme palette changes
* UI: preserve legacy theme logo colours with the borderless mark
* assembly(feat): ground first parts and drag free joint chains
Ground the first top-level part when it is created and let face dragging reposition free occurrences or any member of a floating joint chain in one undo step. Preserve exact repeated-placement frames, joint values, modifier selection and Escape cancellation. Keep the accumulated fixture, viewport layout and documentation integration fixes from the existing PR.
Signed-off-by: Karl Von Richter<karl@embeint.com>
* assembly(fix): update movable fixtures for first-part grounding
Accept the documented grounded override in component creation, explicitly free movement fixtures, and keep linked clipboard pastes movable even in an empty document. Cover first and reused linked pastes, default insertion grounding, and single-step undo in the existing link tests.
Signed-off-by: Karl Von Richter<karl@embeint.com>
* assembly(test): keep the motion-cost crank free
Unground the first component in the document-level joint-face measurement fixture so its motion samples can rotate after first-part grounding. Preserve the rotation and shared-measurement assertions that caught the Linux CI failure.
Signed-off-by: Karl Von Richter<karl@embeint.com>
* assembly(test): free the frame placement fixture
Create the frame-member placement fixture ungrounded before moving it above its root sketch. Keep its world-space member assertions while respecting first-part grounding in the frame tests added on main.
Signed-off-by: Karl Von Richter<karl@embeint.com>
---------
Signed-off-by: Karl Von Richter<karl@embeint.com>
* Extrude: picking a face, vertex or parallel plane off the profile turns the extent to To Object
With profiles picked, a face off their plane (or curved), a vertex off it, or a
construction plane parallel to it is taken as the To Object target instead of
another profile: the extent switches without the Extent Type choice, and the
distance shows how far the target is. Coplanar faces still join the profiles.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Extrude: automatic To Object is a Preferences setting, off by default
Preferences > Design > "Extrude to a picked target" (`extrude_auto_to_object`, default
false) turns the automatic To Object on. Off, picks in the Extrude dialog only add
profiles and the distance is not synced to a target, as before this change.
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
* claim: #4587
* Agent chat: end the agent's process group with the system call (#4587)
The group was signalled with a `kill` program: `kill -s TERM -- -<pgid>`.
That works with procps' `kill` and with BSD's, but busybox' `kill` takes no
`--`: it prints its usage, exits 1 and signals nothing. The agent's helpers
(the CLI an adapter wraps, the MCP servers it starts) then stay alive after
the agent is ended.
The group is signalled with the system call instead, so no `kill` program and
no shell are needed and the behaviour is the same everywhere. `nix` (MIT) is
already in the tree through zip, and only on Unix: the browser build does not
take it.
The two tests that cover this now pass where they failed before:
`agent_chat::process::tests::dropping_the_process_ends_its_group` and
`agent_chat::process::tests::stopping_the_agent_leaves_no_helpers_running`.
* claim: #4692
* Delete removes a selected construction axis (#4692)
Delete on a construction axis skipped it with "the origin axes can't be
deleted". It now deletes the construction axis feature, like construction
planes; the origin axes X, Y and Z are still refused.
#5109 added Frame::hover_tint and selection_tint; two test frames that landed alongside it didn't set them, so solidcraft-gpu's tests stopped compiling on main.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Sketch on a picked origin plane uses the plane shown, also in a moved component
After a STEP file opens its component stays the active one, and standing it up
turns that component a quarter about X. Starting a sketch on XY picked in the
viewport then sent `{"plane": "XY"}`, which names the active component's own
XY: upright after the stand-up. The sketch landed on the world XZ, Look At
turned to face Y and the extrude went along Y.
The viewport and browser only show the design's origin planes, so a picked
origin plane now goes to sketch.create written out as that world plane while
the active component is moved; sketch.create maps it into the component's
frame once. The flip toward the viewer uses the same plane.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Stand-up axes tests: stand the file up through the offer when one is waiting
#5655 offers to stand up SolidWorks files instead of turning them silently.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Keep physical modifier events out of the ordinary-key slot, preview held keys immediately, and confirm after release and a one-second settling interval. Preserve legacy control-channel modifier behavior while allowing explicit physical Control/Command input. Add regression coverage, a UI scenario, localized guidance and documentation.
Fixes#1809.
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Hovering or selecting whole bodies -- a browser row, a History group, a body
under the pointer in body-pick mode -- built a CPU copy of every body's
triangles into the highlight scene. On a 40-body 4.3 MB STEP assembly that was
147 bodies, 70 MB of triangle copies and a 519 ms frame.
The model already has the same geometry on the GPU. The highlight now names it
and the renderer draws it again in the highlight colour:
- bodies drawn from a display plan: the model's evaluated plan chunks, with a
hover/selection tint uniform (the chunks stay resident in the model scene);
- bodies drawn as placements of shared triangles: the same placements of the
model's shared triangles, coloured per instance;
- bodies the model builds on the CPU: their vertex range in the model scene's
block.
See-through and faded bodies keep the CPU feedback, which colours them; faces
and edges highlighted on their own are unchanged. The highlight looks the same
(verified side by side).
On the assembly the highlight build drops from 519 ms to 9 ms and no longer
copies any triangles; selecting the whole assembly takes 9-20 ms.
Co-authored-by: Claude Code <noreply@anthropic.com>
#5187 gave the scene its own offscreen target with the app's sample count,
which skipped the OpenGL clamp to 1x from 6dd202d. The scene's count now
goes through effective_msaa_samples as well.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #4965
* Joins and cuts follow only the placements they depend on (#4965)
Tests that turning a joint between parts cuts no hole again, and that a
tool cutting another part cuts again when that part moves relative to it,
while an unrelated part moving keeps the result.
Booleans and Combine now hash only the placements of the bodies they take
as inputs, relative to the frame that evaluates them (#3397), so these
pin that behaviour for joins and cuts with named targets across parts.
The app opens its localhost control channel on port 27878 (or the next free
one) unless Preferences > Assistant turns it off; --control and
SOLIDCRAFT_CONTROL_PORT still win. A current.port discovery file lets
solidcraft-cli mcp find the running app with no arguments.
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* claim: #5079
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Crash journal keeps component.activate (#5079)
Activating a component changes only the session's active component and
occurrence path, not the design, so the journal dropped the command and a
crash report's replay built later parts under the previously active
component. The journal now also keeps a command that changes either.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #5124
* Flatpak: find the host's fonts for CJK text and the font list (#5124)
Inside the Flatpak sandbox the host's fonts are mounted at /run/host/fonts
and /run/host/user-fonts, which no font search looked at. Japanese and
Chinese text fell back to boxes, and the font list missed the host's
families.
- Search both folders for the CJK and Chinese fallbacks and for the
installed-font list.
- The CJK fallback reads SOLIDCRAFT_FONT_DIRS (a path list, as the font
list already does), with CRAFT_FONTS_DIR kept as a fallback.
A line drawn across a circle only snapped its end onto the curve, so a
diameter could miss the centre at any angle. Near the centre the line now
locks onto the radial line: on the rim it stays on the circle and the centre
is kept on the line, and on the centre point the end lands there.
* claim: #5137
* Dragging a part along its joint no longer highlights what's under the pointer (#5137)
The viewport's hover pick was skipped during a box selection and camera
moves, but not while a jointed part was dragged, so faces and edges under
the pointer lit up on every frame of the drag. The pick now waits until
the joint drag ends.
* claim: #3250
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Return to the originating toolbar tab after finishing a sketch (#3250)
A sketch started from SHEET METAL or PLASTIC always ended on the SOLID
tab. Remember the tab that was active when the sketch began
(UiState::sketch_return_tab, not saved) and go back to it when the
sketch ends. Falls back to SOLID when that is not one of the normal
toolbar tabs. A sketch started from SOLID behaves as before.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Test: no one-element loop in the return-tab test (clippy single_element_loop)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* claim: #3216
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Parameter import and canvas image pickers offer their file types (#3216)
Both opened the picker with no file type of their own, so it listed only
designs, CAD files and meshes (and All files). macOS lets you pick only
the types a filter names: CSV parameters, and PNG or JPEG images for a
canvas, were greyed out. They now ask for csv/json and png/jpg/jpeg,
which the picker lists first (#1801).
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: #3372
* Fix Escape cancellation in Change Parameters
Consume Escape before parameter editors can commit pending text on focus loss, while preserving autocomplete and shortcut capture priority. Add 11 UI regressions covering draft cancellation, preview cleanup, document and history preservation, and reopening saved values.
* claim: #3858 (3MF import: names from a Bambu Studio project)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* 3MF import: name a Bambu Studio project's bodies after its objects (#3858)
Bambu Studio leaves the name off the root model's objects and keeps it in
Metadata/model_settings.config (<config><object id="N"><metadata key="name">),
so every object of a project came in as Mesh1, Mesh2, ... A build item whose
root object has no name of its own now takes the object's name from those
settings. A name in the model still comes first, and part names, other
settings and objects of other model parts are left alone.
The settings are read only when a body needs a name, within what the model
parts left of the 1 GB budget. Settings that can't be read (malformed, nested
too deeply, too large) never fail the import: the bodies keep the generic
names, with a warning saying why.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Tyler Chiriboga <18270301+Tyler-Chiriboga@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #3838
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SBN24y2TcqRcm2zn8Gkj27
* Cancel Sketch: leave a sketch without keeping its changes (#3838)
sketch.cancel (Cancel Sketch, ribbon Exit group and the sketch's
right-click menu) puts the design back as it was when the open sketch
was created or opened for editing: a new sketch is removed with its
dimension parameters, an edited one returns to how it was. It is one
undo step, so Undo goes back into the sketch.
The session remembers the design and the undo history at the point a
sketch is entered. Cancel uses that only while the history still runs
through it (the same steps before, every step since taken in this
sketch); otherwise it refuses and points to Undo, so a stale entry can
never throw away later work.
The icon is the approved close glyph in the sketch accent (alias
sketch_cancel); ja and zh carry the new label.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SBN24y2TcqRcm2zn8Gkj27
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #3857
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Joint dialog: a body of a further instance picks that occurrence, not the component's first (#3857)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* claim: #4100
* Deleting an inserted file removes its components, placements and joints (#4100)
A STEP or mesh file inserted into a design makes a component tree. Deleting
its feature left that tree behind: the components, their occurrences, the
rigid joints that hold them and the bodies' component assignments. A later
body that took an imported body's name was then placed in the stale component.
Document::remove_component_tree removes a component subtree and everything
that refers to it: the features made inside it, its occurrences, the joints
on them with their motion links, joint origins, contact set members, motion
study keys and exploded moves, body component and offset entries, standard
part and link records, and configuration columns. Deleting an inserted
file's feature and component.delete both use it; component.delete now also
removes joints left pointing at a deleted occurrence. The body transfer
refusal moves with it, unchanged.
* Only an inserted file's feature takes its component tree when deleted
Mark the feature a STEP, IGES or mesh insert makes (`inserted`), and remove
the component tree on delete only for those and for a linked design's
feature. Deleting a copied component's independent base deletes just that
feature, as before. Older designs get the mark as they open (pending format
change), except copied components' independent bases.
* claim: #4226
* Esc ends the navigation bar's Pan, Orbit and Zoom modes (#4226)
The nav bar toggled a mode on, but nothing cleared it on Esc, so a left drag
kept panning, orbiting or zooming. Esc now ends an active nav mode first;
the next Esc reaches the tool or dialog as before.
each_frame_hands_the_view_its_motion_through_the_drivers_mapping dropped
the output of a frame it ran (`let _ = ctx.run_ui(...)`). epaint panics
in debug builds when a TexturesDelta is dropped with deltas left ("Dropped
TexturesDelta with 1 unapplied deltas"), so the test failed under a plain
`cargo test` on every platform while the ci profile passed. It now clears
them, as the other UI tests do since #60.
Co-authored-by: Tyler Chiriboga <18270301+Tyler-Chiriboga@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #4380
* "Don't Save" looks like a button in the Save changes prompt (#4380)
The prompt's third action used the kit's low-emphasis text style, so it
didn't read as clickable and people thought Cancel was the only way out.
Add an opt-in secondary_left style to the confirm footer (a framed
secondary button from the theme tokens) and use it for Don't Save.
* Save prompt test: clear each frame's texture delta once it is applied
egui 0.36 panics when a TexturesDelta with unapplied deltas is dropped;
the test kept only the last frame's output and dropped the earlier ones.
* claim: #4440
* Flat Pattern view: draw perforation holes (#4440)
The flat view drew the outline, cut-outs, pockets and bend lines but not
perforation fields, though the flat-pattern data and the DXF export list
their holes. The view now reads the hole centres from the sheet through
SheetBody::perforation_holes, caches them per model state and body, and
draws them as circles in the cut-out style. Holes outside the viewport or
smaller than one pixel are skipped, and the cache is capped at one million
holes.
* claim: #4464
* Sketch offset keeps its source lines free to turn (#4464)
An offset adds a Parallel between each source line and its offset line.
Since offsets became associative, the offset's points are held during a
solve, so each Parallel pinned its source line to the offset line's
current direction: a closed chain lost its turning freedom (8 degrees of
freedom became 6), and Horizontal or an angle edit on a source line was
refused as conflicting.
The solver and the constructive reading now leave out a Parallel that
ties an offset line to the source it follows while the association lives;
the association already keeps them parallel. After Break the stored
Parallel drives again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #5254
* Toolbar: a constraint dragged out of the small box becomes a full button (#5254)
The Constrain panel's small box of constraint icons was a fixed list, so
dragging one onto the full-size buttons saved a new order but it was still
drawn in the box. The toolbar settings now remember, per tab, which box
commands were moved out; those are drawn as full buttons, the box closes
up, and dropping one back on a box icon returns it. Reset Toolbar restores
the default box, and settings saved before this load unchanged.
* Renaming a body that later features change evaluates nothing (#2467)
Model::rename_body now keeps the results when later features change the
body (a pattern, a chamfer, a join). It still evaluates when a body of
that name comes and goes or isn't the same body throughout, when a later
body's name is worked out from it (X (2)), or when a message quotes it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Rename box: rename off the window thread, and locked bodies follow a rename (#2467)
The rename box commits body.rename through run_heavy, so a rename that
still evaluates later features (one names the body in its targets)
computes beside the window instead of freezing it. Locked bodies now
follow a body.rename from anywhere (the rename box, the control channel,
scripts), not only the rename box.
---------
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #3187
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Reorder design tabs by dragging them (#3187)
A tab dragged along the strip follows the pointer while the others make
room, and lands where it is released; Escape, or releasing down the
window, leaves the order as it was. With the custom title bar a drag that
starts mostly up or down (or on the only tab) still moves the window.
The control channel's ui.documents gains a move action (from, to).
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: #1783
* FreeCAD/Rhino import: decode caps with room for detailed parts (#1783)
cadmpeg decoded FreeCAD and Rhino files under its service profile, whose
million collection items and 2.56e8 work units a detailed part (a sprocket,
a threaded rod, a part with a few hundred faces) runs past: the import was
refused with "resource limit on CollectionItems: BudgetExceeded". Allow
8 million items and 2e9 work units, half of cadmpeg's desktop profile; the
input-size cap stays SolidCraft's STEP limit and every other cap stays at
the service profile's.
decoding_is_capped checks the two raised caps (and that they stay below the
desktop profile's) and that every other cap is still the service one.
Fixes#1783
* claim: #3278
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* 2-point circle takes a typed diameter (#3278)
After its first point the 2-point circle tool showed no value box, so the
circle could only be sized with the cursor. It now shows a Diameter box,
like the centre circle: the typed value puts the second point that far
along the cursor's direction, and Enter draws the circle.
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>
* Macos: keep touchpad/key modifiers true to fusion
* fix: remove obsolete orbit snap reset from trackpad navigation
Upstream removed ViewportState::orbit_snap. The new trackpad orbit path retained one reference, causing E0609 in the PR merge build.
Validation: all seven trackpad tests pass; cargo check -p solidcraft-ui-egui --all-targets passes on the exact failing CI merge 66a2791b with this fix applied; rustfmt and diff checks pass.
Co-Authored-By: GitHub Copilot <noreply@github.com>
* fix: retain Fusion trackpad help in refactored preferences
Preserve upstream's new preferences pages and apply the translated Fusion trackpad tooltip in prefs/navigation.rs.
Validation: cargo build -p solidcraft passed. Tests and full CI were not run, as requested.
Co-Authored-By: GitHub Copilot <noreply@github.com>
* style: format Fusion trackpad preferences tooltip
Apply rustfmt's required wrapping for the relocated tooltip. No behavior changes.
Validation: cargo fmt --all -- --check and git diff --check passed.
Co-Authored-By: GitHub Copilot <noreply@github.com>
---------
Co-authored-by: GitHub Copilot <noreply@github.com>
* claim: #3471
* Delete in a rolled-back history warns about the features after the marker (#3471)
With the timeline rolled back, the checks a delete runs first (the
confirmation's "these features will fail", timeline.dependents and
selection.delete's would_fail) compared models evaluated only up to the
marker. Features after it, broken by the delete, were not listed, so their
error marks turned up only once the timeline moved to the end.
newly_broken_in_history evaluates both sides over the whole history when
either has a marker (on copies; the model and document are unchanged) and is
used by the delete's report and by timeline.dependents.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #3042
* Warn when cut pattern copies remove no material
A rectangular or circular pattern whose copies miss the body succeeded silently and those copies did nothing. The pattern now reports how many of its cut copies removed no material, and keeps the copies that land.
* claim: #4003
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Frames: the profile library (#4003)
The first slice of frames and weldments: the cross-sections structural members
will be made of, each SolidCraft's own parametric definition built in code
(no vendor or standard size tables are shipped).
- solidcraft_doc::frame: bar, round bar, rectangular and round tube, angle,
channel, I-beam and a generic T-slot extrusion as exact line/arc regions,
with defaults worked out from the profile's size, refusals that name the
dimension that does not fit, anchors and placement, and exact section
properties (area, centroid, Ixx/Iyy/Ixy by Green's theorem).
- frame.profile.list, frame.profile.properties (with mass per metre for a
density) and frame.profile.insert (draws a profile into a sketch, locked by
default, at a point, anchor and angle).
- Tests against hand-computed values (rectangles, circles, composite
rectangles by the parallel-axis theorem, fillet spandrels), hostile input,
extrusion volume, undo; catalog entries in every locale; docs/frames.md;
ROADMAP M15.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Frames: drop the built-in T-slot profile (#4003)
Aluminium extrusions and other vendor sections come from the user's own
sketch (drawn or inserted from the vendor's DXF), so the library keeps the
plain steel shapes only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Frames: members along frame sketch lines (#4003)
frame.member lays one body per straight line of a frame sketch (2D lines or
3D sketch lines), its cross-section square to the line from end to end:
- profile: a built-in profile, or profile_sketch, a sketch whose material
(nested loops alternating, so two circles make a tube) is the section in
its own x and y around its origin; profiles picks override it.
- anchor (origin, centre, centroid or a bounding-box corner or edge middle),
angle (an angle expression, so a parameter), mirror.
- Orientation: the profile's y as near world up as the line allows, x
square to both; a vertical member's x is world X (frame::member_axes).
- Members follow sketch and parameter edits, undo as one step, save and
reopen (format: pending/frame-members.rs), and are named after the profile
(Rect Tube 40x40x3, (2)…), numbering on past names already taken.
Tests against hand-computed volumes and bounding boxes (rectangle, 3D
tower lines, parameter edit, sketch tube profile and picks, anchor/angle/
mirror), refusals, undo/redo and reopen; walk fixtures and goldens; docs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Frames: fixes from the adversarial review (#4003)
- Default member lines are what was drawn: projected and other linked curves
and 3D wires (a face sketch's edges) and lines of no length are left out.
- A profile sketch's material starts from its drawn profiles, as
solid.extrude's "all" does, so a section sketched on a face works; the
even-odd test uses outlines and boxes worked out once, capped at 256.
- Member names are pinned to their lines (`names`, by line id; `body_name`
is the base): deleting or adding a line, or deleting another feature, no
longer renames other members, and frame.member stores every name it gives.
body.rename renames a member on its line (also for later member features);
a copy whose body_names were set still names its members by position.
- Built-in profile dimensions are expressions (ProfileSpec): they follow
parameters, bare numbers take the design's units, and they are feature
inputs. frame.profile.properties/insert read them the same way.
- Default root/toe radii shrink to the room a thick section leaves; radii
given are checked by name; a radius must be 0 or at least 0.01 mm; error
messages report the wall or flange value given, not twice it.
- A line within 1e-6 rad of vertical orients as vertical, so rounding never
turns a post a quarter turn.
- A 2D frame sketch in a differently placed component is mapped into the
member's frame, and member features recompute when placements change.
- Caps before lookups for a design's `lines`; a line listed twice is
refused; a 3D wire that runs back on itself is not a line.
Regression tests for each, from the review's scenarios.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Frames: the Frame Member dialog on the ribbon (#4003)
- Frame Member (MODEL > Create) opens a dialog: the frame sketch's lines
picked in the view (or All Lines), a built-in profile and its dimensions or
a section picked in another sketch, anchor, angle, mirror and name, with a
live preview. Edit Feature reopens it filled in.
- Lines picked in the view keep their sketch (a SKETCH_LINES input picks
ExternalSketchCurve): line ids are each sketch's own, so two 3D sketches
both have a `w2`, and the members went on the newer sketch's lines.
- timeline.redefine takes frame.member. Members keep their names through an
edit unless it gives them a new body_name; frame.member takes `names`.
- A member whose section is a whole profile sketch keeps it when edited
(the dialog shows the sketch instead of picking every region, which would
fill a tube's hole).
- An original frame_member icon, open for review, on the ribbon, timeline
and browser.
- The angle profile is listed as L-Angle, and web and flange as Web/Flange
thickness, so they translate apart from the existing Angle, Web and Flange;
the dialog's labels are in en, ja and zh. en-GB no longer repeats the frame
command labels it spells the same.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Nine tests dropped an egui frame's output with its texture delta still
unapplied (the expression completion editor's frames, the slot preview's
three, the browser's feature rows). epaint asserts against that in debug
builds, so they failed under the dev profile and passed only where debug
assertions are off. They clear the delta as the other tests do; one more
hover test that dropped its output the same way does too.
* claim: #4083
* Feature editors refill named edges and faces where they are now (#4083)
After an upstream sketch change, Fillet, Chamfer, Shell and Draft evaluated
correctly, but their editors refilled the originally clicked points and
could then fail with "no edge at ...". Resolve the stored names through the
document's naming helpers first, keep the resolved index, and map the pick
into the displayed body's frame; the point lookup stays as the fallback.
* claim: #4097
* Sketch fillet keeps a corner placed by an expression (#4097)
Filleting a corner whose position comes from parameter expressions deleted
the point and its expressions, so the sketch gained two degrees of freedom
and later parameter changes distorted the outline. keep_corner now also
keeps fixed or expression-positioned points as virtual sharp corners.
* claim: #4150
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Browser: a second click on a Document Settings entry closes its window (#4150)
Each entry under Document Settings only opened its window. A second click on
Units closed the menu on the press (the row is outside it) and opened it again
on the release; Precision left Preferences open; Parameters and Design
Properties came back as a fresh copy. Now a click on an entry whose window is
open closes it the way the window's own x does:
- Units: context_menu::toggle_for with the row as the opener, the fix#1522
made for the workspace button's menu. A right-click still opens it at the
pointer.
- Precision: prefs::close(app, true), the Cancel/x path that puts back what
changed since the window opened (now pub(crate)).
- Parameters and Design Properties: the window is dropped, as its x does. A
Parameters value typed but not applied yet is dropped with it, as today's
second click already drops it.
Fixes#4150
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #4353
* Renaming a plane keeps its projections, canvases and hidden state (#4353)
timeline.rename only renamed the feature in other features' references. A sketch's
projection of a construction plane, a canvas placed on it and the hidden list keep it
by name too, so they lost it: the sketch warned that the plane no longer exists, the
canvas left the view and a hidden plane showed again. Document::rename_feature_keys
renames it in sketch links and canvases, as rename_body_keys does for bodies; the
selection follows in the command, and the hidden list in Session::refresh_from, so a
hidden plane stays hidden through undo and redo too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #5269
* DXF: write and read a Bézier at its own degree (#5269)
The DXF writer and reader held a spline's degree to 7, the value MAX_BEZIER_DEGREE had before
#1642 raised it to 9. A G4 blend (one Bézier of degree 9) was written as a degree-7 SPLINE over
its first 8 control points, so a flat pattern's cut-out stopped 4.444 mm short in the file.
- seg_shape takes its limit from MAX_BEZIER_DEGREE: a Bézier is written whole, at its own degree.
- read_dxf reads SPLINE degrees 1 to MAX_BEZIER_DEGREE, the degrees a sketch spline takes.
- Tests: every Bézier degree 4 to 9, a degree-9 round trip, the reader's degrees, hostile SPLINE
degrees and too few control points, the G4 cut-out exported and read back with its areas, and a
degree-9 SPLINE with 2 to 10 points inserted into a sketch.
Fixes#5269
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #3989
* ui-egui tests: offscreen GPU tests start their devices one at a time
Every offscreen test started a wgpu instance of its own, and starting one
makes the Vulkan loader scan its drivers. With several test threads two
tests doing that at once crashed the test process with SIGSEGV inside the
loader (loader_icd_scan). offscreen_device now holds a process-wide lock
while it starts the instance, adapter and device; each test keeps its own
device and the rendering still runs side by side.
A new Graphics preference, "Show reference geometry through bodies", draws
inactive sketch contours, origin and construction planes and their axes over
shaded bodies. It is off by default, so the viewport looks as before: reference
geometry is depth-tested and a section leaves it whole (#1229); Create Sketch
and dialogs picking a plane still draw the planes through the model.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #5295
* An empty intersect is refused instead of deleting the body (#5295)
An intersect whose boolean came out empty removed the target body and
reported success. The shared evaluator now refuses it when nothing of a
named target would remain, when nothing of any target would remain, or
when the tools meet a target and the result is still empty. An unnamed
target the tools miss altogether is still removed, as an intersect with
every body does (#5160). Sheet metal flat machining follows the same rule.
* Sheet metal: an intersect with already removed material is refused (#5295)
* claim: #133
* mesh: Save As Mesh declares the panel that shows it (#133)
The ribbon redesign (#2176) removed the MESH tab, but three things still
pointed at it:
- `file.save_mesh` declared `.at("MESH", "MODIFY")`. The command inventory
prints the placement a spec declares, so `docs/features.md` advertised a
`MESH > MODIFY` location no ribbon has. Its real home is already
INSERT / FILES (`SECTIONS` lists it), so the declaration now agrees with
the ribbon; `destination()` resolved it there before and after, so nothing
moves on screen.
- The user guide sent people to "the Mesh tab under Modify" for Save as mesh.
- A test now refuses any command that declares a tab the ribbon removed, and
pins where Save As Mesh lives.
`PLACEMENTS` keeps its MESH entries: those migrate toolbars users saved under
the old panels, which is a different question from what a spec declares.
---------
Co-authored-by: Geraldrunion <geraldrunion@users.noreply.github.com>
* claim: #4563
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Section Analysis: a Hatch option, off for a plain cut in the body's colour (#4563)
The Section Analysis dialog gets a Hatch check box beside Flip, and
inspect.section a hatch?: bool (default true), so scripts, the CLI and the
control channel reach it too. Hatched, a cut is its body's colour tinted
toward the cap colour with 45 degree stripes, as before; off, it is the
body's own solid colour. The viewport (cap alpha in the uniform), the CPU
rasterizer and ui.render follow it, and the section keeps it when hidden
and shown again or edited.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #4589
* Expression suggestions stay closed while another control is dragged (#4589)
A drag elsewhere leaves a text box its focus (egui gives it up on a click,
not a drag), so dragging the Drive Joints slider rewrote the focused Angle
box under its old selection. A letter before the caret ("5.0 d|eg") made a
new word, a fresh undismissed popup, and the list opened over the slider.
Dismissed suggestions now stay closed until the box is typed in, a drag of
another widget dismisses them, and a hidden list forgets its row rects.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #4609
* Mesh Convert works without activating the component first (#4609)
Inserting a mesh puts it in a new component while the parent stays
active, so Convert Mesh always refused the first time. Convert now
resolves the mesh body's owning component and adds the conversion
there when the active component is that component or an ancestor,
leaving the active component unchanged. From another branch it still
refuses, and the error names the component to activate.
* Control: a whole-design measure reads a copy off the window thread
`inspect.measure` and `document.inspect` (which measures every body by
default) ran on the window thread. On a large assembly that took about a
minute, the window answered nothing meanwhile, and Windows closed it as
not responding (#4604). They change nothing, so they now run on a copy of
the session on a worker and are answered from there, like exports, but
whatever the "agent commands off the window thread" preference says: an
answer from the design as it was when asked has no order to keep. The
window keeps answering `ui.*` and drawing meanwhile.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Control: clearance and interference checks read a copy off the window thread too
inspect.clearance and inspect.interference only read the design, and on a
large assembly measure every pair of bodies: on a 153-body design a clearance
check computed for over five minutes on the window thread, the window showing
"not responding". They join inspect.measure and document.inspect in running on
a copy, answered from there.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #4030
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Sketch Symmetry: a dimensioned shape slides onto the axis (#4030)
With a rectangle's width dimensioned, Symmetry of its two sides about an
axis was refused: the second side can't move on its own, the axis never
moves, and the ordinary solve that slides the whole rectangle only ran
when every first-picked entity could move. One movable first entity is
now enough.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #4328
* Fix sheet conversion picks on tangent bend edges (#4328)
Resolve cylindrical bends through the nearest edge's incident faces when a
pick on a tangent boundary resolves to its planar panel. This keeps valid
remote bends connected, including picks on the other skin of a shell.
Keep conversion geometry and picks in the body's local frame, and format
conversion diagnostics through its world placement. Cover rotated bend
picks and disconnected-pick coordinates with synthetic regressions.
A join failed with the kernel's internal error "tolerance must be no less
than 1e-6" at vendor/truck-geometry/src/decorators/intersection_curve.rs:172:
IntersectionCurve::parameter_division asserted truck's minimum tolerance,
and the guard turned the panic into a failed operation. A tolerance below
truck's TOLERANCE, or one that is not a number, now divides at TOLERANCE:
the curve's points are only found to within it anyway.
The test divides the intersection edges of a hole cut through a B-spline
face at 5e-7, 1e-12, 0, -1 and NaN: each gives the division at 1e-6. Without
the fix it panics with the reported message at the reported line.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #4593
* Pattern and mirror: a rib as the source (#4593)
A pattern or mirror of features refused a rib as a source ("Rib1
cannot be patterned or mirrored yet"). A rib's walls run until they
meet the body, so each copy is built again where it is: the rib's
sketch is moved by the copy's transform and the walls are run against
the bodies there, as a hole is drilled again at each copy. The copies
join the body, as the rib does. The rib's own tool building moves into
rib_tools, which the rib feature calls unmoved.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Sketch Point: a point put on a midpoint or a curve snap is held there (#4691)
The Point tool sent only a snapped point's coordinates and sketch.point
took only [x, y], so a point clicked on a line's midpoint (or on a curve,
or a model vertex) was free to move off it. sketch.point now takes the
snap forms the other draw commands take ("mid:<line>", "on:<curve>:x,y",
{vertex}, {on_edge}); the snap makes the point with its constraint. The
tool passes those snaps as themselves; a snap to an existing point still
goes as coordinates, where `connect` joins them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Sketch Point: refuse a snap it can't read instead of answering as if it made a point
sketch_project::snap_points leaves a snap it can't parse ("on:c1:abc",
"on:c1", "on:c1:1,2,3") as it was given, and sketch.point then read
that string back as the point it had made: no error, no point, and an id
a script would go on to dimension. It now takes the snap only when it
names a point of the sketch, and otherwise refuses, listing the snap
forms (found by the test agent on #4745).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The app's background loader (file_load::control) kept only `up` and
`names` of a control-channel open or import, so file.insert_step's
`matrix` and `name` were dropped and the part landed at the origin, and
file.insert_mesh lost all its options. It keeps every parameter but the
path, as given; each command checks them as it does when run directly.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
timeline.rollback / timeline.roll_to read a negative position as 0, so
an agent passing -1 for "one step back" rolled the whole timeline back
and every body disappeared, with no error. A negative position is now
refused. The two commands shared one function that refused in
roll_to's name; each now refuses in its own.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Nine messages: the pattern tools, their headings and two sample
descriptions. "Réseau" can be mistaken for a network.
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* claim: #4452
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Combine takes bodies clicked in the Browser (#4452)
A body row in the Browser only replaced the selection, so with Combine
open its target and tools stayed empty and overlapping bodies could only
be picked in the view. When the open dialog's active input takes bodies,
a body row now goes there, as plane and axis rows already do.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Combine Browser picks: test both layouts and control-channel clicks (#4452)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The Keyboard Shortcuts page refused a key another command used, even a
sketch tool and a model command that never run in the same mode, so
Shift+S could not open Create Sketch in the model and place a point in a
sketch. Edited by hand, the press ran Create Sketch inside the sketch.
A sketch tool (SKETCH tab) and a model command (the other tabs) may now
share a key. A press runs the enabled command of the mode the view is in
first: the tool in a sketch, the model command outside. Two commands of
one mode still clash, and commands on no tab (undo, views, files) clash
with everything.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #4783
* Physical material: one rule for a body's material and its density (#4783)
inspect.measure, component.properties, occurrence.properties and the
Properties window took a body's material name from the raw assignment,
but its density from Document::density, which falls back to the default
material when the assigned one is unknown or has an unusable density. So
a damaged or hand-edited design read one material and weighed another: a
design material "Broken" at -3 g/cm³ read "Broken" at 1.29 g/cm³.
One rule, in one place:
- DEFAULT_MATERIAL (new, beside DEFAULT_DENSITY in the material library)
is the default material's name. The library's first row uses it.
- Document::body_material (new, beside Document::density) is the material
a body's mass is computed with, as the library or the design spells it:
the assigned one when it is known and has a usable density, else
DEFAULT_MATERIAL. Document::density is that material's density, with
the same value as before for every input.
inspect.measure and the physical properties name body_material.
inspect.measure now names every body's material, measured or not, since
naming it takes no measuring; the density stays with the mass, on the
bodies measured. So the Properties window shows the engine's name without
a fallback of its own. The places that keep the assignment as stored
(configuration cells, material.assign, material.edit, material.delete
and the dialogs) only use the constant, so a configuration still keeps a
damaged assignment as it is.
The visible change: a body whose material is unknown, or has an unusable
density, reads "Default", the material it weighs, and a miscased name
reads as the library or the design spells it. A body inspect.measure
can't measure now has its material too. Masses and densities don't
change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The Move dialog's floating value box always showed and edited the X
distance, whichever arrow was dragged, and Tab left the box for another
widget. Now:
- dragging an arrow shows that axis's distance, turning a ring (Free Move)
that axis's angle; a plane square or the ball leaves the box as it is;
- Tab in the box goes to the next value (X, Y, Z, then in Free Move the
X, Y and Z angles) and Shift+Tab back, the keyboard staying in the box.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #2987
* Flange on a plain solid says how to start sheet metal (#2987)
Flange, Flat Pattern, and Export DXF on a design with no sheet metal body
now say how to make one: Flange > Base, or Convert to Sheet Metal on a
plate's large flat face. An edge flange checks for a sheet metal body
before it adds a feature. The message is in the catalogs, and engine
messages that match a catalog entry are shown in the interface language.
* Dialogs show a refusal without the engine's parameter prefix (#2987)
The Flange dialog showed "invalid parameters for `sheet.flange`: " in front
of the new guidance. The dialog already names its command, so the red
refusal text leaves that prefix out and follows the interface language
when the message is in the catalogs. Create Issue still gets the full text.
* claim: #4116
* Web: a trackpad swipe pans instead of zooming (#4116)
Browsers deliver trackpad swipes as wheel events without scroll phases, so
the shared navigation treated every vertical swipe as a mouse-wheel zoom.
The web raw-input hook now brackets point-mode swipes (small or horizontal
deltas) with gesture start/end phases, ending after 100 ms idle or before
line/page wheel or pinch input. Wheel notches and pinch still zoom, and
camera input still goes through navigation::handle.
* claim: #221
* Units of new designs apply to the blank design already open (#221)
Changing Preferences > Units of new designs only took effect at the next
File > New, so the empty design open at launch stayed in millimetres. When
the preference changes (the dialog, Cancel, Restore Defaults, or
preferences.set) and the open design is blank and unsaved, it takes the new
units at once, without an undo step or unsaved changes. A design with
content or a file keeps its units.
* claim: #4794
* Hem: the direction arrow points the way the hem builds (#4794)
The Hem dialog shared the edge flange's arrow, which points out of the
sheet's larger neighbouring face. A hem folds back the other way, so the
arrow pointed away from the material it adds. Flip the hem's arrow so it
matches the geometry; edge flanges keep theirs.
A regression test compares the arrow with the side the finished hem adds
material on, for both Flip states, top and bottom edge picks, and both
dialog layouts.
* claim: #3220
* Model cache: keep fewer states of a design too big for its file instead of none (#3220)
A model cache over the container's budget keeps the final state, then the
slowest features' states while the bodies they add fit (a body several
states hold counts once), and is left out only when not even the final
state fits. Opening evaluates just the features whose state the file left
out, each from the state before it, so the model is whole afterwards.
* Model cache: keep each encoded body apart, copy only what a cache stores
Encoding a model too big for its file no longer copies every distinct body
into one buffer before thinning: the payload is copied from the encodings
once its size fits, so saving holds no more than before.
* Model cache tests: canonical runs in the timeline fixture; rustfmt
* Model cache: count the records a thinned cache keeps; always try the final state
Thinning counted only the bodies, so a model of many copies of one part
(records, not bodies, filling the room) shrank its budget to nothing and
saved no cache although its final state fitted. The choice now counts the
records of what it keeps, always keeps the final state, and says what it
counted, so each retry keeps less and the final state alone is always tried.
* docs: a thinned model cache counts its records and always tries the final state
* Model cache test: the slowest feature's state holds bodies 16 to 23
* Model cache: thinned opens with fingerprints that read the state before a feature
Since #3397 a feature's fingerprint reads where the bodies before it lie.
After a state the file left out, adopt takes the saved fingerprint and the
evaluation that works the state out checks it. A result evaluated ahead
is never taken as one the file left out. The thinned round trips include
the placed components' design, whose cut reads those placements.
---------
Co-authored-by: MateuszSychXpl <151755914+MateuszSychXpl@users.noreply.github.com>
* claim: #4880
* Rename a body: keep the names of siblings a later feature removed (#4880)
Renaming a body sets the stored names of all bodies its feature made, so
the siblings are not renumbered. Those names were read from the final
model, where a body that a later feature removed no longer exists: its
stored name was lost, it came back as `Ext (3)`, and the later Remove
failed with "no body `Body3`". Read the siblings from the feature's own
result instead, as the later features already are.
* Test: renaming one half of a split leaves the other
Renaming one half keeps the other half's name, also once a later feature
joins that other half away, after both an incremental and a full
evaluation.
* claim: #4846
* MCP tools: unknown command ids suggest the closest ids (#4846)
An unknown command id in `execute` or `batch` now lists up to three closest
ids, and an id with parameters packed into it says to pass them in `params`.
* claim: #4804
* Keyboard Shortcuts page: draw only the visible rows (#4804)
Off-screen rows keep their height but skip their cells, the list is
filtered once before measuring and drawing, and each row's shortcut
comes from the command spec already in hand instead of a registry
lookup.
* claim: #4899
* Sketch: offer Break Offset Association for offsets of lines (#4899)
Trim and Extend refuse an associated offset output and point to Break
Offset Association, but the right-click menu offered it only for
free-form offsets. Offer it whenever the curve is an offset output
(analytic, topology or free-form), using the sketch's shared lookup.
* claim: #4851
* solidcraft-cli bench-assistant: the standard Assistant model test
Runs the eleven v1 tasks of #4851 against a desktop app that is already
running with a control port. Each task starts a new design and a new chat
with All permissions, runs only the prescribed setup, sends the prompt
once and waits for the turn. The result is checked on the design
(document.inspect with measurements): body count, volume within 1%
(0.3% for the fillet), bounding-box size in any orientation, the lowest
corner, the body name, live feature kinds, and changed or unchanged
content; the volume question is checked on the reply. Failures get the
issue's categories from the conversation's items. Prints a table, and
writes the issue's comment template (--markdown) and the raw results
with each conversation (--json).
Dropping a feature just behind the history marker now rolls it back, and
dropping it just before the marker makes it active again, in both the
bottom strip and the left panel. timeline.reorder and timeline.move_many
take an optional `active` param that sets the marker in the same command,
so the move is one undo step and one recompute. A position on the wrong
side of the marker for the requested `active` is refused.
Drop feedback:
- bottom strip: the drop line sits centred between two features (it
used to touch the previous one) and shows at the first position
(it used to be drawn outside the strip there)
- left panel: a drop slot below the marker when it is at the end
- no drop line when the marker is dragged back to where it is
- a dragged feature is outlined in the drop line's green
- grab cursor on the marker, grabbing cursor while dragging the marker
or a feature
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #4932
* icon_audit: compare the requested-icons manifest with CRLF checkouts ignored (#4932)
On Windows, Git for Windows checks out requested_icons.txt with CRLF line
endings (core.autocrlf; the repo has no .gitattributes), while the test
builds the manifest with \n, so the comparison fails although the lists
are the same. Read the file with \r\n normalised to \n, as #1931 did for
the icon tools. Regenerating still writes \n.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #4966
* Agent chat: serve the bundled skills' reference through an MCP tool (#4966)
The bundled skills start with "read reference.md", but SolidCraft's own
chat turns the agent's file tools off, so its agent could not follow them.
The MCP server gains a read-only `skill_reference` tool that returns a
skill's documents from copies built into the binary. It takes a skill
name and a file name from a fixed list, never a path, and refuses an
unknown skill or file with the available ones. The skills point to it,
and docs/mcp.md and skills/README.md describe it.
* test(mcp): reproduce whitespace bypass of request-size limit
Add a source-level regression that rejects a JSON-RPC request larger than
MAX_LINE even when most of it is whitespace, plus bounded-stdio framing
coverage and invalid UTF-8 recovery.
* fix(mcp): bound stdio line buffering and validate raw request size
Consume oversize JSON-RPC lines in bounded memory while keeping the
following request available. Reject whitespace-padded lines before
trimming, preventing a request-size cap bypass.
* docs(mcp): keep server framing comment idiomatic
* claim: #4585
* Sketch drag: a linkage held by redundant parallelograms still moves (#4585)
Cursor motion looks for the sketch's freedom in the null space of the
constraint Jacobian. Redundant constraints (a parallelogram held both
parallel and equal on each pair of sides) are dependent only at an exact
solution; after a correction to the solver's tolerance their rows differ
by about 1e-9, which the 1e-9 pivot tolerance counted as rank, so the
null space came out empty and a one-DoF linkage stayed locked. Use the
rank analysis' 1e-7, so the drag sees the freedom the sketch reports.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #4991
* Browser: Shift+click above the anchor selects the whole range (#4991)
The browser rebuilds its row order while drawing, and a Shift click looked
the anchor up in that half-built list, so an anchor further down was not
there yet and only the clicked row was selected. Resolve the range against
the previous frame's complete row order instead.
* claim: #5005
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Timeline: delete the selected features, Shift+click ranges, delete after the history marker (#5005)
Delete in a feature's menu deletes the selected features when the
right-clicked one is among them ("Delete 3 Selected Features").
Shift+click selects the range from the last feature clicked (or the last
selected one), in the order taken before drawing. The menu of a feature
after the history marker deletes every feature after the marker, with
their number in the label. All of it goes through delete_features: one
timeline.delete and one undo step. The confirmation no longer lists
features that are themselves being deleted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #5015
Co-Authored-By: Claude Code <noreply@anthropic.com>
* Assistant: add Anthropic-compatible API agents (#5015)
The Add agent wizard now offers Anthropic-compatible endpoints beside OpenAI-compatible ones. The API backend speaks the Anthropic Messages protocol with the same MCP bridge, tool gate, limits, undo and keychain rules: x-api-key and anthropic-version, a top-level system note, input_schema tools, content-block answers and one user tool_result message per round. Test sends a short message because compatible endpoints (Qwen's) need not list models.
Co-Authored-By: Claude Code <noreply@anthropic.com>
---------
Co-authored-by: Claude Code <noreply@anthropic.com>
* claim: #5021
* Opening a design: range-check integer-valued coordinates too (#5021)
The file-level number check skips every integer-valued number so that ids
and counters may be large, which let a coordinate such as
9223372036854775808 through. The typed design is now checked as well:
float fields (coordinates, sizes) are serialized as floats and must be
within the same +-1e9 limit, while integer fields stay integers and are
not limited.
* claim: #5022
* Plastic features: refuse sizes under 0.01 mm (#5022)
A snap fit with a near-zero thickness (1e-9 mm) got past the size checks,
which only asked for a positive number, and panicked in the kernel's
intersection code. The boss, lip, groove and rest had the same gap.
Every length of a plastic feature that becomes geometry (snap fit length,
thickness, width, catch_depth and catch_length; boss diameter, height,
hole_diameter, hole_depth and rib sizes; lip and groove width and height;
rest width, length, height and thickness) must now be at least 0.01 mm.
The check sits where the feature is evaluated, so a parameter changed later
is refused the same way, and the message names the parameter.
* claim: #5023
* Extrude and loft: refuse repeated profile indexes (#5023)
`profiles: [0, 0, ...]` with 10 000 copies built 10 000 identical bodies in
about 35 s, and a loft section listing one profile 10 000 times took 25 s
before it was refused.
A list of profile indexes now refuses a repeated index while it is parsed
("profile 0 is listed twice"), as offset association ids already were.
Before the profiles are resolved, a selection naming more profiles than its
sketch has, an index the sketch doesn't have, or two points in the same
profile is refused too. A design file that repeats an index still opens,
using the profile once.
Catch up with en.json: the new Preferences pages, assistant settings,
3D mouse axis assignment, bug report form, history names and the tool
help texts. pl now covers every message of en.json.
Coincident with two circles (or arcs) picked was refused: "two curves must both
be straight lines; otherwise choose a point and a curve". It now lays the second
pick on the first: Concentric and Equal together, in one step, with the first
staying put. A pair already concentric or equal gets only the other half; one
already lying on the other is refused as nothing to add.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* A renamed body keeps its number (#4957)
The design's body counter starts over at every evaluation, so a body
renamed after a join (the join changed it, so the rename cannot be
followed without evaluating the design again) gave its number back: the
next body took the name the rename freed, and a script that names a body
found another one.
The design now saves the names a rename freed, and the counter skips
them.
* A renamed body keeps its number (#4957)
The design's body counter starts over at every evaluation, so a body
renamed after a join (the join changed it, so the rename cannot be
followed without evaluating the design again) gave its number back: the
next body took the name the rename freed, and a script that names a body
found another one.
The design now saves the names a rename freed, and the counter skips
them.
* Format
A favourite is listed twice, under Favourites and under User. Double-clicking its name opened a
name editor in both rows, under one widget id; the two took each other's focus, one lost it at
once, and the rename closed with the name unchanged. The rename now opens only in the table it
was started in, under an id that includes that table.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #5087
* view.apply: keep the camera's roll, and refuse an up axis (#5087)
An explicit camera was read as a named view, which has no roll or up, so
roll was silently reset to 0 and up was ignored. Read roll as a finite
number in radians, clamped to ±π, and refuse up with a message saying the
up axis comes from the design settings.
A docked dialog is placed from the previous frame's drag offset, so a fast
drag can leave the pointer outside the dialog, over the model. The viewport
then treated the pointer as hovering the model and highlighted faces and
edges under it.
drag_dock now marks the frame in egui temp data while its title band is
dragged, and navigation::handle treats the pointer as not hovering the view
while that mark is current and a button is held. This covers every dialog
docked through drag_dock.
NVIDIA's EGL answers wgpu's request for a GL 3.3 context with exactly 3.3
(GLSL 3.30) yet lists GL_ARB_compute_shader. wgpu-hal then reports compute
shaders, wgpu-core builds its indirect-call validation pipeline, naga cannot
emit compute GLSL below 4.30, and request_device fails as Device(Lost).
The test draws nothing indirect, so it opts out of that validation.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #5049
* GPU tessellation: an empty plan no longer fails buffer validation (#5049)
Empty tessellation inputs were padded to 16 bytes, smaller than the
shader's 32-byte vertex and 48-byte surface records, so wgpu refused the
dispatch. Skip the dispatch when there is nothing to evaluate, and pad
each empty buffer to one whole element of what the shader reads.
The component's sketches, features, bodies, subcomponents, the joints among
them and the parameters they use move to a new .solidcraft design, keeping
their ids and names, and the component becomes linked to it as Insert Design
makes it. The new design must build the same bodies and the assembly must
look the same afterwards, else nothing changes and no file is written. Joint
faces on the component are found again on the linked bodies, and components
that build the same bodies can become placements of the link.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A STEP file inserted into a new design sits in its own component under the
empty root, and a sketch on its face must join that part going out and cut it
going in rather than make a separate body beside it. With a distance, main
already resolves this; Through All with no distance gave `auto` no reach to
probe, so it fell back to New Body and cut nothing. Through All now reaches
past every body in its direction, and `auto` reads it as a cut (#1234).
Co-authored-by: Steven <steven2021067613@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The 14 new built-in themes still listed four logo colours, so none of them read after #5465 and main's theme test failed.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #5461
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* App icon: draw the borderless mark everywhere (#5461)
app_icon.rs drew the older mark with white lines between the faces and a
white outline around it, so the Windows, Linux, web and README icons did
not match the macOS icon, the social card or the title bar. It now draws
the borderless mark from the faces and gradients of the macOS artwork
(03-solid-blue-cutout.svg): five faces with transparent gaps between them.
The filler fills even-odd spans so the concave faces work.
The Linux hicolor PNGs and both Windows .ico files are regenerated with
`cargo xtask app-icons`. The README logo, the web app's icons and every
package pick them up from there.
The logo colour token's fourth entry (the white edge) is gone: nothing
draws it any more.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The agent cursor only acted out engine commands: pointer input sent over the control channel
reached the app as synthetic events without moving it, so an agent's clicks, right-clicks and
drags were invisible to a person watching. While the cursor is shown, each synthetic pointer event
now moves it to where the input lands, a press leaves a ripple, and a drag is followed step by
step. A request being acted out keeps the pointer; with the cursor hidden nothing changes.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #5172
* Extrude without profiles keeps nested loops as holes (#5172)
With no `profiles`, solid.extrude took every region of the sketch, so
circles drawn inside a plate outline were extruded as well and filled the
plate's holes. The default now applies even-odd nesting to the drawn
profiles: a loop inside an outline is its hole, a loop inside that hole is
material again, and separate outlines all extrude. Sketches without nesting
keep the `all` selection, and explicit `profiles` (including "all") are
unchanged. Saved designs store their selection, so they recompute as before.
* claim: #5174
* component.create refuses parameters it does not take (#5174)
A call with `bodies` (or a misspelled key) used to succeed and make an empty
component. Unknown keys are now refused before anything changes, the error
lists the accepted parameters, and `bodies` points to component.move_bodies
and component.from_bodies.
* claim: #5175
* Joints: a joint origin's name given as a face says to pass it as the origin (#5175)
When the name in `face` or `circle` matches no face but is a joint origin,
the error now says so instead of "no face is named".
* Viewport: show the picture it drew last when nothing changed (#4820)
A frame redrew the whole scene whenever egui asked for a frame, even when
the camera, scene, highlight and overlays were identical: pointer moves over
empty space and UI animations over a still view paid for the full scene.
The renderer now hashes everything the picture depends on (scene keys,
camera, size, section, images, analysis, accessibility map, occlusion) and
blits the last offscreen picture when the hash matches. The first still
frame filters a supersampled picture down once and later frames copy it as
it is. A frame with the same content costs a blit instead of a scene draw.
Co-Authored-By: Claude Code <noreply@anthropic.com>
* Viewport: draw the scene offscreen, into a 1x window pass (#4820)
The scene keeps the app's depth, stencil and MSAA, but egui's window pass now
has one sample and no depth. Whenever the two targets differ, the scene
(product previews too) is drawn offscreen and blitted, so the UI pass no
longer clears and resolves a multisampled window.
Over 8 s of pointer moves on the 450E rotor head, the app's GPU intervals fell
from 1325 ms to 1006 ms and egui_render from 892 ms to 509 ms; idle stays at
0%. A GPU test checks the split path's picture matches the direct one.
Co-Authored-By: Claude Code <noreply@anthropic.com>
* 3D mouse: a repeated report at rest asks for no frame (#4820)
A device may repeat its latest report while the cap rests. Publishing it
again asked egui for a frame although nothing changed. Only a report that
changes the reading now asks for one; while the cap moves, the view asks for
the next frame itself.
Co-Authored-By: Claude Code <noreply@anthropic.com>
---------
Co-authored-by: Claude Code <noreply@anthropic.com>
The File view's recent row already shows its path on hover; Home added
a second hover text with the raw path, so both appeared.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Add the format registry: one table of formats, extensions and options in crates/io.
The hosts' Open-dialog filters and the web accept list derive from it, and
registry tests pin it to the path predicates and the command docs.
* Fix format-registry tests that compared FormatSpec without Eq.
assert_eq on Option<&FormatSpec> does not compile; the none cases
only need is_none, matching the other rows that compare ids.
* claim: #5230
* Viewport: draw the ground shadow on the direct path (#5230)
The ground shadow went into the model's translucent triangles, so every
frame with a body counted as see-through and took the order-independent
transparency path. Give it its own batch, drawn alpha-blended after the
opaque geometry as before, so only truly see-through bodies use that path.
* claim: #5248
* feat(motion): regenerate kept traces when source inputs change
* feat(motion): add a parametric Chebyshev trace demo
* perf(joints): reuse face measurements across motion samples
Face-centred joint origins called Body::faces on every solve, bypassing the
reference-mesh reuse used for rigid placements. Share face measurements with
unchanged model geometry at the existing tolerance, while guarding replaced
bodies and applying offsets after the cached measurements.
Cover repeated named/nearest-face solves, shape edits, retained snapshots and
direct replacement with deterministic work counts. The 145-point Chebyshev
dimension and range updates retain identical sampled and interpolated curves.
* perf(naming): skip preparation for cached face names
Both face lookup paths spawned fresh workers over every body even when their name cells were ready. Prepare only cold cells, with direct handling of zero or one cold body. Preserve the existing parallel naming path and all face selection semantics.
Deterministic regressions cover repeated picked and unique-name lookups, misses, partial warmup, renames, snapshots and moved copies. Release Chebyshev medians fall from 859 to 50 ms for a dimension edit and 871 to 34 ms for a sweep edit; all 145 samples and interpolated points remain identical. Full cargo xtask ci passes with RUST_TEST_THREADS=4 and the existing local Windows validation overlays.
* claim: #5287
* web-offline-test: wait only for the files the browser can ask for at once
The fail-fast step waited for every other file of build v2 to reach the
server. Chrome sends at most six requests to one server at a time, and
the page's failure cancels the download, with it the files still waiting
for a connection. A real build keeps nine files, so only six were ever
asked for and the Pages job failed with "only 6 of 8 slow files were
asked for" whenever the installing worker didn't happen to ask again.
Wait for as many as can be in flight (less the page's connection), and
give the stand-in app more files than that, so the default run covers it.
* claim: #5227
* Web: leave Ctrl+Tab and Ctrl+Shift+Tab to the browser (#5227)
eframe cancels the default action of every Tab keydown on its canvas
(egui moves focus with Tab), so browsers that let a page cancel their
shortcuts, such as Vivaldi, could not switch tabs while SolidCraft had
the keyboard. A capture listener on the window now stops Ctrl+Tab and
Ctrl+Shift+Tab before they reach the canvas, without cancelling them,
so the browser acts on them as usual. Plain Tab and Shift+Tab still
move between the app's fields. Desktop builds are unchanged.
web-api-test checks it in headless Chrome: Ctrl+Tab and Ctrl+Shift+Tab
keydowns are not cancelled, Tab still is.
---------
Co-authored-by: MateuszSychXpl <151755914+MateuszSychXpl@users.noreply.github.com>
* claim: #4765
* Agent chat: explain the Flatpak sandbox when no agent is found (#4765)
In the Flatpak the sandbox has no home-folder or network access, so agents
installed through nvm or ~/.local/bin are never discovered and the chat only
said that no agent was found. With FLATPAK_ID set and nothing discovered, the
picker and Preferences > AI now say why and offer the exact
`flatpak override --user ai.storyteller.solidcraft ...` command with a Copy
button. The message is in every catalog, and docs/agent-chat.md gains a
Flatpak section listing what each permission is for. The manifest is
unchanged.
* claim: #5150
* Stand it up: don't offer it for SolidCraft's own STEP files (#5150)
STEP replies from file.insert_step and doc.open now carry the file's
originating system, and the window skips the stand-up offer when it is
exactly SolidCraft, as SolidCraft's STEP export writes it.
* Stand up Y-up STEP and IGES files from SolidWorks automatically
The STEP header (originating system, preprocessor version, FILE_DESCRIPTION) and the IGES
global section's native system ID say which program wrote a file. A SolidWorks header means
Y up: the import's component is stood up as it opens, as one undo step, and the message offers
Undo. SolidCraft's own files are left alone; an unknown exporter keeps the offer.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Ray <raykholo@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
The viewport's frame-rate readout asked for a new frame every frame, so with
it on an untouched window redrew as fast as the display allowed and kept the
GPU busy: 77% GPU at 60 fps on an M1 with a 5K display showing the sample,
against 0% with the readout off. It now asks for no frames of its own and
shows how often the view really draws; one frame a little over a second after
the frames stop shows the view idle at 0 FPS. The preference's note no longer
says the view redraws continuously.
The status toast also asked for a frame every half second for the six
seconds it shows; it now asks for one frame, when it is due to go.
* claim: #4172
* GPU: a lost device stops drawing instead of crashing (#4172)
After the driver resets the GPU (Windows TDR), the first queue write to meet the
lost device panicked inside wgpu-core (PendingWrites::activate unwrapping
DeviceError::Lost). Nothing watched the device for its loss.
- egui-wgpu: RenderState::device_lost (DeviceLost) is set by the device's lost
callback. The painter draws nothing on a lost device, and before a frame's
uploads it submits nothing (DeviceLost::probe), so a loss the driver reports
is found by that submission instead of the first write.
- Pictures (thumbnails of saves, ui.render) watch the same flag: on a lost
device the capturer refuses with GpuError::DeviceLost without touching it,
and the picture comes from the CPU rasterizer. Headless capturers watch
their own devices.
- The app, seeing the loss, closes keeping unsaved changes for recovery (as a
forced quit does) and starts again; the new start says what happened. A
second loss within ten minutes starts on the software renderer. The loss is
noted in the crash journal.
`Body::size` walked a mesh body's vertices on every call, and it is asked
for every frame. A mesh body's triangles never change after `mesh_body` or
`mesh_transformed` makes it, so both now measure it then (`TriMesh::span`)
and `size` returns it like a B-rep body's. `ModelBody::mesh` returns a mesh
it already has before working out the display tolerance.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* claim: #4107
* Keep bottom viewport overlays above the floating History card (#4107)
In the floating layout the History card covered the analysis legend and
the frame-rate readout (#3896). One bottom inset, computed from the
History card (zero when History is hidden or docked), now positions every
bottom-anchored viewport overlay: the surface-analysis and Continuity
legends, the frame-rate readout, the command-progress chip and the
navigation bar fallback.
On main, 120 label literals (95 distinct) can't be translated in any
locale: the English catalog doesn't reach them. Catalogue them in en,
ja, zh and de, translate indented row labels once for display and
measuring, look up the thread profile by its id, and give each
navigation preset's help, shown by the mouse mapping picker, a catalog
message.
A guard tokenizes the UI sources and checks every label literal of a
kit call, of context_menu::button and of tr_label/tr/SelInput against
the English catalog, resolving kit calls by their imports.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #3567
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Yz6dHZiXcgTgrxjVNC6hx
* Edit Offset Plane: keep a face base, refuse a plane on itself (#3567)
Reopening an Offset Plane built on a face looked the face up at the
plane's origin, which can lie off the face, so the Plane input came up
empty. Look it up at the point picked on the face. A redefine that
builds a feature on itself is refused with a plain message instead of
failing with "plane offsets nested too deeply".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Yz6dHZiXcgTgrxjVNC6hx
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #2874
* Sketch fillet and chamfer refuse an offset's corners (#2874)
An analytic offset's output ends are recomputed from its source curves on
every solve and held fixed there. Filleting an output corner moved those
ends to the tangent points, the next refresh pulled them back to the sharp
corner, and the new arcs closed into full circles with the sketch left
unsolved.
Fillet and chamfer now refuse a corner on an offset output before anything
changes (a batch fillet applies all of its corners or none) and say how to
get the rounded result: round the source corners before offsetting, or
break the offset association first, after which the same corners round
normally.
* claim: #2885
* Circular Pattern and Revolve: a construction axis can be the axis (#2885)
The dialogs send a picked axis by its name, but solid.pattern.circular
only knew X, Y and Z, and solid.revolve / surface.revolve read any other
name as a sketch line id. Picking a construction axis therefore failed.
- solid.pattern.circular takes a construction axis name, saves it as
`axis_feature` and turns about the axis as evaluated, so the copies
follow it when it is edited.
- AxisRef::Construction { name } for revolve and surface revolve,
resolved at evaluation and checked to lie in the sketch plane.
- construct.plane.at_angle accepts a construction axis name (its fixed
axis line is taken from the axis as it is now).
- Reopening these features shows the construction axis as the pick.
- Design format: pending/construction-axis-refs.rs.
* Construction axis references follow the axis in the feature's component frame (#4506)
- Revolve, Surface Revolve and Circular Pattern resolve a construction axis
where it is evaluated, mapped into the feature's component frame.
- A line of the revolved sketch with the same id as a construction axis
still wins, so existing scripts keep their meaning.
- Tests: the revolve keeps the reference through save and reopen and
follows an edited axis (60π to 80π mm³); perpendicular, offset and
profile-crossing axes are refused without changing the design.
* Joint drive moves parts without evaluating them
A joint drive (also the Drive dialog and motion steps) changes only occurrence transforms, yet it
re-evaluated nearly the whole timeline: every feature that might read another component's
geometry (a sketch with any link, even to its own axis) hashed every occurrence's transform into
its fingerprint, so any motion invalidated it and everything after it, and no new pose was ever
in the cache.
- Placement digest: a feature now hashes only where the geometry it actually reads lies relative
to its own frame (link sources, anchors, profiles and paths from other components, mirror
placements; To Object and face/edge-picked planes read every component, relatively). Moving
parts that keep those relative placements leaves every result in place: nothing is evaluated
or re-meshed. In-context features whose referenced part really moved still rebuild.
- Evaluated ahead, applied once: `Ahead::evaluate` returns what it evaluated and
`Session::prime` lets the window-thread pass take it (history commands and dialog OK), so an
off-thread command no longer costs twice on designs past the cache's budget.
- Contact meshes are kept per body geometry across drives.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Joint drive: booleans read only their input bodies' placements
A join, cut or intersect with named targets, and a Combine, read every
placement, so a joined part transferred to a component re-ran its boolean
at each new joint pose. Each now hashes only the placements of the bodies
it actually takes as inputs, resolved in its timeline input state (before
any later BodyTransfer), relative to the frame that evaluates it; Combine
reads its tools relative to its target. Unresolved and legacy component or
path reads keep the conservative dependency. 24 new joint_pose_ tests
cover new angles, transfers, targets, save/reopen, undo and real relative
motion (900 -> 800 mm3).
Supplement offered on #3397 by takuto-NA.
Co-Authored-By: takuto-NA <183465875+takuto-NA@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Creating a component recomputes nothing
A new, empty component changes nothing a feature reads, yet it changed
the placement fingerprint of every targeted boolean, Combine and To
Object feature, so `component.create` evaluated the timeline again:
- the first component of a design: `placement_digest` returned 0 while
the design had no placements and a nonzero value once it had one;
- To / From Object and legacy face- or edge-picked planes hashed every
component, not just those holding the bodies, sketches and
construction geometry the feature can reach.
Co-Authored-By: Tyler Chiriboga <18270301+Tyler-Chiriboga@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Test: creating a component after a legacy face sketch recomputes nothing
A face extruded after its construction certificate is gone (a patterned
cut, a pad joined flush with it, then one more cut) is sketched on a
legacy face plane, which reads every placed body. A new, empty component
must not change what it reads either (reported by Tyler Chiriboga).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: takuto-NA <183465875+takuto-NA@users.noreply.github.com>
Co-authored-by: Tyler Chiriboga <18270301+Tyler-Chiriboga@users.noreply.github.com>
* Viewport: levels of detail for large meshes (#4724)
A large instanced mesh (20 k triangles or more) gets coarser copies, made
once on a worker (`mesh_lod`): a quarter, a sixteenth and a sixty-fourth of
its triangles, by quadric edge collapse (`geom::simplify`, after Garland and
Heckbert 1997; no fold-overs, no sheets joined, open boundaries kept), each
with its measured deviation from the full mesh. Each frame, as placements
are culled, each is drawn from the coarsest copy whose deviation stays
under 0.75 px where it stands (`placement_scale`, `level_for`). Until the
copies are made the mesh is drawn in full; the scene is built again when
they are. Display only: picking, measuring and exports keep the reference
mesh. Outside the kernel.
25 placements of a 2 M-triangle STL, 3600 x 2190 px, 4x MSAA, M2 Max
(`instanced_scene_gpu_benchmark`): 20.2 -> 1.2 ms a frame with all in
view, 21.9 -> 2.9 ms close up. The copies take 10 s to make, on a worker.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Viewport: see-through and faded parts drawn as instances (#4888)
See-through (an appearance's opacity) and faded (context around an active
component) bodies in one colour are drawn as instances like opaque ones,
their alpha in each placement's colour: one copy of the triangles, levels
of detail and culling, no per-placement copies built on the window thread.
Their placements are ordered back to front each frame (not their
triangles one by one) for devices that sort see-through surfaces.
The GPU benchmark gains `SOLIDCRAFT_BENCH_OPACITY` and the CPU share of
each frame, and its pictures each placement's own triangles too.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Viewport: order-independent transparency for see-through surfaces (#4734)
The model's see-through surfaces (glass, faded context; instanced or not)
were sorted back to front triangle by triangle on the window thread
whenever the view turned. They are now drawn with weighted blended order-
independent transparency (McGuire and Bavoil 2013): when there are any,
the frame is drawn into the renderer's own textures (as supersampling
does), the see-through surfaces gathered into a weighted colour and a
revealage target against the frame's depth, laid over the frame in one
full-screen pass, then the overlays drawn over them. Nothing is sorted.
A device that cannot blend into float targets keeps the sorted way.
25 placements of a 2 M-triangle STL at opacity 0.5, M2 Max, 3600 x 2190 px:
about 10 s a frame sorted, 21.6 ms (17.2 ms instanced, 2.8 ms with levels
of detail).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* claim: #2614 (3D mouse: find devices on Windows without asking every HID device for its strings)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* 3D mouse: find devices on Windows without asking every HID device for its strings (#2614)
On Windows the 3D mouse reader rescanned every two seconds while no 3D
mouse was plugged in, with hidapi's refresh_devices(). hidapi's Windows
enumeration opens every HID interface and reads its serial number,
manufacturer and product strings: on USB, three control requests to
every keyboard, mouse and touchscreen, forever, on a machine without a
3D mouse. That fits the report of an idle machine whose USB keyboard
drops off and returns and whose cursor jumps.
Windows now asks Plug and Play which present HID interfaces are
multi-axis controllers (hardware ID HID_DEVICE_UP:0001_U:0008), which
sends nothing to any device, and opens only those with open_path().
hidapi's own discovery is turned off before the reader makes its one
context, so making it touches no device either. macOS keeps hidapi's
enumeration (IOKit's registry); Linux is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* AGENTS.md: list the Plug and Play scan among the files allowed unsafe code (#2614)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Tyler Chiriboga <18270301+Tyler-Chiriboga@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Finish Sketch, double Esc (including a blank sketch discarded), and Cancel no longer
restore the pre-sketch view. Entering a sketch still turns the view to face the plane.
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* claim: #2027
* Redesign the home page as the File view (#2027)
* Match the File view with the accepted component design (#2027)
* Restore the Start subtitle and simplify the recovery icon (#2027)
* Integrate File view with main's shared UI containers
Every edit clones the document for undo, and `MeshData`'s positions and
triangles (plain `Vec`s) were copied with it: ~165 MB a command for a 2 M-
triangle STL. They are now `Arc<[f64]>` and `Arc<[u32]>`, shared by clones;
the few places that rewrite them build new arrays. Saved designs are
unchanged (an `Arc<[T]>` serializes as the array).
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The pick buffer drew the model's surfaces and edge ids for every still
view to narrow which edges a pick tests. Measured against the BVHs alone
(release, M2 Max, 1400 x 900 view, 600 cursor positions), a pick costs
2-51 us through the BVHs and the tile saved 0-4 us of it, while each pass
cost 0.7-12.5 ms of GPU time per still view, plus gathering its scene on
the window thread, read-backs a frame or two late and repaints to wait for
them (and, before, a copy of every triangle, which made it refuse models
past 6 M triangles). Picks are unchanged: the BVHs give the same answers.
`pick_timing_on_curved_models` gains eight layers of bodies seen from above
(the pick buffer's best case) and `SOLIDCRAFT_BENCH_MESH` placements.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The highlight's key hashed the hovered item with its point under the
pointer, so every move across a face built and uploaded its highlight
again: for an imported mesh (one face) all its triangles, every frame.
The key now leaves out a face's and a plane's point, which the highlight
does not draw; an edge's middle stays in (it picks the part lit).
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`SharedTris::new` used the standard SipHash hasher and collected each
triangle's corners into a Vec: 451 ms for a 2 M-triangle STL on the window
thread. A quick hasher for the coordinates' bits and a fixed array make it
278 ms, and the background mesher that meshes an opened or inserted
file's bodies now welds their large meshes too (`warm_shared_tris`), so the
first scene that shows them takes the shared vertices ready-made.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`toolbar_custom::panel_lists` is asked for every frame and matched every
registered command against every section by string each time (~337 us a
call for MODEL, several calls a frame). The registry and the ribbon layout
never change while the app runs, so each tab's lists are kept: 0.2 us a call.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* claim: #4616
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Viewport: draw a part placed many times from one copy of its triangles (#4616)
A part placed by many occurrences was drawn, framed and picked from a moved
copy of its mesh per placement: on the CPU (the moved mesh and its scene
bytes) and on the GPU (its vertices uploaded once per placement).
- The model scene draws bodies that share a mesh as GPU instances: the
shared triangles once, a rigid move and colour per placement
(`InstancedMesh`, `vs_tri_inst`), in the solid, section and AO passes.
`GpuScene::merged` expands them, so scene tests see world triangles.
- `ModelBody::mesh_in_place`, `plan_mesh_in_place_at`, `mesh_bounds` and
`display_identity` give the shared mesh, its placement, the exact box and
an identity without making the moved copy.
- Display refinement's body ids, Fit/Home bounds, the viewport's body boxes,
the background mesher and picking use them (picking moves the ray into
the part's frame and shares one pick index).
25 placements of a 2 M-triangle STL on an M2 Max: orbit 45 -> 37 ms a
frame, footprint 41 -> 16 GB, 24 copies 39 -> 18 s.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Viewport: draw large meshes from shared, indexed vertices (#4637)
An instanced mesh's vertices are shared between its triangles and drawn
indexed (`SharedTris`). A flat-faceted mesh (an STL's facets) shares them by
position alone and lights each fragment with its triangle's own plane
(`vs_tri_inst_flat`, `fs_*_flat`), taken from the fragment's position
relative to the eye so it stays exact at any zoom; its vertex outputs are
kept few, since a tiled GPU stores every vertex's outputs. A body alone is
drawn this way from 50 k triangles. A mesh too large for one buffer falls
back to unshared vertices.
`instanced_scene_gpu_benchmark` (manual, ignored): 25 placements of a 2 M-
triangle STL at 3600 x 2190 px, 4x MSAA, M2 Max: 21.3 ms a frame for the
placements' own triangles, 16.1 ms shared and indexed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Viewport: leave out placements of an instanced mesh that are out of view (#4646)
Each frame `Batches::cull` keeps the placements whose box may be seen
(`placed_box_in_view`: all eight corners past one side of the view in clip
space, so it holds behind the eye too) and writes them again only when
that set changes. The manual benchmark gains `SOLIDCRAFT_BENCH_CLOSE=1`:
close up on the middle of 25 placements of a 2 M-triangle STL (13 in
view), 3600 x 2190 px, 4x MSAA, M2 Max: 28.2 ms a frame for the
placements' own triangles, 17.2 ms instanced and culled.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The slow steps waited on the clock (6 s sleeps, a 1 s timer, a 30 s cap), so on a busy
runner a launch could outlast them and time out at 240 s. The server now holds a response
until the test releases it, and the test waits on events:
- overlapping downloads: build v5's own files are held; files both builds share (index.html,
names no build changes) are served, since the browser queues a request behind an in-flight
one for the same URL. The held files are released once v6 has started.
- fail-fast step: the other files wait until the test has seen them asked for.
- update in another tab: the other tab updates once the launch is waiting for build.json, and
the browser's own sw.js check waits for the same moment.
- hung requests no longer give up after 20 s.
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Registers them after the Gruvbox set in the Theme list, adds attribution rows,
and keeps the heading-band ordering check passing by nudging heading/subheading
(and, for High Contrast, dialog) backgrounds.
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* Vertical history strip: left of the browser, compact rows, hollow-circle marker, collapsed rail hover
* Vertical history strip: seam handle to expand and collapse names, drag to resize with snap
* Vertical history strip: no empty gap below the transport
* Show the history seam arrow only on hover (#2028)
* claim: #2021
* Redesign the Assistant chat panel with shared UI components
* Show Assistant in the shared floating panel shell
* Extend floating Assistant to viewport height and shift navigation
* Dock Assistant before history and match docking preview bounds
* Align Assistant top margin with floating shell cards
* Extract a shared panel drag grip
* Rebuild responsive history sizing and three-way docking
* Share browser and History collapse hover styling
* Show the History title as soon as its text fits
* Animate stacked browser and History expansion and collapse
* Make panel folding responsive and independent of frame delays
* Let folding panel contents shrink continuously to header height
* Use four explicit History transport actions in every layout
* Resize the shared Browser and History column from either card
* claim: shared compact-mode containers (#2029)
* Centralize layout-aware UI containers and enforce their use
* Keep docked Assistant at the outer edge beside tool sheets
* Use shared surface policy for Assistant tool space
* Let tool sheets drag over shell panels while avoiding Assistant
* Update Parameters drag bounds assertion for shell overlap
* Keep dragged tool sheets below ribbon and workspace selector
* Enforce universal UI containers in mandatory repository checks
* Remove legacy report windows and require shared window composition
* Remove retired UI styling and require shared controls
* claim: compact layout preference (#2029)
---------
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
* Chamfer: a cutter that would reach into a nearby wall is not used
A straight chamfer's cutter reaches several times the chamfer's size out
from its edge, and further on a larger part. On a pin standing in a socket,
that far side lands inside the socket's walls, and the cut took bites out
of them. A cutter whose far side is inside the part is now refused, as one
whose ends are, and the chamfer is built locally instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Robustness goldens: seed 9 re-recorded for the chamfer cutter check
A chamfer in seed 9 was refused because its result changed more of the
part than a chamfer can. With its cutter refused, it now builds locally.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Navigation: opt-in free orbit and shared Normal to Selection
Preserve the complete reviewed feature, shared camera setting propagation,
rigid pivot behavior and focus/drag regressions from PR #1912.
Consolidate the prior main-integration commits before rebasing, so their
merge-resolution fixes remain in the linear feature history.
* docs: move free orbit help beside viewport controls
* Merge main into fix/solidworks-free-orbit
Camera animation is gone on main, and with it the orbit snap spring: the Shift
snap keeps its unsnapped orbit in orbit_free, which the free-orbit checks now read.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* claim: #3708
* Export Current View as DXF includes shown sketches (#3708)
The view export refused a design with only a sketch and left shown
sketches out when bodies were present. It now draws the sketches the
viewport shows (profile curves only: no construction geometry, points,
dimensions or constraints) on a SKETCH layer, one entity per curve.
Finished sketches are hidden behind bodies through the body-edge
occlusion; the active sketch is drawn on top, as in the viewport.
Sketch points use the same projection, budgets and cancellation.
The export is refused only when there is neither a visible body nor a
shown sketch curve; the dialog and the command share that check
(cmd::view_dxf_exportable).
---------
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* Sketch Dimension: `name = value` defines a user parameter (#2916)
In a dimension's value (the D tool's box, and `sketch.dimension`'s `value`
from scripts, the CLI, MCP and the control channel), `name = expression`
adds the user parameter `name` with that expression and dimensions with
`name`: typing `width = 40` on a line gives a 40 mm user parameter `width`
and a dimension whose expression is `width`. A bare number is read in the
design's unit, as a bare dimension value is; an angle's parameter is in
degrees. The expression may be anything the parameter editor takes.
It is one command: one undo removes the parameter and the dimension
together, and a dimension the sketch refuses leaves no parameter behind.
A taken name is refused with a hint (type the name to use it, or change
it in Parameters), a reserved name with the parameter editor's message,
and a left side that is not a name (`2 = 3`) still fails to parse as
before. A driven dimension defines nothing. The reply carries `defined`,
and the status line names the parameter.
The splitter lives in the doc crate (`expr::definition`) so other value
inputs can take the same form later; editing an existing dimension, the
inline shape boxes and feature dialogs are follow-ups.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Sketch Dimension: say why a name is refused, in the command's own words (#2916)
The dimension editor showed a refused definition raw, in the box and the
status line: "invalid parameters for `sketch.dimension`: value: `mm` is a
unit, constant or function name". It now reads "Sketch Dimension: `mm` is
a unit name, so it cannot be a parameter name": the editor passes the
command's errors through `friendly_error` like every other command, and
drops the `value:` field tag, since the box is the value. The taken-name
hint reads the same way: "Sketch Dimension: there is already a parameter
`w`: type `w` to use it, or change it in Parameters".
A reserved name says what kind of word it is and what that means
(`param_name_problem`, shared with the Parameters dialog): a unit name, a
function name, or a constant, "so it cannot be a parameter name";
`expr::reserved_kind` tells them apart.
`friendly_error` translates the command label with the interface language
(the engine's sentence stays English), so the prefix reads "Wymiar szkicu:"
in Polish.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Sketch Dimension test: keep the third line off the ribbon (#2916)
On current `main` the ribbon floats over the top of the canvas, and the
test's third line at sketch y = 50 projected under it: the click opened
Parameters instead of picking the line. The line now goes below the
first one.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
* claim: #3085
* Extrude preview: a cut from an empty root is not refused (#3085)
A cut or intersect sketched where the active component has no body of
its own (the root of an assembly, or of a design whose part was
imported into a component) takes part in every body (#1234): the
command makes the cut. The Extrude preview checked the targets itself,
looked only in the active component, and showed "there is no body to
cut or intersect" beside a tool going through the part. Pressing OK
still cut, so the dialog said one thing and the model another.
The rule now lives in one place, Session::cut_takes_every_body, which
both the command and the preview ask, so they cannot disagree again.
---------
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* claim: #3313
* fix(ui): the Rename box keeps the new name when it loses focus (#3313)
Only Enter committed a rename: Tab (and anything else that moved focus away)
dropped the typed name. Leaving the box now keeps it, as a click elsewhere
already did; Escape still cancels.
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: #2581
* fix(view): dialogs picking a plane show the origin planes (#2581)
Create Sketch showed the origin planes while it picked its plane; now any dialog
whose active input takes origin planes does (Mirror's plane, Offset Plane,
Midplane, the construct tools), so the plane can be picked in the view.
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>
* Vertical history strip: left of the browser, compact rows, hollow-circle marker, collapsed rail hover
* Vertical history strip: seam handle to expand and collapse names, drag to resize with snap
* Vertical history strip: no empty gap below the transport
* Show the history seam arrow only on hover (#2028)
* Extract a shared panel drag grip
* Rebuild responsive history sizing and three-way docking
* Share browser and History collapse hover styling
* Show the History title as soon as its text fits
* Animate stacked browser and History expansion and collapse
* Make panel folding responsive and independent of frame delays
* Let folding panel contents shrink continuously to header height
* Use four explicit History transport actions in every layout
* Resize the shared Browser and History column from either card
An offset's output curves have their ends recomputed from the source
curves on every solve. Trimming a piece off one reported success and left
the curve whole, because the refresh pulled the new end straight back.
Extending one was refused as "connected to other geometry", which does not
say why. Both now refuse with the reason and the way out: edit the source
curves, or break the offset association first. Trimming away a whole
offset curve, which removes it from its association, still works.
The fuzzer chamfers randomly picked edges and expected every chamfer to
shrink the body, so a chamfer of a concave edge, which fills the corner,
was reported as a wrong volume: 53 of the 6,000 designs of a run (seed
7100465: a box, a shell, a chamfer of an inside edge). Fillets already
expect any volume change for the same reason; chamfers now do too.
The regression test checks both directions on a shelled 20 mm box (an
inside-edge chamfer of 1 mm adds 8 mm³, an outside one takes 10 mm³
away) and that seed 7100465 is no longer a volume finding.
Co-authored-by: Tyler Chiriboga <18270301+Tyler-Chiriboga@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #3099
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Centre circle: a second click snapped to a point keeps the circle on it (#3099)
The tool sent the centre's snap reference but only a radius for the
second click, so a circle drawn from a line's midpoint through its end
was left free to change size. sketch.circle.center takes through: a point
reference, kept on the circle (PointOnCurve), and the tool passes it when
its second click snapped to a point.
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: #3257
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Clicking a parameter suggestion inserts it (#3257)
A press on a suggestion took the focus from the expression box before
the list could see the click: the list stopped drawing, the click never
landed, and the box gave up what was typed. The rows are kept from the
frame before; a press on one picks it and gives the box its focus back
(again on the release, which the box takes as a click away). The rows
are no longer focusable. The completion tests' frames now drop their
texture updates on purpose, so they run where egui checks that.
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: #2919
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Parameters: a new parameter's expression starts blank, and Tab selects a box of the add row (#2919)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Parameters tests: the add row's blank expression in the manage dialog tests (#2919)
The two inventory tests typed a name and pressed Enter, counting on the
10 mm the expression used to start with; they now type it.
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: #2440
* feat: choose ISO metric tapped-hole sizes and derive the drill diameter
* fix: label the tap drill and hide the unused diameter editor
* test: cover tapped hole catalogue and drill sizing
* fix: omit coarse metric pitch from generated designations
* Keep custom tapped-hole nominal diameter and pitch editable
Preserve Custom as dialog state, derive the existing metric designation from numeric inputs, and refuse invalid drafts without applying the last valid size. Retain manual text editing, physical tap-drill geometry and saved-design compatibility.
* fix(i18n): register custom tapped hole labels in source catalogs
* Adapt tapped-hole Custom fields to the current form kit
* fix: preserve tapped designation focus with dynamic custom rows
* solid.mirror: copies join their source's component; follow the saved placement
Three follow-ups to mirroring bodies of placed components:
- A copy joins the component of the body it mirrors (or, when that
component is placed more than once and a copy there would show at
every placement, the innermost component around it placed once), so
checks that group bodies by component see it beside its source.
- The saved placement path, not the body's name, says which component
a mirrored body is in: inserting the same file again reuses the body
names, and the copies of the first insert vanished.
- The mirror is evaluated again only when its own placements or its
component's frame change, not on every occurrence move: each later
command paid for one more mirror evaluation.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* solid.mirror: a saved placement must still lead to the body's component
A stale or edited placement path that leads to another component's
occurrence mirrored the body silently where that other component is
placed. The mirror now reports that the placement no longer exists.
The body's component comes from the evaluated body where a transfer
recorded it, else from the component its name is recorded in or its
feature's. When a later insert of the same file has taken over the name
(the recorded component lies outside the components the body's own
feature made), the placement has to lie inside those components instead,
so mirrors of the first insert keep working.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* solid.mirror: the copies' component change is a pending format change
v26 shipped in 0.15.0, so its mirror-placements note stays as released;
the change of where a Mirror with placements puts its copies is noted in
a new pending file instead.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* 3D mouse calibration: find each axis from a guided recording (#1463)
A recording of the cap at rest and moved along each axis both ways gives which raw axis is which,
which way it runs, where it rests, how much it wanders at rest and how far it goes each way.
Hands aren't exact: on our SpaceMouse Pro, sliding the cap right moved Ry further than X. So each
raw axis is picked by the one-to-one assignment that best explains all six motions together, each
raw axis scored against its own range so a wider one doesn't win through cross-talk. Each
direction is scaled on its own, so a device that goes further one way still reaches full scale
both ways; a direction under a twelfth of the furthest axis counts as not having responded.
A small state machine walks the user through it and moves on by itself once the cap has been held
out for 0.4 s in one go and let go. "Held out" is the least the solver accepts, twice an axis's own
rest wander, so whatever the solver would accept can be recorded and a noisy axis doesn't hold up a
quiet one.
A calibration also carries a dead zone the user sets, a share of full deflection taken off the
bottom of the travel, with full deflection still reading full scale.
Serde support is behind an optional feature, so the crate still needs nothing by default.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* 3D mouse calibration in Preferences, saved per device (#1463)
Preferences > Navigation gets a 3D mouse calibration row: whether the device being read is
calibrated, Calibrate... and Forget calibration. The window asks for each motion in turn and
saves what it found under the device (hid, web, spacenavd or 3dxware, with vendor and product ids), so
switching devices or turning spacenavd on doesn't apply the wrong mapping. A calibration is saved
for the device it started with, and ends if another one is read instead.
navigate_frame puts the reading through the device's calibration before the driver mapping, so
the per-axis Invert and speed and everything after them are unchanged. While the window is open
the reading goes to it and the view stays put; it closes with Preferences. Restore Defaults on
the Navigation page keeps calibrations, since they describe the device.
The web app hands over the gamepad's readings as they are, so calibration sees where the cap
rests; navigate_frame now leaves a reading inside the dead zone alone after calibration, so a
cap resting just off zero still doesn't keep redrawing. Whether the cap is moving is asked of the
calibrated reading there, in the web page's idle waker and in both wheel filters. Those run before
the frame, so they keep the last frame's calibration with its device and use it only for that
device. On the desktop, an axis with nothing fresh (timed out, or not reported since the device
connected) reads as the device's calibrated rest, so a device resting off zero doesn't drift.
A saved calibration that can't be read or used (a hand edit) is dropped on its own when the
preferences load, with a log line.
The window shows the device's raw readings live and has a dead zone slider, a share of full
travel on top of the measured noise; it's saved with the calibration and adjustable in Preferences
afterwards.
New strings are in all 14 catalogs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* 3D mouse: choose which motion drives each axis (#1463)
After calibrating, which motion of the cap should drive which view axis is partly a matter of
taste: rolling and twisting swapped, say, or Y and Z. Each axis row in Preferences > Navigation
now has a "driven by" choice next to Invert and speed. Picking a motion another axis uses swaps
the two, so each motion always drives exactly one axis, and Reset axes puts them back.
The choice applies after the device's calibration and before the driver mapping, so Invert and
speed stay with the view axis rather than moving with the motion. It's a preference, not part
of a device's calibration, and stays in force when recalibrating; the calibration window says
so while one is set. A saved assignment that doesn't drive each axis once loads as the default.
New strings are in all 14 catalogs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* 3D mouse: axis choice review follow-ups (#1463)
- An axis's speed, like its Invert, stays with the view axis when a motion is reassigned; tested.
- Calibrating records the device's own reading, before the axis choice, so a saved swap isn't
baked into a new calibration and then applied twice; tested through a whole calibration.
- A settings file without the axis choice, or with one of the wrong length, loads with the
default and keeps every other setting; tested.
- Each axis row says "driven by" in sight before its choice, which otherwise only explained
itself on hover (no help on a touchscreen or the web).
- "Reset axes" is now "Reset axis assignment", so it doesn't read as forgetting the
calibration just below it; the "driven by" tip reads more clearly in English.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* 3D mouse: name axis rows by what they do, and motions without a device in mind (#1463)
The axis rows and the "driven by" choices showed the same names, "Push left / right" and so
on, which read as one thing mapped to itself, and they described a cap that springs back to
centre, which a trackball or a knob that stays put isn't.
Each row is now named for what it does to the view: Pan left / right, Zoom, Pan up / down,
Orbit up / down, Orbit left / right or Roll, or Off. The name comes from the driver mapping
navigation used last frame, so it stays right where the mapping swaps axes (object mode swaps
the last two). Before navigation has run there's no mapping to read, and a row is named for
its own motion instead of a guessed effect. The choices are the device's calibrated motions:
Translation X / Y / Z and Rotation X / Y / Z, under a header row, and the calibration window
names a motion that barely moved the same way. This renames #1814's existing rows, which had
no translations; every new name is in all 14 catalogs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* 3D mouse: rows never fall back to a motion name (#1463)
Without a driver mapping, a row was named for its own calibrated motion,
which is also what its dropdown lists. It now takes the default mapping's
view axis, so the two sides never share a name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* 3D mouse: grey out Invert and speed on rows the driver turns off (#1463)
A row the driver profile turns off says so on hover, and its Invert and
speed are greyed out, since they do nothing there. Its motion choice stays
live so a motion can still be swapped out. The column header is shorter.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* 3D mouse: unmapped rows say Axis 1-6, and the off-row tip explains the swap (#1463)
Without a driver mapping, a row is now just "Axis n" rather than the
default mapping's view axis, which would be a guess there. The tip on a row
the driver turns off says that a motion swapped into it stops moving the
view, and a test swaps one in and back out.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* 3D mouse calibration: a device that stops short of rest after a release (#2778)
Through spacenavd, a relative-axis device like the Space Navigator never
reports its return to zero, so the reading stays at its last small report
after every release. The wizard never saw the cap let go, and once
calibrated, the stuck value crept the view.
A reading that stays exactly the same, close to rest, now counts as let go,
and where the cap settled after each step goes into the calibration: such a
device rests at the middle of where it settled, with a dead zone out to the
furthest of them. A device that comes back to rest calibrates as before.
* 3D mouse calibration: a steady hold through spacenavd stays a hold (#2778)
* 3D mouse calibration: stopping short of rest only counts from spacenavd and 3DxWare (#2778)
Recentring on where the cap settled kept the rest stage's wander radius but
not its offset, so a rest extreme could fall outside the dead zone; the
radius now grows by how far the centre moved. A reading that stops changing
close to rest only ends a step from a source that sends nothing at rest, so
HID and web devices calibrate exactly as before.
* 3D mouse calibration: settling off-centre counts on any device (#2778)
A drifting stick settles off its rest after a release just as spacenavd's
stuck reading does, but it keeps reporting, noisily. A step now ends once
the reading stays steady, each raw axis within twice its rest wander (so
exactly still from a silent source), close to rest, on any device. The
dead zone takes that wander on top of the furthest settle point.
* 3D mouse calibration: steadiness by the run's spread, settled on the step's own axis (#2778)
An axis that read flat at rest may dither once off-centre, so steady
readings may spread at least 2 units and a 50th of the furthest any step
went. Steadiness is the spread of the whole run, not distance from its
first reading. Close to rest is judged on the axis that set the step's
peak, since a device's axes may have different ranges, and the settle point
is the middle of the steady run.
* 3D mouse calibration: readings close to rest never count as held (#2778)
* 3D mouse calibration: the dead zone reaches every reading of each release (#2778)
Saving one reading of each release and padding it by the rest wander could
miss a reading seen in the same steady run. Each release now keeps the whole
run, and the dead zone is centred on and reaches everything read at rest
and after the releases.
* 3D mouse calibration: settle for half a second before a step counts as let go (#2778)
A hand held still close to rest is less likely to end a step early; a
device that comes back to rest never waits for it.
* 3D mouse calibration: show the dead zones as a game controller tester would (#2778)
Once calibrated, the window, and Preferences under the Dead zone slider,
show three pads: slide, press and twist, and tilt. Each has a circle for
the full travel, the dead zone shaded inside it with each direction its
own size and the user's dead zone on top, and a dot where the cap is now,
lit once the view moves.
* 3D mouse calibration: settle through cross-talk, per-axis steadiness floor (#2778)
The axis that went furthest in a step needn't be the one asked for:
cross-talk can move another raw axis further. A step now counts as let go
once the furthest axis is at least halfway back and some axis that moved
about as far is within a quarter of its own peak; a hand still holding the
push keeps the furthest axis out. The steadiness floor follows each axis's
own reach, and each stage's steadiness starts with its own readings.
* 3D mouse calibration: steady readings may dither ±2 on any axis (#2778)
* space-mouse: make the settle check's near band match the steadiness spread, test the 4-unit floor
near() measured distance from the settled run's middle against the whole tolerance,
so it accepted twice the band the steadiness check does. Use half. Add a short-range
test where only the .max(4.0) floor lets a +-2 dither settle, and one where dither
the spread check rejects is also not skipped as untouched.
* space-mouse: settle on the middle of the steady run so a lopsided dither isnt movement
near() allows half the steadiness spread around the settled point, but the point was the run mean. A dither that is mostly one value with an occasional far one (10, 10, 10, 14) is steady, yet the far reading sat 3 from the mean and started a hold in the next step. Use the middle of the run range instead.
Adds a lopsided-dither test and a test pinning the documented cross-talk limit ([100,300] then [5,90] stays open).
* Judge an untouched cap by the settled run's spread, not a rounded midpoint
Rounding the midpoint to i16 could leave one end of an odd-width run more
than half the tolerance away, so the cap read as touched while sitting at
the end of the run it settled on. Keep the run itself and ask whether one
more reading keeps its spread within tolerance.
* space-mouse: pin the wider skip band of a narrow settled run and a single-reading settle
* space-mouse: keep a step held while a lesser axis stays out as the larger one relaxes
Settling needed one axis furthest and returned, and one axis about as far and settled, which a single relaxed cross-talk axis satisfied alone. Every axis that moved at least half as far as the furthest now has to be back by RETURNED_SHARE of its own peak too.
* 3D mouse: the calibration tests run in a focused window, as the focus check now requires
* 3D mouse: the axis tests run in a focused window, as the focus check now requires
* 3D mouse: the calibrated-rest test runs in a focused window, as the focus check now requires
* Settle a spacenavd reading that is nearly at rest (#3523)
spacenavd sends a record only when the values change and nothing when the cap springs back, so the last small leftover of a release was held forever and the view kept drifting.
A held reading now reads as rest once every axis is within 150 raw units of the device rest and no record has arrived for 0.5 s. A firmer push stays as long as it lasts, and so does a small value next to a large one. 150 is the smallest round number above the worst leftover in the issue (129 on one axis).
* Choose which 3D mouse driver to read on Linux
When both spacenavd and the device itself (hidraw) are there, Preferences now has a "3D mouse driver" choice: Automatic, spacenavd or Direct. Automatic is what it always was, spacenavd when it runs and the device otherwise, so nobody's setup changes. If the chosen one isn't available the other is read and the log says so once. Each driver keeps its own calibration, as before.
To switch without a restart both reads now wake up when idle and look at the choice: spacenavd with a 250 ms read timeout, hidraw opened non-blocking and polled every 5 ms when it has nothing. A record that arrives is read as before, and one split across a timeout is kept. The reader also publishes which of the two exist, which is what shows the combo box; with only one it says which one is in use.
* Build the 3D mouse row header from existing labels
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
* Sketch tools come back in the variant used last, and it is kept in preferences
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Scenario: a new design keeps the remembered sketch variant
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
A sample picked before its picture arrives still opens; the language test waits for the file check.
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* Revert "Start page: sample pictures and the existence check of recent files run off the window thread (#4295)"
This reverts commit d9059d8da5.
* Test: an older design is marked format 28 now that 0.18.3 folded face pieces into 29
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Thread: turn a shaft thicker than a modelled thread down to its major diameter
A modelled external thread on a shaft thicker than the thread no longer refuses: the shaft
is turned down to the major diameter over the threaded length (run past a free end of the
face), then the thread is cut as before. The turned face is named F<n>:turned and the step
it leaves F<n>:shoulder:start/end. A shaft the part goes on around is refused rather than
cut into, shafts under the minor diameter and cosmetic threads on thicker shafts are still
refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Thread preview test: an 8 mm shaft is turned down for M6; M10 still refused
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Face names: pieces of a split face keep their numbers across evaluations (#3902)
Pieces sharing a name were numbered in the kernel's order, or by the index of the operand face
they came from. A boolean can list its result's faces in another order on the next evaluation,
and an operand made by one (a closed sweep tool) carries that order into the indices, so the
#1/#2 of a split face could swap between two evaluations of the same design. Pieces that
nothing else tells apart are now numbered along the spread of their centres (as edge pieces
already were), and pieces from different faces of one operand by kind, then by place.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Face names: older designs keep the piece numbers they were saved with
Numbering pieces by place renumbers split faces in many existing designs, and a stored name
matched exactly would then land on the other piece. Features of a design saved before this
change are marked `kernel_piece_order` by a pending format step and number their pieces as
before; features added afterwards number by place.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* STEP import: mesh band faces that wrap around a closed surface correctly
Fixes two related bugs that caused SolidCraft to fail to mesh a face wrapping
all the way around a closed, non-periodic surface (a revolved ridge band, or a
tube-shaped B-spline surface): a missing face, a folded/self-overlapping
triangulation, or a hole in the rendered body.
1. Ring edges that run slightly off their surface. STEP files write a surface
and the curves bounding it to the file's own declared uncertainty, so a
ring edge can legitimately sit a few micrometres off the surface it
bounds. The code finding where a ring edge crosses the planned split line
compared against the edge's raw (off-surface) point using a hardcoded
tolerance far stricter than the file's own precision, rejecting a valid
cut. Now matches against the point on the surface directly below the ring
crossing, respecting the file's declared tolerance.
2. Closed B-spline surfaces, their slits and their slivers. truck only treats
the rotation of a revolved surface as periodic; a face on a B-spline
surface whose control net closes on itself (a tube) wasn't recognized as
wrapping around at all. This produced a folded face, a hole (a boundary
loop landing a full period away from where it should cut), and one
near-zero-width sliver face costing the entire body its tessellation
plan. Adds periodicity detection for closed B-spline surfaces, period-
aware hole-loop alignment, dropping of zero-width slit boundaries, and
isolating a too-thin-to-triangulate face so it no longer takes the rest
of the body down with it. The GPU shader's periodic wrap mirrors the CPU
math so the two don't drift apart.
Regression tests: band_faces_split_when_ring_edges_run_off_their_surface,
band_face_with_a_zero_width_slit_is_still_meshed,
a_sliver_face_too_thin_to_triangulate_does_not_cost_the_body_its_plan.
Ablation-tested: disabling either mechanism in (2) reproduces its target
test's original failure message while leaving the other test unaffected.
* STEP import: mesh the band's split faces, not just split them
jyviko asked whether a band wall missing from the interactive app's render
was a shading issue or a genuinely missing face. It was missing: the ring-
edge split fix made the band's topology correct, but plan_face still
produced zero triangles for the two new sub-faces.
Two causes, both in plan_face (crates/kernel/src/tess/mod.rs):
1. The tolerance for how far an imported boundary may lie off its surface
was tied to the requested mesh's chord tolerance instead of to the
body's own size. A finer display tolerance could then reject a boundary
a coarser one had just accepted, so the face would drop out exactly when
zooming in on it.
2. Near a sharp corner of this flat-panelled band, several boundary points
that are legitimately a few micrometres apart (within the STEP file's
own declared precision) all projected onto the same surface point. The
boundary loop then repeated a point in (u, v), which a triangulator
can't follow, so the face's plan failed outright. One face without a
plan costs the whole body its plan, and the fallback mesher can't
handle a closed B-spline surface either, leaving both halves of the
split band with no triangles at all: present in the topology, invisible
in every renderer, and absent from the body's volume.
Fix: scale the off-surface tolerance to the body's own extent (new
Rules::extent), and collapse a run of boundary nodes that land on the same
surface point to one (drop_collapsed) before triangulating.
Regression test additions to band_faces_split_when_ring_edges_run_off_their_surface:
the body's volume now matches the analytic pentagon prism (was a third of
it - only the top cap - before this fix); display_plan leaves no face
without triangles from coarse to fine tolerance; and the actual tolerance
and facet angle the viewport draws with gives the split band its full
lateral area on both halves.
Confirmed on review_band_big.step, the fixture behind the review screenshots:
volume's relative error against the analytic pentagon prism drops from
1.4e-4 to 4.1e-6, and the band wall now occludes the floor grid behind it
in a snapshot the same way the top and bottom caps always did.
Full kernel suite: 739 tests, 0 failed.
* STEP import: hold an imported boundary to the file's own tolerance, not the part's size
jyviko: the off-surface acceptance added in the last commit
(rules.extent * OFF_SURFACE) scaled with the part, so a 2 m part
silently accepted a boundary 2 mm off its surface while a 20 mm copy
of the same part accepted only 0.02 mm. A STEP file's own declared
uncertainty does not grow with the part, so the bound shouldn't either.
Body gains boundary_tol: Option<f64>, Some(file's declared tolerance)
for a body read from STEP (crate::step_in::Units::tol, already used
by the ring-split fix in split.rs), None for anything this kernel
built. It's set before the import's own unmeshed-face check runs, and
carried over by the two face-split operations that re-cut an imported
surface (splitface.rs, outline.rs), since the file's uncertainty still
describes those boundaries afterward.
plan_face's accept now reads: the file's own tolerance when the body
declares one, or the old chord-tolerance/extent bound otherwise. The
old bound's tol * 10.0 term had to go too, not just the extent term:
at a 2 m part's typical display tolerance that term alone is ~20 mm,
nearly ten times the extent bound jyviko objected to, so keeping it
would have left the exact 2 m/2 mm example still silently accepted.
Two new tests in step_in/tests.rs:
- band_faces_split_when_a_scaled_copy_runs_off_its_surface: the same
band fixture at a realistic 2 m scale with the same absolute slop
and declared uncertainty as the small one, meshes the same way at
every chord tolerance from coarse to fine.
- off_surface_budget_follows_the_file_not_the_part_size: the actual
regression. The same off-surface error is accepted or refused the
same way whether the part is 20 mm or 2 m; a 2 mm error against a
1 µm declared uncertainty is refused at both sizes now (accepted at
2 m under the old bound, refused at 20 mm - that asymmetry was the
bug). Checked at both a coarse and a fine display tolerance.
Known gap, not fixed here: a STEP-imported body saved into a
.solidcraft design and reopened loses boundary_tol (native.rs's
decoder has no field to read it from) and falls back to the old
extent-based bound on reload. Today's design format has no concept of
this value at all; persisting it is a real format change, out of
scope for this fix.
Full kernel suite: 776 tests, 0 failed. clippy -D warnings clean.
* Display plans for STEP threads and pointed ends (#3847)
Three faces cost a threaded or pointed STEP part its whole display plan, so it
was drawn from the reference mesh: cracked, faceted, and never refined up close.
- A loop through a pole (a cone's apex on the boundary) seemed to wind around
the surface: the jump at the apex, unwrapped to the nearest copy, was
arbitrary. The loop is turned to end where it leaves the pole, so the step
that closes it is an ordinary one (`seam::close_through_pole`).
- Projection onto a spline with hundreds of spans (a helical flank) restarted
from a 16 x 16 grid and settled on another turn; it now starts from two
points per span, capped.
- Sag refinement no longer inserts a point exactly on a boundary side, which
split the side and failed the face.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* tess: a refused face's edges are divided finer and it is planned again (#2814)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM
* tess: sliver faces plan empty; a face no method plans is filled flat from its nodes (#2814)
A face whose boundary runs out and back along itself (two edges between the
same vertices, a rounding apart, as STEP files write slivers) encloses nothing
and is planned with no triangles. A face still refused after its edges are
divided finer (a cusp, a sliver whose sides sag across each other, a small cap
at a pole) is filled flat from its shared boundary nodes, so the rest of the
body keeps its display plan instead of falling back to the reference mesh.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM
* display mesh tests: a rim the chord tolerance leaves a line is divided again (#2814)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM
* portable B-rep: up to 8192 control points along a spline direction (#2814)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM
* solidcraft-cli: --debug logs what the engine reports to stderr
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM
* STEP import: a spline written over a tiny parameter range is read over one (#2814)
Some writers parameterize splines over a few millionths (a humanoid robot's
edges span 5.4e-6). The kernel tells parameters apart only to 1e-6, so over
such a range it evaluated the curve's end wrongly, and trimming an edge to its
vertices dropped a control point: the bodies could not be stored exactly and
the whole model cache was left out of the design. The knots are now stretched
to span one; the curve is the same and edges are trimmed by their vertices, so
nothing read from the file refers to the old parameters.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM
* model cache: a body keeps its file's declared accuracy (#2814)
A body restored from the cache dropped the accuracy the file it was read from
declares, so the planner held its boundaries to the kernel's own bound and a
reopened design drew differently from the import. It is stored with the body's
runtime values, and only when there is one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM
* tess: a boundary within a hundredth of its body lies on its surface, at every quality (#2814)
#1721 held a body read from a file to the accuracy the file declares. A writer
declares what it aims for, not a bound its edges keep: a 251-body robot
assembly declaring 0.01 µm has boundaries 10 to 30 µm off their surfaces, and
95 of its bodies lost their display plans. The bound is a hundredth of the
body again, what Normal quality always allowed, and now the same at every
quality, so zooming in still never drops a face (#1721's point). A file
declaring more raises it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM
* STEP import: keep faces whose edge vertices sit slightly off their curves
Some exporters write an edge's vertex beside the end of its curve, further off
than the accuracy the file declares (a surface export declares
0.01 mm and puts a vertex 0.19 mm off two spline ends). The reader refused
such edges and left the face, and with single-face bodies the body, out.
A vertex may now sit up to the file's tolerance or 1/1000 of the model's size
(the diagonal around its VERTEX_POINTs), whichever is larger, never more than
the existing 1 mm ceiling. Spline and open-conic edges have the end control
point moved onto the vertex, so every such edge meets its vertices exactly.
The file's declared accuracy still governs closed-curve, seam and circle
checks.
* tess, io: review clean-ups (#2814)
- A sliver face is one whose loops are all slit: drop_slits finds it in the
retry after a failed plan, so the pre-check on every face (folded) is gone.
- The flat fill uses cdt::triangulate (a lenient fill that leaves out sides
crossing one already kept) and its flood-fill inside test, and the plane
from geom::Plane.
- Refinement only builds its indexes when a face was refused, takes the
faces to plan again from edge_faces, and re-samples edges on the pool;
Rules::finer for the finer rules.
- One PackageWriter::fits for the model cache, the preview and the thumbnail.
- The band split's ring tolerance lives in split.rs; comments say what the
off-surface bound is now.
- The CLI logger prints Level::as_str().
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM
---------
Co-authored-by: David Lackovic <lackovicdavid1234@gmail.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com>
Co-authored-by: Ray <raykholo@users.noreply.github.com>
While a new geometry kernel is evaluated, a workflow labels new issues, pull
requests and discussions about kernel work kernel-paused, so a maintainer finds
them and answers in person. A pull request counts when it changes
crates/kernel/ or vendor/truck*, anything else when its title names a kernel
operation. Crash and data-loss reports and anyone who can push are left alone.
CONTRIBUTING.md says so.
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM
Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Two OK-off-the-window-thread tests checked that OK was computing right
after Enter without holding the evaluation. The preview's evaluation is in
the cache, so the worker was usually in before OK's zero wait was over and
OK applied at once: cancel_while_computing_applies_nothing failed in most
runs (5 of 5 in a debug build, 4 of 5 with the ci profile). Both now hold
the evaluation with the test gate, as their neighbour does, and let it go
once the computing state is checked.
Consecutive pieces of a trim loop can miss each other by micrometres to a tenth of a
millimetre, past the file's resolution, so the loop did not close and the face was
dropped. Faces are now read first and sewn after: each small gap's ends, and the other
piece ends at them, move to one vertex. Gaps over 0.1 mm or not small next to their
pieces stay open; the sewing tolerance stays the file's. The stated accuracy covers how
far a vertex moved, so the STEP reader keeps those edges.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Ribbon: Finish Sketch follows the last sketch panel
The Sketch tab's exit group sat alone at the far right of the ribbon,
about 600 px from the sketch tools on a 1600 px window. It now comes right
after the last panel, as before the redesign. Its room is still kept at
every width, and when panels go into the » menu it moves to the right end
after it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ui_layout test: Finish follows the last sketch panel, right end only with the » menu
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The menus no longer ask for offset_one_side, offset_symmetric,
offset_two_sides, pattern_center or pattern_flip, so the manifest check
failed on main and on every UI pull request.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Sketch: a quick double Esc finishes the sketch
One Esc still puts the tool down, then clears the selection, and with
nothing left to cancel keeps the sketch open (#1804). A second press within
0.4 s finishes the sketch, keeping it, the same as Finish Sketch, also when
the first press cancelled a tool.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Sketch: a double Esc takes away a blank sketch; only Esc twice in a row counts
A quick double Esc on a sketch the user added nothing to removes it in one
undo step, the view and tab going back as on Finish, so no empty sketch is
left behind. A sketch with anything in it is still finished and kept.
The double press counts only for two Esc presses in a row with no tool or
dialog left after the first: any other key in between resets it, and while
a tool is still out (its first Esc only ended a chain) the press puts it
down as before. Tests that press Esc on its own now wait past the window.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sketches drive surface extrudes, revolves, sweeps and lofts, so the Surface
tab gets the same Sketch panel as Model and Sheet Metal. A sketch started
there keeps the tab until it opens.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
An agent's design changes over the control channel (engine.execute, engine.batch) are evaluated
ahead on a copy of the session on an `agent` worker, then run on the window thread reusing every
feature computed there (doc::KeptResults, whatever the bounded evaluation cache dropped); the
worker also makes the display meshes of the bodies they end with. Exports run on a copy. While a
change computes, or the agent cursor acts one out, ui.*, inspection and screenshots answer; later
design changes wait their turn. A chip names the command with Stop. A person's edit meanwhile goes
first; a replaced design refuses the request. Preference: Preferences > AI, agent_off_thread, off
by default.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
transform_matrix took any finite 4x4 matrix. A perspective entry in the
last row, or a singular linear part, was built into faces that mesh to
nonsense: a revolved ring placed with such a matrix measured hundreds of
millions of mm3, and a boolean on it ran for over half an hour before
failing with "no result". Matrices come from design files and command
parameters, so they are refused with KernelError::Invalid instead.
* claim: #3394
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* STEP export: a merged cylinder or cone face's loop starts on the side its angle grows from (#3394)
A whole-turn cylinder or cone face is written as one loop: ring, seam, ring, seam reversed. Readers lay the ring that follows the seam's first use out from the surface's angle 0, so that ring must turn the way the angle grows (anticlockwise about the written axis). Three of the four tapered legs in #3394 came out the other way and other readers dropped their sides. When the second ring turns against the angle, the loop now starts with it.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* claim: #2966
* Expressions: a leading decimal comma such as ,5 is accepted (#2966)
In a comma-decimal locale `,5` means 0,5, but the lexer only started a
number on a digit or a point. A comma now starts a number outside
parentheses when a digit follows and an operand is expected (at the
start or after + - * / ^). Commas after an operand keep their meaning,
so function arguments and the existing error cases are unchanged.
* claim: #2882
* Holes from a sketch on a body's face drill that body (#2882)
A multi-point hole from a sketch attached to a body's face chose its
side and moved its points onto whichever body lay along the direction,
then cut every body, so points on a front body's face could drill the
body behind it. `targets` was accepted and ignored.
- solid.hole: holes at the points of a sketch on a body's face store
that body as their participant (the lookup a face-sketch Join uses);
`targets` sets the bodies to drill, `["*"]` every body; explicit
participants still override.
- Evaluation: for sketch-point holes with participants, the drilling
side, the surface each point moves onto and pattern copies are found
on those bodies only.
Sketches on planes, position and Single holes, and saved holes without
participants recompute as before; no design-format change.
* claim: #2998
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* io: the 3MF and DXF budget tests do not check wall-clock time
nested_fan_out_of_empty_objects_is_bounded spends about 20 million
visits reaching the work budget: 1.5 s alone, up to 40 s and more when
every test binary runs at once, past its 30 s limit. The time checks
run only after the reader returns, so a missing budget hangs these tests
with or without them; the error, the empty geometry and the three
vertices per placement are what show the budgets work. CONTRIBUTING.md:
a test must not depend on wall-clock timing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: kezmdor <15539254+kezmdor@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Measure: a body whose meshes leave a gap keeps its volume wherever it is
A part ring cut by solid.combine (two prisms through its ends, a notch
through its chamfer) measured 992.03 mm3 at the origin, 999.17 moved
100 mm and 1063.48 moved 1000 mm; it encloses 993.92. The reference
mesher leaves a crack where the notch meets the chamfer's cone: the
volume beside it is left out, and the divergence-theorem sums, taken
about the world origin, close the crack by a cone to the origin, so the
error grows with the body's distance from it.
A closed surface's area vectors sum to zero. When the picked meshes'
sum could move the volume by more than 1e-5 within the body's size, and
the display plan (which shares every edge's nodes between its faces) is
closed, the body is measured on the plan, about its box centre. Every
other body measures exactly as before.
Tests: the cut ring at the origin and moved by up to 10000 mm, against a
grid quadrature of the cut; a revolved ring of cones and cylinders moved
100/1000/10000 mm along each axis (1e-6); a cylinder and a frustum far
from the origin against hand values. Robustness seed 64 moves by 1.4e-6
and is recorded.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Measure: a gap the display plan can't close is measured about the box's centre
When the meshes leave a gap and the display plan isn't closed either, the
sums used to fall back to the origin-based volume, which drifts with the
body's distance from the origin (7 % at 1000 mm for the part ring). The
per-face sums are now taken about the fine mesh's box centre, which closes
the gap by a cone to a point inside the body; over a closed mesh nothing
changes. The gap decision moves into gap_closed, and a unit test with a
cube left open by hand covers closed vs open meshes, the closed plan, and
the fallback with an open plan or none.
Seeds 14, 20, 40, 55 and 61 end with such a body and are recorded with
ROBUSTNESS_GOLDENS_BLESS (volumes move by -0.51 %, +0.16 %, +1.9e-4,
+9.0e-5 and -1.5e-5; steps, outcomes and counts unchanged).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Record robustness seed 86 after measuring bodies whose meshes leave a gap
Seed 86 was added on main by #2387 after this branch's goldens were recorded. Its last
body leaves a gap in its meshes, so the measure change here moves its volume by -0.27 %
and its area by +0.13 %; steps, outcome and counts are unchanged. Seed 86 passes
unchanged on main 57d21013. Every other seed matches on the merged branch.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* measure: take the display plan only where it agrees with the sums to within what the gap could move
A closed display plan meshed less finely than the sums differs from them by its own chord error,
which can be larger than anything the gap explains: an imported part with small blends measured
0.03 % low on its plan. The plan is now taken only where the two differ by at most what the
gap's area could move the volume within the body's size; otherwise the sums about the box's
centre stand.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* measure: sum a mesh left open about its box's centre only, without the display plan
The closed display plan meshes some faces of an imported B-spline part
more coarsely than the sums do, so a body and its boolean pieces no
longer add up: clearance's union cross-check then refused a 17.3 mm3
overlap between two such parts (the pieces implied 20.5). Summing about
the box's centre alone keeps the gap closed by a cone to a point inside
the body and stays consistent with the pieces. Seeds 64 and 86 are
re-recorded with the reason.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* measure: placement tests in the area's test files
The ring and frustum tests join measure.rs's tests, and the cut ring
joins measure_centre_tests.rs (AGENTS.md: a regression test goes into
the existing test file for its area). The goldens note for seeds 14, 20,
40, 55 and 61 describes the centre-only sums.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* measure: keep the origin for a body left open on purpose
A surface, or a cup whose lid is still a separate sheet, has no volume of
its own. Measured about its box's centre, two such bodies stopped
comparing once the centre moved (a crowned lid rises), which broke the
patch crown test. Only a mesh with a small crack is measured about the
centre now.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aac7mP9rthK3WsPpPRvCsF
* Robustness goldens: keep main's notes, one note for this change's seeds
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aac7mP9rthK3WsPpPRvCsF
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* claim: #3654
* Control channel: control.running lists what the app is running, across connections (#3654)
control.pending answers only for the connection that asks, so a second client,
the MCP bridge after a reconnect or a script saw {"pending": null} while the app
was busy. control.running answers for the whole app, the same on every
connection and at once (the connection's own thread answers it, also while it
is busy with a timed-out request): every request the app has been sent and not
answered yet, oldest first, with its connection, id, method, command id (or a
batch's step count), seconds since it arrived and whether it still waits its
turn. A timed-out request stays listed until the app answers it, also after its
connection closed. Parameters are never shown.
The window marks a request started when it takes it (ControlRequest.started),
which tells queued from running. The MCP server gets a read-only `running` tool.
---------
Co-authored-by: MateuszSychXpl <151755914+MateuszSychXpl@users.noreply.github.com>
#3609 added Applying::seen; dialogs_sketch_tests' apply_keeps_the_panel_open_after_asynchronous_evaluation
still built the struct without it, so ui-egui's tests stopped compiling on main. The test stands
for a job that is already being shown, so seen is true (the behaviour it had before).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #2929
Co-Authored-By: Claude Code <noreply@anthropic.com>
* OK off the window thread: a job that has just started waits a frame (#2929)
The off-thread evaluation of a feature dialog was taken up in the same frame
it started, so a job whose worker had already finished was applied at once
and the dialog never said it is computing. Under load the window thread is
held up between starting the job and the check, and the feature appears
where the computing state belongs - which is what made
`apply_off_thread_tests::cancel_while_computing_applies_nothing` fail with
`assertion failed: h.app.applying.is_some()`.
A job that has just started now waits one frame: its result is taken up from
the next one, so the dialog says it is computing before the feature appears.
The app waits APPLY_AT_ONCE (150 ms) for the evaluation before it lets the
window go on, so a job only exists when the evaluation took longer than
that: the change shows the computing state for a frame in the 150-151 ms
window, and nothing else moves.
Co-Authored-By: Claude Code <noreply@anthropic.com>
---------
Co-authored-by: jeremymiribung-blip <jeremymiribung-blip@users.noreply.github.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
A sketch attached to a face is evaluated in the face's frame. When that frame is the body's
own, the body was still rebuilt as a new ModelBody with empty mesh cells, so projecting the
face's edges display-meshed a throwaway copy and the next cut or join meshed the same body a
second time. On a body whose cut edges are slow to mesh this added about a second to each
feature after a sketch on a face. In place, the anchored body is now the model's own clone and
shares its mesh.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #3223
* Extrudes from a nested STEP part's face join and cut it with the imported root active (#3223, #2872)
Opening a STEP assembly leaves its root component active, and the root
owns no body: its parts are components inside it. A profile sketched on
such a part's face was therefore treated as outside the active scope.
Join made a separate body instead of joining the part, and the automatic
operation saw no material and offered New Body in either direction; an
automatic extrude that came out as a cut found nothing to cut.
A component with no body of its own now works on the bodies of the
components inside it whose geometry lies in its own frame (as an import
places them): Join targets the body whose face the sketch is on, the
automatic operation looks at those bodies, and an extrude whose own
operation is Cut or Intersect takes part in every body as an explicit
cut already did.
* claim: #2089 (3MF production extension: review follow-ups)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* 3MF import: production extension review follow-ups (#2089)
Follow-ups from the review of #2127:
- Only the root model's build is read; other parts' build sections are
ignored as the spec asks (a slicer's child build pointing at a thumbnail
no longer matters).
- A model part that can't be read fails only the placements that need it;
the package-wide caps (part names, 1 GB of model XML) still refuse the
package. The root XML is dropped once parsed and each part is read
against the remaining budget.
- Loops are reported apart from depth, keyed by (part, object).
- Messages name parts as written; objects are kept in id order so parts are
read in the same order every run; %XX escapes in part names are decoded;
the model unit is a local of the parser.
- Tests: a component without p:path stays in its part (T1), the same id in
two parts is no loop (T2), a slicer-shaped project (T3), the placement
budget across parts (T4), a loop through three parts and a chain past the
depth cap (T5), broken parts and child builds (T6), escaped names; the
hostile-parts test asserts its messages; the mutation test also mutates
the parts' XML and zips it again.
- Guide: what slicer-only data is not read.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Tyler Chiriboga <18270301+Tyler-Chiriboga@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Split the schema contracts into one module per command file
`schema/commands.rs` becomes `schema/features.rs` (solid.box, solid.extrude,
FeatureResult) and `schema/inspect.rs` (engine.describe), each with its own
tests file holding its `schemas_and_the_key_check_agree` key lists and its
`strict_keeps_todays_errors` cases, so conversions of different command files
touch different files. The shared loops become helpers in `schema/tests.rs`;
the assertions are unchanged. `engine.describe` gains a key-list test, and
`schema_defaults_are_valid` now covers every command with a contract.
No behaviour change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Add shared schema field types for the next conversions
For the feature commands to be converted next, each deciding acceptance with
the reader the handlers already use:
- `FaceRef` and `FaceRefs` (`face::face_ref`, `features::face_points`)
- `EdgeRef` and `EdgeRefs` (`features::edge_points`)
- `SketchRef` (`features::sketch_id`)
- `FeatureCommon`: `name`, `body_name`, `body_names` and `participants`,
which `add_feature` reads for every feature, flattened into a contract
with `deserialize_with = "feature_common"`, which keeps the key in the
message and the issue's path.
The shape checks of `face_ref`, `edge_points` and `sketch_id` (and
`feature_sketch`) move into pure helpers (`face_ref_form`, `edge_ref_form`,
`sketch_key`) that the handlers and the types share; the handlers behave as
before. Tests check that each type and its schema accept what the reader
accepts (apart from a face name's byte cap and an index written `1.0`),
that the contract's messages match the handlers' errors, and that the
handlers still look picks up one at a time.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #3510
* Split Body: each piece can be renamed on its own (#3510)
A split named its new pieces after the split body every time (`Top (1)`),
so renaming a piece did nothing, and renaming the split body renamed the
other piece too (and broke the features that used it). The pieces now
take the names body.rename stores, and a rename keeps the names later
features derive from the renamed body.
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: #3383
* Combine: tools from another component meet the target where they are shown (#3383)
Bodies are evaluated in their own component's frame and an occurrence's
placement is applied only for display. Combine passed tool bodies to the
boolean untouched, so a tool in a component placed differently from the
target's was read as if its coordinates were in the target's frame: a
join that overlapped on screen gave two separate shells, and cuts and
intersects missed the same way. Any placement difference did it, a turn
or a plain move, in either direction.
Combine now moves each tool by the inverse of the target's placement
times the tool's own, the same placement used to show the body, keeping
face lineage. A tool placed exactly like the target is left untouched,
so combines within one component give the same result as before.
Moving an occurrence now re-evaluates a combine that depends on it.
* Joins, cuts and intersects draw their tools where they are shown (#3383)
A primitive, extrude, revolve, loft, sweep, pipe or coil that joins,
cuts or intersects bodies draws its tool in its own component's frame,
but each body is kept in its own component's. A target in a component
placed differently (an occurrence moved or turned, or a body moved into
it) met the tool as if the tool's coordinates were in the target's
frame: a join that overlapped on screen gave a separate lump 100 mm
away, and cuts missed the same way.
While such a feature evaluates, the tool now goes into each body's frame
(the inverse of the body's placement times the feature's component's)
wherever the join, cut or intersect meets it, and a body joined into one
placed differently is moved into its frame. Bodies placed as the feature
is are left untouched, so results within one component are unchanged.
Features that name the bodies they change re-evaluate when an occurrence
moves.
This also settles the moved-fastener cut of #439 (Combine, earlier commit).
* Through all and an extrude's contact tests see targets where they are shown (#3383)
An extrude or revolve measured its through-all length, and tested where
its tools meet the targets (a profile on a target's face, coplanar
growth, holes over empty space, taper run-on), on the targets as stored
in their own component's frame. With the tool now moved into a target
placed differently, a through-all cut drawn from the root under a cube
in a component moved 100 mm up was cut only as far as the cube is tall
in its own frame, and missed it.
These features now see the bodies they change that are placed
differently moved into their own component's frame, the same moves the
tools are taken back through, so the length reaches the target where it
is shown. Bodies placed as the feature is are read as before.
* claim: #3353
* Sheet metal: mirrored and copied sheet bodies stay sheet metal
Mirroring a sheet metal body into a new body gave a plain solid: the
opposite-hand bracket had no flat pattern, DXF or bend table
("`Body1 (2)` is not a sheet metal body"). Copies from a body pattern or
Move with copy lost their sheet model the same way.
A copy now takes the sheet model with it (SheetBody::copied). A rigid
copy moves the flat frame. A mirror moves the frame with its x axis
turned round, so the thickness side stays, and mirrors everything in
flat coordinates (x -> -x): base, flange bend lines (run from their far
end, with their end extensions, slopes and tears swapped), panel shapes,
cut-outs, pockets, bores, letters and base sketch lines. Arcs stay exact
and loops keep their sense. A perforation field of a mirrored copy runs
its grid from the right, so its holes are the mirror image too. The flat
length, bends and K-factor are unchanged, and the copy takes further
sheet metal features of its own.
Mirror with combine is unchanged: the joined body has no single sheet
model yet. Mirroring or patterning flange features is still refused.
Sheet models are derived from the timeline, so no design format change.
* Format: note the saved from_right of a mirrored sheet's perforation field
The new `from_right` flag is saved in the design, so it gets a pending
format note. No migration: a field without it runs from the left as
before.
* claim: #3221
* Save designs with large STEP imports: compress the STEP part (#3221)
A STEP between 256 and 512 MiB imported fine but could not be saved: a
design stored the STEP text uncompressed, and a design part is capped at
256 MiB. STEP sources are now always Deflated (level 2), may be as large
as an import accepts inside the file, and an import that could not be
saved even compressed is refused with a clear message. Reading stops at a
part's declared size, so a decompression bomb is refused early.
* claim: #3749
* Edit Joint: deleting the joint being edited closes the dialog and deletes it (#3749)
A joint delete was dropped without a word while any dialog was open, and
Edit Joint is a dialog. Deleting only the joint Edit Joint edits now
cancels the dialog first (as its Cancel does) and goes through the usual
selection.delete dry run; Delete with nothing else selected in Edit Joint
targets that joint. Every other refusal (another dialog, a tool, a
computing history, a loading file) now says why in the status bar.
Edit Section Analysis (its menu item, or a double-click on its browser row)
opened the dialog at distance 0 with no plane, so moving the cut on by a
millimetre meant picking the plane and typing the whole distance again.
OK now keeps the dialog's plane pick, distance and flip with the cut they
made, and Edit starts from them while the section shown (or hidden) is still
that cut. The toolbar still starts a new section afresh, and a section made
some other way (a command, an agent) opens the dialog as before.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #3798
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* fix(ai-chat): replay once with reasoning_effort none when tools are refused (#3798)
Newer OpenAI models answer HTTP 400 to function tools on /v1/chat/completions
unless reasoning_effort is "none". On that exact refusal the request is sent
again once with it, and later requests of the chat carry it directly.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* claim: #3483
* Settings > AI: Add agent opens from the Home tab too (#3483, #3513)
The Add agent dialog was drawn by the chat panel, which Home doesn't
show, so Add agent did nothing while Home was open. It is now drawn
from the frame itself, before Home and the workspace part ways.
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>
Type-first (pick, then type), the dimension's text follows the pointer, and where it goes
picks the kind: across from two points or a slanted line the distance between them, above or
below the horizontal span, beside them the vertical one. The draft kept the value of the kind it
started as, so moving from diagonal to horizontal previewed (and Enter applied) the aligned
length as the horizontal span: the solver moved the picked points (the first dimension of a
sketch even scaled it), so the dimension's extension lines no longer met them.
While nothing has been typed, moving the text now re-derives the draft for the new place, so
its value is what that kind measures and stays selected for typing. A typed value is kept.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #2501
* English (UK) as a language: a plain catalogue of the 25 messages UK English spells differently (#2501)
assets/locales/en-GB.json holds only the English messages whose spelling differs in UK English (Centre, Customise, Organisation, Licence, colours, towards, ...), in UK spelling; every other message falls back to English. One registration in i18n.rs (English first, then by id), a test that shows UK spelling and English for the rest, a row in ATTRIBUTION.md, and the docs/localization.md note. The translation-coverage checks skip a regional English catalogue, which has no gaps to report.
Fixes#2501
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(skills): refresh Boolean checking facts that fail on current release (#1835)
* fix(skills): restore warning coverage in test_run and use present tense in Boolean facts
* fix(skills): the slot keeps its side at zero since the solver takes the shortest step
The upstream solver now takes the shortest step that fits (#2060), so the slot
that other constraints also held slides to the origin when the distance reaches
0 and keeps the side it was drawn on when the value comes back. It no longer
comes back mirrored, so the trap and its test state the current behaviour.
---------
Co-authored-by: jeremymiribung-blip <jeremymiribung-blip@users.noreply.github.com>
* Extrude: a join from a whole face rounded by fillets sweeps the face itself (#2040)
A face's sketch fits an edge that is no line or arc (the quarter-ellipses
where fillets meet a slanted face) as splines a hair off the body's edges,
so a join from the face touched the body along a face it did not match and
failed with "boolean Union: no result" after minutes. When the profile is
a whole planar face of the one target, the join now sweeps the body's own
face, which shares its edges and is sewn on along it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Extrude: a cut into a whole face keeps the walls that cut and moves the others off (#2040)
A cut from a whole planar face sweeps walls from each of its edges. Where
the face beside an edge turns away from the sweep (n_N . -n_P >= 0) the wall
only touches the body along that edge, or lies in the face beside it, and
the boolean failed on it with "no result". The cut now keeps the walls that
go into the body, exactly (the body's own edges), runs the outline round
the others well outside the body, and starts a little out from the face.
A cut whose walls all only touch takes a slab beyond its depth plane.
Not covered: a cutting wall tangent to the face beside a touching one (a
round's wall meeting an end face, as in the issue's part); the boolean
still fails there.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Extrude: leave a whole face outlined by lines and arcs to the profile's own tool
Only a face whose outline the projection had to fit needs the body's own face as the tool. A square drawn over a box's top already meets the body exactly, and the swept face lost the names a sketch on the new top attaches to (stacked_face_tests, #489).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Run the whole cut tool from a face under the kernel guard (#2040)
Review: `builder::vertex`, `builder::line`, `Face::try_new` and
`builder::translated` ran outside `crate::guard`; only the sweep was
guarded. `cut_tool` now runs all of it under one guard.
Adds an end-to-end test of a cut into a face the body stands out past
below it (the block's top strip between its rounds): it takes only
what lies over the face.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test: the slab cut of whole_face_tool, on a spline prism (#2040)
A prism on a closed spline standing on a wider plate, cut 3 into its whole
top face: the slab takes the face's area times the depth and leaves the
plate alone. A test-only counter asserts the slab was the tool, so the test
cannot pass through the profile's own tool. Shortens the name of the
rounded block's top-face cut test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Kernel: the face's outline as a Vec now that polyline returns a closed chain (#2040)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Build on #1426's import components and nested STEP assembly instances.
Replace placeholder solid labels with meaningful part or instance names.
Translate IGES groups and subfigure instances through the existing STEP
component path, preserving nesting and placements and sewing surfaces
within each group. Bound assembly walks and reuse work buffers.
Related to:
- tangency-labs/solidcraft#1386
- tangency-labs/solidcraft#1426
Co-authored-by: Codex <noreply@openai.com>
* claim: #2570
* Loft: a circle lofted to a non-star-shaped profile is split once round (#2570)
A circle section was split at the lead section's vertex directions, kept in
the lead's order and forced to increase by whole turns. A non-star-shaped
lead (a C, a comb) turns back on itself, so the circle wound round many
times and closed with a negative arc: self-crossing walls and a huge mesh
(which froze the web build while meshing it).
The circle is now split exactly once round: at the lead's directions when
they go round once in order (clockwise on a plane facing the other way),
otherwise at the lead's perimeter fractions, evenly as the fallback. Every
arc is finite and positive and they total one turn; the unbounded loop is
gone. Section preparation is split out as loft_section_loops so the
resampled sections can be checked.
* claim: #2847
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* loft: keep sections turned past 90 degrees facing the same way (#2847)
The loft oriented each section by the sign of its normal against the first section's, so on planes turning about an axis every section beyond 90 degrees was reversed and the side faces folded. Follow the facing neighbour to neighbour instead.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* Ribbon: New Sketch on the Sheet Metal tab again; tests pin sketch enter/exit
Before the ribbon redesign the Sheet Metal tab started with New Sketch. It
gets its own Sketch panel again, like the Model tab's, and a sketch started
there keeps the tab until it opens.
Workspace tests pin the ways into and out of a sketch: New Sketch then a
plane, a double-click in the timeline or the browser, and the browser's Edit
Sketch all open the SKETCH tab looking at the sketch. Finish Sketch from the
palette or the ribbon lands on MODEL with the earlier view back. Escape and
Enter keep the sketch open (#1804), and Undo after Finish reopens it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs: ribbon layout lists Sheet Metal's Sketch panel
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Workspace test: press the keys well apart
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* CI: build and run the tests in one job, sccache on a Blacksmith sticky disk
The tests job compiles and runs the tests itself on the 32 vCPU runner, so a pull request or merge
group uploads and downloads no 3 GB archive (the download took 45 to 385 s per shard, and once
14 minutes) and starts beside the repository checks instead of after them. The archive is built
only for the nightly and manual jobs that need it. sccache moves from GitHub's 10 GB cache, where
about 1100 per-ref entries were evicting each other, to a sticky disk that only main commits.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* TEMP: let this branch save caches so the warm run can be measured
* Revert "TEMP: let this branch save caches so the warm run can be measured"
This reverts commit 1bbbfcf5ff.
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* claim: #3378
* Pattern and mirror copies of a join that merged its targets join the merged body (#3378)
Since #2755 a pattern or mirror copy of a primitive reuses the seed's targets. When the
seed was a join that merged two targets, the second one no longer exists, so the copies
failed with `unknown body`. The copies now keep the seed targets that are still there;
when none is, the names stay and the error says which body is missing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Rerun CI: the previous run was cancelled before any check ran
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #3242
* MCP batch: rollback restores the design the batch began in (#3242)
A failed batch with rollback: true reported rolled_back: true even when the
design never went back. Two defects, both fixed:
- file.new (and any document-replacing command) replaced the session with it,
taking its checkpoints down too, so a batch that failed after file.new had
nothing to restore. Session::replace_design now carries the checkpoints over
to the fresh session: the checkpoint belongs to the batch, not to the
session that happened to be holding the document.
- rolled_back echoed the requested flag. It now reports whether the design is
actually back: the app answers engine.batch with a new restored field, and
the MCP tool reports restored && rollback. An app that predates the field
falls back to undone/completed.
* Batch: answer with main's rolled_back field after the merge (#3242)
Upstream #2900 put the real restore check on engine.batch as
`rolled_back` while this branch named it `restored`; the merge kept
main's control test and this branch's field, and CI caught it.
Standardize on main's wire name: the app answers `rolled_back`, the
MCP tool reads it (fallback for older apps unchanged), docs and tests
follow. The checkpoint carry-over across file.new is unchanged.
---------
Co-authored-by: DahakBuilds <dahakbuilds@users.noreply.github.com>
* claim: #3141
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* 2-tangent circle: start from the touching circle, not the click (#3141)
The tool passed its second click as the place to start solving, and a
click lies on a curve: the circle began centred on that curve, where its
tangency has no side yet. The solve then moved the user's curves to fit
(a rectangle came out skewed), or failed outright when they were fixed.
The command now starts from the centre, nearest the requested point, of
a circle of that radius touching both curves (where the curves moved by
the radius meet), and the tool asks for the circle between its two clicks.
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>
* CI: give the slowest tests a shard each
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* CI: spread the slow tests on pull requests too, and keep both junit files
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* CI: pull requests skip the goldens and the action matrix sample unless the kernel changes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Docs: which pull requests run the goldens
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* CI: run the tests in one job on a big runner, slowest first
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* CI: allow the 32 vCPU runner label in actionlint, read junit.xml in the golden hint
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The origin point was drawn in the view and listed in the browser, but
nothing could pick it: the viewport never offered it as a hit and the
browser's O row had no selection. A new ORIGIN accept bit lets an input
take it as a vertex of no body; Move's point inputs (From, To, Point,
Pivot) accept it, from the view or the browser's Origin folder.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A curve written over a knot span of 1e-7 or less was refused on the STEP side
("knot vector consists single value"), and both faces it bounds were lost. Its
knots are moved and scaled onto (0, 1), which leaves its shape as it is.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
cone_point and cone_point_vertex join the sweep now that #3871 reads a cone
bounded only by its base circle up to its point.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Fix recovery for unsaved background tabs (#2884)
* Recovery: scheduled autosaves of every tab write on the worker thread (#2884)
After #3139, Autosaver::save waits for the write and Autosaver::tick runs
it on a worker. The scheduled path now goes through tick (which skips a
tab whose previous write is still running), so several dirty tabs no
longer write on the window thread each interval (#3044). save stays for
Save, close and shutdown. Each tab's autosaver has no clock of its own:
the shared cadence decides when it is due. The tests wait for the
writes a tick started.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Recovery: closing a tab waits for its running autosave before removing the entry (#2884)
With the autosave on a worker thread, discard could remove the files and
the lock while a write was still running; the write then recreated the
entry without a lock, and the next launch offered a design closed with
Don't Save. discard now ends the running write first, as Drop does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A BREP_WITH_VOIDS void's faces face into the cavity, so it encloses volume of
the opposite sign to the outer shell. Its ORIENTED_CLOSED_SHELL flag alone
doesn't tell: SOLIDWORKS writes .F. over a shell already facing into the
cavity, SolidCraft .T., and flipping by the flag added the cavity's volume. A
void enclosing volume of the outer shell's sign is now turned inside out.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* STEP import: regression sweep over the step-conformance parts of known shape
Fifteen generated parts from step-conformance (CC0): plain, NURBS, seamed,
placed, inch and far-away cylinders, domes, a grooved shaft, a plate with 100
holes, a 3.6 m disc, a 0.5 mm pin, a B-spline half-torus and a washer. Each
reads back with its exact volume, its box and its face count. The half-torus
on a TOROIDAL_SURFACE and the cones to a point are left out until they read
right.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* STEP conformance sweep: the pin's volume as FRAC_PI_8 (clippy)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A cone face written with no vertex at its point has the base circle as its only
loop (or that circle and a VERTEX_LOOP at the point, which has no edges). Its
surface was spanned over the circle alone, a sliver, so a solid cone came in as
a flat disc. Such a face is now spanned to the apex, and its circle is joined to
the apex by a seam line out and back, as a cone with a seam is written.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Boolean: a small hole in a large body's face stays a hole when the face is divided
A boolean divides faces at a tolerance that follows the larger operand (a few
millimetres on a part a few metres long). A small hole's rim, two half circles
when read from STEP, then became two chords: its polygon in the face's
parameters had no area, and the sign that tells a hole from a face piece came
out at random. The hole became a face of its own, inside out, and the face it
was cut in stayed whole. A box cut round such a hole gave a piece without it,
and the shared part of that piece and a pin in the hole came out negative
("malformed") or the boolean failed.
divide_face now divides a loop whose polygon is flat again, finer (down to a
hundredth of the loop's own size), before reading its sign. Only the polygon
that places the loop changes; loops that have an area are untouched.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Kernel: the long-plate hole test in tests.rs; a flat loop's size from its box
Review: the test moves into the kernel's test file (no file of its own),
and finer_if_flat takes the loop's size from the diagonal of the box round
its sample points instead of the largest distance between any two.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aac7mP9rthK3WsPpPRvCsF
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* claim: #3358
* IGES import: trim curves with reversed pieces no longer drop their faces
A curve on a surface (142) gives each trim loop twice: B in the surface's
parameter space and C in model space, running the same way. Some exporters
write C as a composite curve (102) whose pieces are in order but some of
them end to start. The reader passed those pieces on as they were, so the
loop did not close and the STEP reader left the face out ("face boundary is
not a closed loop").
The pieces of C are now turned to run head to tail, starting at S(B(start)),
the point where the parameter-space curve begins on the surface. That keeps
the loop's sense from the file, so holes still run against the outer loop.
A piece whose ends are one vertex at the sewing tolerance keeps its
direction.
On a 9,276-face trimmed-surface car model, 7,887 faces now import instead
of 3,882.
A surface of revolution (120) wrote its arc generatrix as a whole STEP circle, which
turns into a whole sphere twice over when the arc is centred on the axis; the trim could
not pick the cap out of it and the face meshed as the full ball. An arc in a plane through
the axis now comes in as a sphere or torus, any other arc as an exact rational B-spline of
just the arc.
Hovering an edge lights its circular rim, which starts from the edge's
tangent chain. That looked at every edge of the mesh for each edge in the
chain (and searched the chain itself each time), so a long helical edge in
many pieces, a thread's crest, took seconds: 7.6 s inside the frame on the
first hover of the stem's thread in a real watch-movement STEP file. The
edges are now found by the grid cells of their ends, two join tolerances
across, so each step looks only at the edges that end near it: the same chain
in the same order, in 29 ms for that thread.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #2757
* fix(params): autocomplete offers a parameter for any part of its name (#2757)
Typing `side` now offers `cab_side_thickness`: parameters match the word
anywhere in their name, after those that start with it, both in the expression
boxes and in parameters.complete. Functions, constants and units still match
from the start.
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>
The first hover over a large body made two things in the frame: the body's
pick index (its triangles and edges in hierarchies; about 0.3 s for a long
modelled thread, a quarter of a million triangles) and, with the GPU viewport,
the plan mesh a highlighted face is drawn from (about 0.7 s for the same
thread). The window froze for the sum.
A large mesh's pick index is now made on a worker, and picks scan the mesh
until it is there (a few milliseconds); a body's plan mesh is made on a worker
too (`ModelBody::plan_mesh_soon`), and until it is there the hovered face is
lit on the mesh the viewport already has. Small meshes, and the browser build,
make both at once as before.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* ui: spacing, radius and type tokens, per-tab accents, agent and on-accent tokens
* ui: per-tab accent behaviour and a larger application bar with a Start page button
* icons: silhouette master and flat assistant and agent link icons (open for review)
* icons: apply review notes (sparkle assistant, refined plug, silhouette halo)
* icons: wire the silhouette master to inspect.silhouette; record the accepted icons
* ui: application bar to the design: separators, Save, house, assistant icon, readable hovers
* ui: ribbon structure: Model to View tabs, contextual Sketch and Sheet Metal, migrated pins
* ui: ribbon painting to the compact design
* ui: smaller icon button highlight and assistant mark on the bubble corner
* ui: short ribbon labels that wrap at spaces
* ui: ribbon buttons fit their labels and Finish Sketch shows its full name
* ui: constraint gallery, workspace menu, section list headings and richer tooltips
* ui: lighter selected tab label, ribbon surface and dimmer bar separators match the dark design
* ui: document tabs as rounded chips with a dimmer inactive label
* ui: document tabs fit their names and show close only when active or hovered
* icons: close and plus masters for the document tabs (open for review)
* ui: centre the new-tab plus and overflow button on the bar
* ui: a quiet rule under the ribbon tabs on non-contextual tabs
* ui: the application bar mark separates its faces with the bar colour, not white
* ui: open ribbon section label uses the selected tint and a chevron
* ui: dim the tab close and plus icons until hovered
* ui: every ribbon dropdown uses the chevron, the workspace card included
* ui: the active document tab uses the design's surface colour
* ui: the bug button's right padding mirrors its icon side
* ui: square constraint cells and a rule before the grid
* ui: constraint cells back to their earlier size, keeping the rule
* ui: smaller constraint cells and icons, centred in the section
* ui: constraint cells fit the height of the large buttons' icon and label
* ui: sketch Create titles share the other labels' line
* ui: format the application bar and ribbon changes
* ui: active tab surface token and the colour and hover tests that follow the redesign
* tests: the merged parameter and narrow toolbar tests follow the compact ribbon
* i18n: the Malay Gap label has an English source and its Japanese and Chinese translations
* apply_off_thread tests: the evaluation lingers in tests, so still computing never depends on machine speed
---------
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
* perf: renaming a feature evaluates nothing (#3044)
A feature's name labels it and builds nothing, but a timeline rename evaluated
the whole design again (about 70 s on the Prusa Core One assembly, and again
for undo). When the only change is one feature's name, the model renames its
result, sketch and construction geometry in place and takes the renamed
design's fingerprints. A rename other features follow (a plane a sketch is on)
or of a failed feature still evaluates the design.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Guard cheap commands on designs with thousands of bodies, plus a scaling bench (#3044) (#3121)
Cheap commands stay cheap on designs with thousands of bodies, with a scaling bench.
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Save As and Open use the browser's file dialogs where the File System Access API exists; Save writes back to the same file, its handle kept in IndexedDB across reloads. File > Download design added. Other browsers keep storing designs in OPFS.
A closed loop whose joints meet a few nanometres apart along their parallel
axes (faces modelled that far apart) passes the conflict check, but the loop
solver wanted 1e-7 mm before it moved anything. No value of a planar loop
takes such a gap out, so every drive of it stopped at once (stopped_by_loop,
fraction 0) and joint.reach called the loop open.
The solver now closes such a loop as far as it comes and, when what is left
is within the conflict check, keeps it that closed on the way. joint.drive,
joint.reach and the nearby pose of the dof count share it.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Tessellation: trimmed faces of display meshes stay on their surfaces
Display meshes triangulate a trimmed face in parameters scaled by the
surface's speed along each, so a strip stretched along one parameter is
not fanned across, and every boundary segment stays a constraint: one that
crosses another is split where they meet. Boolean meshes keep truck's way.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM
* Tessellation: one boundary rule for every mesh; drop the #1648 retry
A boundary segment that crosses one already added is split where they
meet in Boolean meshes too, which keeps a pinched face (#1648) whole
without the snap-and-retry. Faces whose segments all go in mesh as
before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM
* Robustness goldens: seeds 1, 23, 55, 64 and 86 re-recorded on current main for #1653
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
* Thread preview test: wait for the stale fit error to clear before the next preview
After switching back from M6x1.3 to M8x1.3, wait_feature_preview stopped at the M6 error still on
screen when the new preview started a few frames later (seen on Linux CI with #1653's meshing).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
* Robustness goldens: the slow seeds 14, 20 and 40 re-recorded for #1653 from the merge queue
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
---------
Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
On an imported part with 2.5 million triangles, every frame of a Section
Analysis drag looked up the picked face's plane several times (each lookup
tessellates the whole body, about 0.1 s) and cut every triangle again for
the outline (38 ms).
- planar_face keeps its last answers for the model state they were found in.
- The outline cuts through a SectionIndex: the mesh's triangles sorted into
slabs along the cut's normal, kept per mesh while the normal stays, so a
cut looks at one slab (0.06 ms on that part) and finds the same segments.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #3249
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* inspect.interference/clearance: list mesh bodies as unchecked
Mesh bodies (STL, 3MF and OBJ imports) were filtered out before any pair
was formed, so two overlapping STL imports reported count 0 and an empty
unchecked list, which reads as "no collision". Each mesh body is now
listed in `unchecked` as {a: name, b: null, reason: "mesh body: not
checked"} by both commands. inspect.clearance also stops refusing a mesh
body named in `bodies` or `against` as "no solid body"; it is listed
instead. The parameter docs and the assemblies guide say so.
Fixes#3249
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* docs: separate the mesh-body note from the paragraph above
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* claim: #3393
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0158is7fBbAVh538YmVF3MpZ
* Sketch: a radius dimension past an arc's end extends the arc to its arrow (#3393)
A radius or diameter dimension on an arc whose leader is dragged past the arc's
ends had its arrow pointing at empty space. The radial frame now keeps the arc's
span, and an arrow outside it gets the arc continued from the nearer end round
to the arrow tip and a little past it, as dimension drawings do. Full circles,
ellipses and arrows on the arc are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0158is7fBbAVh538YmVF3MpZ
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
While editing a dimension's value, a single click on another driving
dimension now inserts its parameter name (a double-click no longer needed,
and its second click does not insert it twice). A lone value such as `10`
or `10 mm` is replaced by the name; an expression in progress keeps what
was typed and gets the name at the cursor (replacing selected text), so
`10 + ` becomes `10 + d10`. The cursor ends after the name and the field
keeps the keyboard.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #2716
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Editing a sketch past the history marker rolls the marker to it (#2716)
Edit Sketch rolled the timeline back to just after the sketch only when
the marker was later. With the marker before the sketch it stayed there:
the sketch wasn't built, so it opened with no palette and couldn't be
edited. The marker now moves to just after the sketch either way, and
Finish Sketch puts it back as before.
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: #3323
* Contact: a check finds bodies crossing face to face, not only a corner inside (#3323)
The collision probe sampled each tessellation's points (vertices, edge
midpoints, triangle centres) against the other body, so plates crossing
face to face - where the crossing strip is smaller than a face triangle -
put no sample point inside and went unreported while inspect.interference
found the overlap. Where an edge of one body passes through the other's
surface, the middle of the chord it cuts lies inside; its distance from
that surface is the depth. Touching and grazing are unchanged. The sweep's
collision work model pays for the added edge pass.
---------
Co-authored-by: DahakBuilds <dahakbuilds@users.noreply.github.com>
The test could lose a race: with a zero wait the evaluation sometimes finished and applied
before the check that the dialog was still computing. The evaluation is now held at a test
gate until the dialog has been seen saying so.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #2724
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0158is7fBbAVh538YmVF3MpZ
* Sketch: an angle between two lines from one point measures the angle between them (#2724)
An angle dimension on two lines that share an end (two radii of a circle, the
two sides of a corner) took the acute reading of the lines' directions, so
radii 120° apart measured 60° and 150° apart 30°. With no text position given,
the angle is now measured from the shared end towards the lines' other ends,
the reading anyone would mean; the supplement stays the alternative reading.
Lines that share no end, and dimensions placed with a text position, are
unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0158is7fBbAVh538YmVF3MpZ
* ci: re-run the checks; the run on the last push was cancelled
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0158is7fBbAVh538YmVF3MpZ
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A body rename changes names only, but committing it went through the general
"design changed" path: the whole timeline was evaluated again. On an imported
assembly that means reading the STEP file again and meshing every body (about
100 s on the Prusa Core One, and the same again for undo).
When the only change from the previous design is one body rename (the
features' stored body names and the body-keyed maps the rename updates), the
model now renames the body in its results instead: every body keeps its
geometry and meshes, and the fingerprints are set to the renamed design's so
the next evaluation reuses everything. Undo and redo of the rename take the
same path. A rename the model can't follow that way (the body is changed or
named by a later feature, sheet metal, threads) still evaluates the design as
before.
`bench-open --ops script.json` times commands and their undo on an opened
design, with the view work each leaves.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* WIP: adopt the saved model on open (#3053)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* WIP: tests
* WIP: bench-open opens as Open does
* docs: opening from the saved model
* Tests: an edit after opening evaluates from the feature it changes
* clippy
* Read SOLIDCRAFT_NO_MODEL_CACHE through the env helper
* Open: no picture from the model when the model itself opens; cancel test evaluates
* Saved model: bodies passed through found without writing their record; states list runs of bodies
* Adopted results go into the evaluation cache: an undone edit, or the marker rolled forward, takes them again
* Model cache size guard: the saved model's writer
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #3248
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* sketch.create: reject a custom plane origin that is not three numbers
A custom plane {origin, x_dir, y_dir} whose origin was given as an
expression ("w/2") or a number with a unit ("20 mm") silently became
[0, 0, 0], while a bad x_dir or y_dir was an error. A missing origin is
still the world origin; one that is present but is not three numbers now
returns a clear error. Expressions are not evaluated, so the saved
format is unchanged.
Fixes#3248
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* joint.inspect: what each joint is attached to, for agents
A read-only `joint.inspect {joint?}` gives each joint's definition: for each
side the occurrence, its component and path, the snap as saved, and what
each persistent face/edge name refers to (the feature that made it, and the
sketch and curve a swept side came from), whether the origin resolves and
why not; plus kind, offset, angle, flip, values with limits, expressions,
the motion links it is in, and its solve errors and conflicts. Its id ends
in `.inspect`, so agent gates treat it as reading only.
MCP `inspect_design` includes the definitions when the design has joints
(`joints: false` leaves them out). joint.list and joint.inspect share the
motion-link JSON.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n: catalog entries for Inspect Joint (en, ja, zh)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
`sketch.trim.path {points, dry_run?}` walks a polyline and, at each place it
crosses a curve trim may change, removes that piece exactly as `sketch.trim`
would there, in the order crossed and in one undo step. A piece already gone
is not trimmed again; projected geometry, infinite lines, offset outputs and
pieces trim refuses are passed over (reported in `skipped`). It returns each
trim's result and each removed piece as a polyline; `dry_run` reports them
without changing the design.
With Trim on, a press and drag in the viewport draws that path: the pieces it
would remove light up (a dry run of the command) and letting go runs it. A
click still trims the one piece under it.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The merge queue reads the required ci check from the newest run of a commit. Adding any label but
approved-for-ci starts a run whose jobs all skip, and its ci job was skipped too (as
'ci (ignored label)'), so a pull request that had passed could not enter the queue (#1850, #3182,
#3252). On such a run the ci job now waits for the commit's other runs to finish and passes only
when the newest one that ran its checks passed.
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #3234
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Sheet metal: refuse an edge flange on a flange's bend line (#3234)
A pick on the edge where a bend meets the plate (or the flange panel)
found a panel edge that is already a bend line and made a second flange
on it, leaving a gap and a sheet of two shells. add_flanges_each now
refuses such an edge before anything moves.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
The test turned a temp path into a link with `file://` + the path, which on
Windows gives `file://C:\Users\...`, not a file URL, so the test failed there.
It now builds `file:///C:/Users/.../my%20part.stl` on Windows (what Explorer
and browsers put on the clipboard) and `file:///tmp/...` on Linux and macOS,
as before. `files_in_text` already reads Windows file URLs correctly; the
fixed test now checks that on Windows.
Co-authored-by: Ray <raykholo@users.noreply.github.com>
Every engine.execute was acted out stop by stop (0.45 s glide + 0.25 s click
per toolbar button and per on-screen point), so a 720-point sketch.line took
minutes while the app answered no other control request. Now the pointer
visits the toolbar button and at most three picks (a long point list is
summarised by its first, middle and last point on screen), and a request is
acted out within 1.5 s whatever its size (the legs get shorter).
engine.batch is no longer refused while the cursor is shown: the pointer acts
out a short summary (the toolbar buttons of up to four of its distinct
commands, within the same budget), then the whole batch runs in one request.
A failed engine.batch now says whether the design really went back
(rolled_back).
(cherry picked from commit 55535e01af45cc9ee7555320863ff5ddcd7c151f)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
BIZ UDPGothic's Cyrillic glyphs are full-width (1.0 em), so promoting the
CJK font to the front of Proportional made Ukrainian text look wide with
large gaps between letters, broke word wrapping, and truncated UI labels.
When CJK text loaded on the web, install_cjk_font hard-coded "ja" and
stayed promoted after switching to Ukrainian or English.
Pass the live UI language to install_cjk_font_for so the CJK face only
leads a CJK UI. Keep fetched CJK font bytes on the web shell and re-apply
fonts when the language changes, and add a regression test that a
non-CJK UI keeps the CJK face as the last fallback.
* claim: #3244
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Fail an unverifiable intersection instead of keeping what it should trim (#3244)
When every route of the general boolean fails, it falls back on the sampled answer
(one body whole, or nothing) with a "not verified ... that part is ignored" warning,
even when the bodies' facets cross well past their faceting error. For an
intersection that keeps exactly the material it was asked to trim: teeth standing
0.27 mm out of a trimming cylinder came back untrimmed, and `solidcraft-cli run`
exited 0 with a part 16.918 mm in radius.
An intersection whose sampled answer is contradicted by facets crossing by more than
twice the depth the warning reports (6 x the check mesh tolerance, three times the
faceting error of two meshes) now fails, and the model is kept. Cuts and unions keep
the fallback: shelling a body in two pieces relies on it (a cavity tool grazing the
other piece), with the same depth and ignored volume as this case.
Fixes#3244
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Leave an empty unverified intersection as it was (#3244)
Only a doubted intersection that keeps one body whole is refused. An empty
one ("the bodies are apart") stays a result reported unverified: the clearance
and interference checks rely on it, and main's new
a_pin_through_a_d_bore_of_its_size_still_overlaps_the_flat (#2714) expects it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #3270
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* A body dropped on a component's Bodies folder moves into that component (#3270)
The Bodies (and Sketches) folder row only took items of its own
component, to take them out of groups. A body dragged there from another
component was refused, though the folder looks like the place to drop
it, so moving a body back to a parent seemed impossible. Items of the
same folder from another component now move into the folder's
component, as a drop on the component's own row does.
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: #2167
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Delete Face asks for its faces instead of failing (#2167)
Clicking Delete Face in the toolbar ran face.delete with no parameters and
reported "`faces` must be a list of points or face names", ignoring any
preselected face. It now opens a dialog with a Faces input, like Shell,
Draft and Offset Face, takes the preselected faces, and reopens from the
timeline.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* Canvas and Decal: the file picker opens on PNG and JPEG images
canvas.insert and canvas.decal opened the general Open dialog with no file
type, so it started on "Designs, CAD files and meshes" and PNG/JPEG files
were greyed out. Offer the image types first, as Insert SVG/DXF do (#1801).
* Canvas and Decal: rustfmt the file-picker test
- The repository is tangency-labs/solidcraft (origin and the claim check pointed at the old one).
- PRs land through the merge queue (`gh pr merge N --squash --auto`) after a maintainer adds
approved-for-ci; merging your own PR straight away is gone.
- A PR is rebased onto main, never merged with it.
- New "Before you start": an authenticated gh, the full open PR and issue lists, and a look for the
same feature or the same files before opening a second PR for one change.
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Mouse gestures: a ring of tools on the right button, per studio and customizable
Preferences > Navigation gets a Mouse gestures switch and a Customize window (number of
tools and their place, per studio). Gestures need the SolidWorks mouse mode and turn the
right-drag orbit off. The ring is 280/350 pt, one zone per tool, alternating tints (three
when the count is odd). The radial context menu is unchanged.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dwcq9eNUMZ1jNeKvWd1bcZ
* Mouse gestures: the ring opens on the right-button press; the right-click menus are off while gestures are on
The ring opens at once on the press, even while a tool or dialog is active. While gestures are
active the viewport's right-click context menu, the right-click that finishes a tool and the
radial context menu no longer fire. With gestures off nothing changes.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dwcq9eNUMZ1jNeKvWd1bcZ
* Mouse gestures: the ring opens after 4 px with the right button held; a plain right-click keeps its menu
The ring no longer opens on the press. It opens where the button went down once
the pointer has moved 4 points with the button held, even while a tool is in
progress. A right-click that does not move opens the usual context menu (or
finishes the tool) again; the release that ends a gesture opens no menu. The
radial menu stays off while gestures are active.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dwcq9eNUMZ1jNeKvWd1bcZ
* Re-trigger CI (the run was cancelled when approved-for-ci was added)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dwcq9eNUMZ1jNeKvWd1bcZ
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Open: show the design as its bodies are meshed, off the UI thread (#3053)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Open: hold screenshots until the design is meshed; profile lines
* Tests: time-bounded opens
* Open: a Loading parts n/N note while bodies are meshed
* Open: frame the design from outlines while it goes into the window
* Outline bounds: vertices and edges only; test
* Frame watch: the window thread's frame times by open phase (SOLIDCRAFT_PROFILE_FRAMES)
* Open: the picture grows in mesh order and stays once complete; refreshes pace themselves; named frame steps; watchdog test
* Open: outlines measured on the file worker and kept per body
* Open: draw the Home picture under the opened file's own key once meshed
* Format the open wait
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #3324
* Sheet metal: refuse a flat pattern that no longer matches the body
A feature that is not sheet metal can change a sheet body's material
without its sheet model knowing: a mirror combined into it, an extrude
joined onto it, a fillet or chamfer on a corner. The flat pattern, the
DXF and the bend table then wrote the sheet as it was before, a part
other than the one in the design, with no warning.
The evaluator now notes, per sheet, the first such feature: one that
changes the body's shape while its sheet model stays the same. That
feature warns, and sheet.flat_pattern, sheet.export_dxf (also with
per_body) and sheet.export_bend_table refuse with "not supported yet",
naming it. A sheet metal feature, or machining the sheet keeps as
cut-outs, rebuilds the body from the model: it clears the note and
warns that the change is not kept. Moving the body, splitting its faces
and cosmetic threads leave the flat pattern as it is.
* Sheet metal: a modelled thread is refused in the flat pattern too
Only a cosmetic thread leaves a sheet body's material as it is. A
modelled thread cuts the thread profile into the hole, which the flat
pattern does not have, and a later sheet metal feature dropped it
without a word. It now marks the sheet like a fillet does: it warns,
the flat pattern, DXF and bend table refuse naming it, and the feature
that rebuilds the body says the thread is not kept.
A loft through two sketches on the same plane built a flat body of zero
volume and reported success, so the mistake surfaced later, if at all
(a measure of 0, or a boolean that did nothing).
`loft_wires` now checks each pair of consecutive sections: parallel
planes with the second section's centroid in the first's plane are
refused with "loft sections i and i+1 lie in the same plane (no volume
between them)". The tolerance scales with the section's distance from
its plane's origin.
Test: two circles on XY; the loft fails with that message and leaves no
body.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
`PlaneRef::walk_exprs` stopped at `Flipped`, so a sketch created with
`flip` on an offset plane (`{plane: "XZ", offset: "d", flip: "x"}`) did
not list the offset as an input. Changing `d` rebuilt nothing that read
the sketch, and the model went stale.
`Flipped` now continues to its base, as `Offset` and `AtAngle` do, in
both `walk_exprs` and `walk_exprs_mut`.
Test: a loft between XZ and a flipped XZ plane offset by `d` has the
volume for d = 30, and for d = 50 after `parameters.change`.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: Convert to Sheet Metal keeps a plate where it is
* Convert to Sheet Metal keeps a plate where it is
Converted without bend edges, a plate moved by one thickness: a 100 x 60 x 2
plate on XY came out at z -2..0 picked on its top face, 2..4 picked on its
bottom. The sheet's frame (its base panel's bottom face, thickness along its
normal) was put on the plate's far face facing away from the picked face, so
the material grew out into empty space.
The frame now lies on the picked face with z into the material, as Convert
with bend edges does. The sheet is the old one moved back by the thickness:
same orientation, flat pattern, flange directions and pocket layers.
A test converts the plate from either face and checks its bounding box; the
engraved-plate test now checks the plate's position, not only its thickness.
* claim: #3376
* Fonts: find a Chinese fallback font on Linux and Windows (#3376)
The Chinese fallback was searched for at a few fixed paths, and in the
font folders only on macOS. On Linux and Windows a Chinese font anywhere
else (Fedora's NotoSansCJK-VF.ttc, WenQuanYi under truetype/wqy/, Source
Han Sans, a per-user font) was never found, so a Japanese CJK font drew
the Chinese interface alone and its simplified characters (设, 语, 实)
showed as boxes.
Search the system and per-user font folders on every desktop system,
three folders deep, and take a font only when it draws simplified
Chinese, so a Japanese-only file with a matching name is skipped. The
"sourcehanss" keyword matched no real file; match SourceHanSans* and
SourceHanSerif*, and Noto Serif CJK/SC.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #2672
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Chamfer: one pick follows a straight edge's curved tangent chain at any size (#2672)
chamfer_tool built a single-edge cutter for a straight edge even when the
edge runs on tangentially into an arc (a slot side into its end). The
cutter's overshoot (max(2*distance, 1% of the part size)) is probed for
material beyond the edge ends; on a small part the probe hit the arc's
material and the cutter was refused, so the blend followed the whole
chain. Past a bounding-box diagonal of about 200 mm the overshoot cleared
the arc, the cutter was accepted, and only the straight edge was chamfered,
silently.
A straight edge whose smooth chain contains a curved edge no longer gets a
cutter; it takes the chain-following blend, as at small sizes and as a
fillet does.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
The two reliefs that would overlap are still resolved one after the other: the
second one is left out, with a note. The corners come out in flange-index
order, so which of the two keeps its relief depends on the order the flanges
were made in.
Two corners that share an edge never come this far: each relief reaches
exactly r along that edge, and a relief is refused unless 2r fits in it. Two
corners that do not share one can: the square relief at a corner of 120°
reaches 2r from it along the bisector, so at the two 120° corners opposite
each other in a rhombus the two reach each other.
Two reliefs that would overlap are now left out together, each with a note
naming the other, so that which of them is cut does not depend on the order
the flanges were made in.
Fixes#3417.
Co-authored-by: jeremymiribung-blip <jeremymiribung-blip@users.noreply.github.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
* claim: #3267
* fix(view): a finished sketch shows its standalone points (#3267)
Points were drawn only for the sketch being edited, so a point placed for a
hole vanished once the sketch was finished although it could still be picked.
The standalone points of every finished sketch shown are now drawn.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Moving an occurrence places every body under it again, and each placed copy
searched every revolved face for where it sits to turn it away from its seam,
although its source had been turned already. On the Prusa Core One assembly
that search was most of the 12.7 s a move of the top occurrence took. A body
now records that its revolved faces are turned, and world placement
(`place_matrix`) skips the search for such a body. Modelling moves (pattern
copies, hole tools) keep `transform_matrix`, which still turns them in the
copy's own frame: where a face wraps all the way round the turn it finds
depends on those coordinates, and later booleans depend on it.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Later instances of a part showed a mesh shared through the first instance,
but the first instance meshed its own, so every repeated part was meshed
twice. The first instance of a part placed more than once now shows the
shared mesh too; a part placed once keeps its own (no second copy).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Autosave serialised and wrote the whole design on the window's thread: about
120 ms on the Prusa Core One assembly (its 61 MB STEP text inside), a hitch
once per interval after every change, and more once saves keep the evaluated
model. A tick now takes a snapshot of the design (its Arc and what is hidden)
and writes it on a worker; a later tick takes up the outcome, and no new write
starts while one runs. Crash replays rebase onto the autosave only when the
design is still the one written. Save and close still wait for the write, and
dropping the autosaver lets a running write end.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #3198
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Look At frames the sketch's geometry, not its plane's origin (#3198)
Look At on a sketch (its menu, Ctrl+8, the navigation bar, entering a
sketch) centred the view on the sketch plane's origin and kept the zoom,
so a sketch drawn away from it - an imported part placed far from the
origin - ended up out of view. Both paths now share onto_sketch: centred on
the sketch's bounds, zoomed out when they would not fit (never in); an
empty sketch keeps the plane's origin.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Register the Look At tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Look At tests: Vec3::len
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The retries in boolean_whole_inner mesh every candidate to compare its
volume. A nudged union at the coarse tolerance could come out with a face
on a cone whose trim loop ran metres along the cone's continuation (edges
up to 12.7 m outside a 155 mm part); meshing it took seconds to minutes per
face and once never finished, though its volume (+-1e10 mm3) failed the
check anyway.
check() first samples the candidate's edges (16 per edge) and discards a
candidate with a point further outside the operands' mesh box than 5% of
their size (or a non-finite point): no edge of a union, cut or
intersection can lie there. The reason goes into the new
BooleanDiagnostics::rejected (omitted from JSON when empty). Results of
sound candidates are unchanged.
On the reporting design the PitchTail join drops from 18.7 s to 4.3 s with
the same result; the regression tests synthesize a cone face whose loop
runs off its surface.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Spike: typed parameter contracts and JSON Schemas for solid.box and solid.extrude
The schema RFC's spike (plan/schema-spike.md). solid.box and solid.extrude get
typed contracts with generated JSON Schemas (2020-12), checked in front of the
unchanged handlers by a per-session level: off (the default, which changes
nothing), warn (the call runs and gains param_warnings) or strict (the call is
refused and the design left alone). Also engine.describe and the MCP tool
describe_command, `solidcraft-cli mcp --param-check off|warn|strict`, a
FeatureResult type for feature replies, and an env-var audit recorder
(SOLIDCRAFT_PARAM_AUDIT) used to check the contracts against every test.
Local spike branch; not for upstream as is (see the results report).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Parameter contracts: put the audit recorder behind a feature; drop two ignored test keys
The audit recorder is now on only in a build with the engine's `param-audit` feature
and `SOLIDCRAFT_PARAM_AUDIT` set. Its code is still compiled, linted and tested without
the feature. `scripts/param_audit.py` summarises a test run's audit logs, and the schema
module's docs give the steps for converting a further command.
Two tests passed keys their command ignores, which the audit found: `position` on
`solid.box` (`hole_face_tests.rs`) and `symmetric` on `solid.extrude` (`face_tests.rs`).
Both are removed, so the tests build what they always did. Comments no longer call
the contracts a spike.
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>
truck placed a face that no intersection divides (every face of a shell
that meets nothing of the other) by counting the faces a ray crosses,
taking each face's surface as stored. A revolved body's reversed faces
counted the wrong way, and an inward shell (a void, a cut's inverted
tool) was read as a solid: a cut beside a sealed void came out empty,
refused or, with a tool whose bounding box covers the target, taken with
the body gone. Intersections of such bodies came out empty as well.
In an AND the crossings now count as each face is oriented, and an
inward shell holds what lies outside it. An OR keeps truck's count.
The corrected cut let a folded offset through: an arc edge next to a
face it meets tangentially had its ends moved past its middle, and the
arc went the long way round (robustness seed 73's shell). The offset
refuses an arc whose middle crossed its chord, as it does a straight
edge that turned round.
Goldens: seeds 0, 45 and 77 had a cut refused as empty that now takes
what it should; their volumes are checked by hand in the PR.
A face pinched to a point, where its boundary touches itself or another of its loops within
TOLERANCE (a sphere touching a box's edge leaves one on the box), had a constraint refused by the
CDT, and the fallback cut across the face: it came out with no triangles or without those at the
pinch. When a constraint is refused, the vendored truck-meshalgo now puts every boundary point
within TOLERANCE of a segment into it (at most 8192) and triangulates the face again. Faces whose
segments all go in mesh as before.
The regression tests live in crates/kernel/src/thin_face_tests.rs, and the #1648 property test in
boolean_props.rs is no longer ignored. Robustness seeds 20 and 55 are recorded with their Linux
values.
Co-authored-by: MateuszSychXpl <151755914+MateuszSychXpl@users.noreply.github.com>
* claim: #2678
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* WIP: timeline.dependents shares the evaluation cache (#2678)
The check before a delete evaluated the edited design on a fresh model:
it recomputed the whole timeline and left nothing for the delete. It now
evaluates on a copy of the session's model. Adds Ahead::replies for
running such checks off the window thread (the UI part is still to come).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Delete: check what a delete breaks off the window thread (#2678)
Both checks a delete makes first (timeline.dependents for the timeline's
Delete, the selection.delete dry run for Delete on the selection) evaluate
the history after the deleted feature, on the window thread. A part with a
slow feature froze the window for as long as everything after it took
(5.9 s in the report). They now compute off the window thread like undo and
the history marker: within the app's 150 ms wait they carry on at once,
otherwise the window says it computes and the delete (or the confirmation)
follows when the report is in. A design changed meanwhile is checked again;
a second Delete meanwhile is left out.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ci: run the checks again (the run for 3b94e8e0 was cancelled when the PR left draft)
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>
* kernel: a blend runs along chains of several faces and lines closed rims up (#764)
A blend's chain may run along the boundaries of several faces (a tube's rim
along the halves of its wall): each edge continues from its own best-facing
face, read station by station. Before, a chain had to be the edges of one face,
so every rim of a revolved or cylindrical surface was refused.
Two closed chains blend from matching seams: the second chain's start (searched
by arclength, then narrowed) and direction are the ones that put each station
closest to its partner, so rims whose seams sit apart no longer twist. A
picked `seam` point on each chain fixes the start instead. A closed chain
against an open one is refused. Closed chains leave towards each other's
centroid, not a point on the rim.
Stations are read on the edges' own curves, not on the chords of the length
table (they sat up to the chord's sagitta inside a curved edge), and the
reported gap is measured to the curves.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM
* kernel: closed blend chains run round the same way; Hermite strip and seam tests (#764)
The second of two closed chains runs round the same way as the first (by
their area vectors), so a picked seam turns the blend by just the angle
between the seams instead of folding it over. The nearer pairing decides only
when the loops stand almost at right angles.
Tests for the issue's gates: the blend between two offset box rims is the
analytic cubic Hermite strip (control points from the chord) and keeps the
top faces' normal along both seams; picked seams a quarter apart turn the
blend a quarter round.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM
* surface.blend: `seam` picks where each chain's stations start (#764)
A point on each chain, saved with the feature (one pending format change),
moved with the design like `align`, and refused when it is not two points.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM
* surface.blend: a test between two tube rims, their seams picked and saved (#764)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM
* Blend dialog: edges at each end, continuity and bulge per end, stitch (#764)
SURFACE › CREATE › Blend Surface opens a dialog: the edges the blend leaves
and the ones it arrives at, each end's continuity (G0–G3) and bulge, and
Stitch to both bodies. Edit Feature brings it back and keeps what the dialog
does not show (align and seam points). Labels in every locale catalog.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM
* ROADMAP: M8 Blend runs along several faces, pairs closed rims and has a dialog (#764)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM
* kernel: a blend station on the vertex between two support faces is read on the neighbour (#764)
A station where a tube wall's two halves meet sits at a corner of one half's
parameters; with a leave direction that turns round the wall (tubes off each
other's axis) the shape could not be read there and the blend was refused. The
neighbouring chain edge's face holds the same point and reads it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM
* blend: review clean-ups (#764)
- ChainLine keeps one list of samples instead of four parallel lists.
- frames_on and boundary_gap take the chain's ChainLine instead of rebuilding it.
- A station is read on the neighbouring face only when it sits on the vertex the two edges
share, each face once, and each face keeps its own walk.
- The seam rule (a closed chain starts at it, an open one runs from the nearer end) lives in
ChainLine::start_at_seam; pair_with reuses the start of its both-ways search.
- support_faces projects with curveblend::project; loop_area sits with plane_through.
- surface.blend parses `align` and `seam` through one point_pair; the Blend and Ruled dialogs
share body_and_edges.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM
---------
Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A cut whose tool ends exactly on a face of the target (a hole cut from a pocket's floor up to the
plate's top face) could come out of the general boolean with the target's face whole over the hole
and the tool's end face under it: a skin of no thickness that keeps the hole closed. No volume
shows it and the sampled membership checks see the hole either way, so the result was kept. The
result check now refuses a body with a planar face lying back to back on another of its own planar
faces with nothing behind part of it (`coplanar::has_skin`), and the retries take the cut.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #3254
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Preview: nothing is drawn once the current inputs fail (#3254)
The last good preview stayed up when the current inputs made the feature
fail, so the view contradicted the dialog (Extrude at 0 mm still showed
the 1 mm extrusion beside its error). Once the result for the current
inputs is an error, the preview scene is hidden and the bodies it stood
in for come back; while the next preview computes, the last good one
still stays up as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Extrude preview: a zero distance is refused at once (#3254)
The straight extrusion feedback gave up on a zero distance, so the
preview built a whole scratch design only for the kernel to refuse it,
and the old preview hung on until then. It now returns the extrusion's
own error straight away.
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>
* Iterate instead of indexing in nine loops
Replace index loops that look up neighbours by position with enumerate,
zip(cycle().skip(1)) for wrap-around pairs and zip(skip(1)) for
consecutive pairs. No behaviour change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* geom: ring_edges for the edges of a closed ring
Every walk over a polygon's edges, the last back to the first, spelled
x.iter().zip(x.iter().cycle().skip(1)); eighteen of them now call
geom::ring_edges. Three redundant take(len) calls go with them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* geom: Ring, a closed ring that knows it is closed
A Ring holds items in order around a closed polygon, loop or fan; edges()
walks each with the next and the last with the first, corners() each
with both neighbours. Ring<Vec2> has signed_area and even-odd contains.
Loop2::polyline returns a Ring, as do doc's face_loops and
boundary_loops, kernel's coplanar wire samples, CapLoop's segments,
tess pieces' rings, the gizmo's plane quads and the navigation cube's
facets. Their walks take edges() or corners() instead of wrapping an
index; sketch's text-hole test and doc's poly_area become contains and
signed_area. Loop2::joints pairs each segment with the next.
Replaces the ring_edges free function.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* geom: Chain, Ring and Path; bounds for a chain of points
Ring becomes Chain<T, Closed> beside Path = Chain<T, Open>: edges()
closes a ring and stops at a path's end, Path::close makes the ring,
and bounds() (the empty box for no points) serves both. Eight
hand-rolled min/max folds over a loop's polyline call bounds(); doc's
flat_polyline returns a Ring.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* geom: area and inside on Ring for any planar point
Ring's signed_area and contains take any Planar point (Vec2 or
[f64; 2]). The cam profile in joints_cam is a Ring: its own even-odd
test goes, and its edge walks, gap and area take edges(). face_params'
loop area, the text path's winding, tess pieces' ring area and the
copies in three tests call signed_area or contains; the formulas are
the same term for term.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* doc: sheet's closed outlines go through Ring
dedup takes and returns a Ring (dropping a last point back on the first
is what makes it a ring's), near_outline walks a Ring's edges, and the
copy of it beside fold checks calls it. point_in goes: its callers ask
the Ring. Its formula anchored each crossing on the other end of the
edge, so the two can differ only for a point on an edge to the last
digit; one caller sets such points aside first, the others test a
region's interior point, a cut's midpoint or another outline's corners.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* doc: cam tests build their outlines with Ring::from
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* geom: area_vector on Ring<Vec3>; three Newell sums use it
Ring<Vec3>::area_vector is half the sum of a × b round the ring. heal's
loop_area, lip's outer-loop turning and the revolve's loop orientation
summed it by hand with a wrapped index; they take it from the Ring
(the half is exact, and two of them only read its sign).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* geom: Aabb3 from points; boxes built and asked through it
Aabb3 collects from points (skipping non-finite ones, as add does) and
gains inflated, contains and meets. Fifteen hand-rolled min/max folds
over points or boxes in kernel, doc and engine become collect or union,
and their "within tol of the box" tests become inflated(tol).contains.
blend's refused-edge boxes and naming's face index hold an Aabb3.
A NaN point is now outside every box, where the old negated
comparisons let it in; an infinite point no longer stretches a box.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* geom: Aabb2, the plane's box; 2D folds and box tuples use it
Aabb2 mirrors Aabb3 (collect, add, union, inflated, contains, meets)
and adds relative, a point's place in the box. sketch's link and
link_loops each had that formula; link_loops' boundaries, prism's
segment and loop boxes, profiles' shape boxes and grid, and ends'
segment box hold an Aabb2 instead of a (low, high) pair. Folds in the
cam, sketch bounds, sheet's perforation grid and the sketch clipboard
collect one.
Boxes are built from the same terms (a round's box keeps c - r - tol).
As with Aabb3, a NaN point is outside every box and an infinite point
no longer stretches one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* kernel: prism's segment boxes are plain Aabb2s
The SegBox alias named a (low, high) pair; Aabb2 says it itself.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* geom: Chain::bounds is an Aabb2; the boxes #2382 left as pairs follow
Chain::bounds returns an Aabb2. Its callers keep it: sheet's hole and
cut boxes (loop_box), letter boxes, island outlines and line-near-cut
test use meets, contains and inflated; profile's outline overlap and
max_clearance, profiles' box tests and the cam test read its corners.
flat_size and the flat pattern's JSON size are the union of the loops'
boxes (0 × 0 for no loop, where the pair fold gave -∞).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* geom: Aabb3::intersection; hand-grown and hand-met boxes use the box
Aabb3::intersection is the box both hold. ops' four overlap boxes of a
boolean's operands take it, and seven boxes grown by hand (ops' body
boxes, the distance set's bounds, coplanar's face box, eval_more's
padded mesh box, compare's group and region boxes) call inflated with
the same amount on every side.
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>
A line and a circle or arc can now be dimensioned to the circle's edge
(ConstraintKind::LineCircleDistance, saved as line_circle_distance): the gap
while the line misses the circle (0 is tangency), or how far the edge reaches
past the line when it cuts through (stored as `inside`).
Rebased onto main as one commit (the branch's own changes; its merges of main
dropped). Conflicts resolved: the test mod lines in cmd/sketch.rs (both kept,
sorted), and README.md, whose feature list main moved to docs/capabilities.md;
the line-to-circle dimension is listed there now.
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
Co-authored-by: jyviko <jyviko@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Preferences: a Plugins page lists installed plugin folders
Each folder under the per-user plugins directory that has a plugin.json
appears in Preferences. Turning one off is saved with the other settings.
The page reads the manifest and does not run any code from the folder.
* Retrigger checks after the cancelled run
The required ci check failed because the workflow was cancelled before
any job started. This commit does not change the plugins page.
* claim: #3087
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Viewport: the ground grid is centred where the view's axis meets the ground (#3087)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Grid: its centre stays within reach of the point under the target
At a grazing slant in perspective, where the view's axis meets the
ground runs far off, and the ground in front of the camera had no grid.
The centre now moves no further than the grid's half-size from the
point under the target (review of #3124).
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>
Two empty numerical cut attempts stopped the boolean retry sequence before
the exact geometric strategies ran. When the result guard refuses that empty
candidate, the exact strategies (sliced planar, pushed walls, local prism,
sphere lens, seam halves) now try the cut first; if none recovers it, the
original empty candidate and diagnostics are kept, before any displaced
numerical retry.
Rebased onto main as one commit (the branch's own changes, its merges of main
dropped). The only conflict was the new engine regression test next to
main's #1985 STEP export tests in boolean_regression_tests.rs; both are kept.
* kernel: loft guide rails (#201)
A loft follows one or two guide rails between its sections: stations are added between each two
sections, each the plain loft there moved so that the point a rail guides lands where the rail
crosses the station's plane — turned about the loft's direction and scaled (or stretched across
to the rail) with one rail, carried, turned and sized between two. The sections and caps stay
exact; the smooth loft runs through sections and stations. Solid and sheet lofts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM
* doc, engine: solid.loft and surface.loft take guide rails (#201)
`rails` (one or two {sketch, curves}) and `rail_fit` (scale | stretch), stored on Loft and
SurfaceLoft (one pending format note), parsed by the sweep's rail parser (now shared) and built
through the kernel's railed loft. Rails on a loft to a point, or on a surface loft with edge
sections or end continuity, are refused as not supported yet. The rails' sketches are walked as
the feature's references and their components placed like a sweep's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM
* ui: Rail, Second Rail and Rail Fit in the Loft and Surface Loft dialogs (#201)
The rail inputs and fit row the sweep dialogs have, shared through dialogs_sweep; Edit Feature
brings the rails back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM
* docs(roadmap): loft guide rails (#201)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM
* loft rails: review clean-ups (#201)
- Sweep and loft rails share one turn-and-size map (`sweep_path::rail_frame`).
- A station's middle and the loft's direction come from one curve through the sections'
middles (exact derivative) instead of two more plain-loft evaluations per station; at most
33 wires, so railed faces stay light.
- Nearest points on a section use `param_on` (kept only where the search comes nearer); the
size uses `Aabb3`; one B-spline builder; anchors keep their nudge.
- The railed and plain lofts share one path in `loft_with_rails` and `loft_sheet_with_rails`.
- The engine leaves the not-yet-supported refusals to the design's evaluation; both lofts start
their rails from the first section's first point.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM
* loft rails: refuse rails on sections with holes, with a test (#201)
Main's lofts now take sections with holes (#2447); rails shape only the
outlines, so a railed loft of rings is refused until the rails carry the
holes too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM
---------
Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Adjacent \X2\...\X0\ runs, as step_out writes one per character, now
decode: the backslash opening the next run was pushed as a literal, so a
name exported as 圆角 came back as 圆\X2\89D2\X0\ (also every linked
component insert, which round-trips through STEP).
STEP and IGES bytes that are not UTF-8 are decoded in their detected
legacy encoding (chardetng + encoding_rs) instead of from_utf8_lossy, so
GBK or Shift_JIS names from vendor files no longer turn into U+FFFD.
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
* claim: #2448
* Sketch: a click that moves on the way places its point (#2448)
egui reports a press that moves past its click distance, or is held past its
click time, as a drag, not a click. The viewport only placed points on clicks,
so a hand that moved while clicking placed nothing with the Slot, Line, arc,
three-point, Ellipse, Spline and the other tools, and on the first point of a
Rectangle, Circle or Polygon the few pixels became the press-drag-release
gesture of #1630 and drew a sliver.
- Any tool's press that turns into a drag (no navigation, no Select Other
hold) clicks where the button is let go.
- A Rectangle, Circle or Polygon drag let go within 24 screen points of its
press is such a click too: the first point goes where it was let go and the
shape goes on by clicks. A longer drag still draws the shape (#1630).
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>
* Model cache written by default: exact bodies, stored once, scaled caps (#3053)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* bench-open: say why bodies were not kept
* Exact planes and revolutions; budgets for large bodies (#3053)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Native B-rep: rebuild shells as the kernel made them (closed and seam edges) (#3053)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs: the model cache and the exact native B-rep (#3053)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* bench-open: time a save without the cache too
* Model cache entries are Deflate-compressed (#3053)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Model cache: one that does not fit the design file is left out, never a failed save (#3053)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Sheet metal: a corner relief no longer depends on the order the corners are cut in
The corner reliefs are planned against the base as it is before any of them is
cut, and the two reliefs at the ends of one edge are compared with each
other. Before, every relief was cut into the base as it went, so the corner
after it saw the relief of the corner before it: the square relief reaches
further from its corner than the round one does (4 / cos(α / 2) against 4), so
on a short edge it was left out where the round one was cut, and which of the
two corners kept its relief depended on the order the flanges were made in.
The reliefs themselves are unchanged: a round about the corner through the
angle the base fills there, or the square with that side (a kite where the
corner is not square, its cuts square to the two edges). What is new is that
they are planned first and cut together, so that a corner's relief depends on
the base and on the other reliefs, and not on the order they are cut in. Two
reliefs that would overlap are still refused, now with a note that says so.
Covered: the four relief shapes at corners of 60° and 120°, with the flanges
of equal and of unequal height, through the kernel and through the engine
(where the flat pattern and the DXF keep one clean loop).
Co-Authored-By: Claude Code <noreply@anthropic.com>
* Sheet metal: cover the corner reliefs at a corner that is not square
The engine and the UI had no test at all for a base whose corners are 60° and
120°: the flat pattern, the DXF, the unfold and the rule manager's own square
relief were all untested there. They work; this says so.
Co-Authored-By: Claude Code <noreply@anthropic.com>
* Sheet metal: say what happens when a corner relief is left out
The guide now says that the reliefs at the corners of one edge are cut
together, and that two of them that would overlap leave the second one out.
A relief that cannot be cut is reported, not dropped.
Co-Authored-By: Claude Code <noreply@anthropic.com>
* Sheet metal: write the corner relief's expected corners out
The engine test hard-coded the corners of the trimmed base; it works them out
instead, and the numbers the test and the sketch share are written with the
precision they need.
Co-Authored-By: Claude Code <noreply@anthropic.com>
---------
Co-authored-by: jeremymiribung-blip <jeremymiribung-blip@users.noreply.github.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
Converting an assembly went one occurrence at a time: a part's solid was read
from the file and oriented on the calling thread, and only runs of four or
more solids in one representation used the workers. Each step now takes the
next solids across occurrences and parts (up to 64, stopping where a part not
yet oriented comes back), and the workers read and orient the new parts side
by side, each taking the next one when it is done. Later instances still only
place the part their first instance oriented.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
World placement already shares a part's reference mesh between its
occurrences (#2924), but the display plan the GPU viewport draws was made
again for every occurrence from its placed B-rep: planning every face of
every instance of the part. A placement now carries the definition's plan
cells too, and a placed body's plan (and its CPU mesh) is the definition's,
moved by the placement's matrix (`TessPlan::transformed`): the part is
planned once however often it is placed.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
`a_long_message_keeps_the_buttons_in_the_window` failed now and then on CI
(Send 914.5 -> 888.5, also on #2893): looking for agents runs on its own
thread, and what it finds (and the status it reports) can land between the
two measurements and change what is around the panel. The test now waits for
the search to finish and measures the buttons from the panel's bottom, which
is what the box growing upwards keeps fixed.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Save ran on the window's thread: about 130 ms on the Prusa Core One design,
and more than a second once saves keep the evaluated model. The toolbar's
Save and Save As now run the command on a scratch copy of the session on a
worker; the design waits for it as it waits for a file picker (commands are
refused meanwhile), and the outcome is taken up as running the command here
would have left it: the name and links the save gave the design, its file,
its saved state (`Session::adopt_save`), the echo, the error status, the
recent files and the home thumbnail. A tab closing or the app quitting after
Save waits for the write, as does the app's exit. Scripts, the control
channel and the browser build still save in place.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* inspect.clearance: pairs whose bodies did not change are not measured again
Each clearance check measured every close pair again, though between two
checks usually only a few bodies change or move. Results are now kept per pair,
keyed by both bodies' geometry and placement and the check's settings; a pair
whose two bodies are unchanged is taken from that cache. With 40 overlapping
parts, a check after moving one of them took 0.8 s instead of 23 s (741 of 780
pairs reused). Pairs that could not be worked out are tried again each time.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* inspect.clearance: boxes and placed keys exactly as before the reuse
The pair boxes are taken over the mesh's triangles with finite corners, as a
distance shape's are, so pruning is unchanged. A placed body's identity now
includes its model body's own frame, so two placements are never mistaken for
one another.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* inspect.clearance: a large body is meshed once for all its pairs
Each pair that touches cuts the larger body down to the pair's region with a
kernel boolean, and that boolean meshed the whole body twice: once for its
checks and once inside the intersection. A sheet with 600 faces paid about
2.5 s per pair for meshes that were the same every time.
A body can now keep its meshes for reuse (Body::keeping_meshes); its booleans
take the meshes from there, and the intersection takes its shell meshes
from there too (truck-shapeops: and_with_history_meshed). Clearance uses such
copies for one check. Results are unchanged to the bit. On a 3 x 2 m sheet
with bends and 300 holes, the clip took 0.23 s per pair instead of 2.6 s, and
24 rivets took 5.6 s instead of 16.5 s.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* inspect.clearance: pins on one cylinder with their hole are answered or reported in seconds
A pin seated line to line in its hole, on its axis, and with a blend running into its wall
(a fillet under a head) sent the boolean through minutes of retries that ended unchecked; a
pin a hair off its hole's axis was passed as clear. Clearance now asks first whether the pair
holds such a pin (from the faces' surfaces alone). On its axis the boolean gets a few tries,
which answer plain pins at once; a hair off, or when those tries fail, the shared volume is
worked out apart by coaxial_overlap, which now also takes the zero-offset, equal-radius case,
moves the hole's wall when a blend keeps the pin's from moving, and checks its band more
tightly. What neither answers is reported as unchecked, naming the pin.
The kernel gains with_attempt_budget: the booleans run inside it get a number of tries of
the kernel's own intersection between them, the same on every machine.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* inspect.clearance: the pin tests sit beside the other measured pairs, not at the file's end
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* inspect.clearance: the bodies left whole meet their booleans with their meshes kept
A body that meets several others at one size (a bracket with balls pressed into it) was
meshed again for each pair's intersect: only the clipping used the copies that keep their
meshes. The intersect, the pin work and the union check now take those copies too.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Near-coaxial overlap: ten tries per intersection, enough for a flush head pulled back
#1984's headed pin made before its plate needs more than eight.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aac7mP9rthK3WsPpPRvCsF
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
* Fix large modeled threads and thin crest reconstruction
Helix stations follow the pitch (radial chord error under 1% of it, 48 to
720 stations), and the faceted rebuild retries at the BSP's precision when
the usual weld tolerance collapses a thin crest facet.
Rebased onto main as one commit (the branch's own commits touched UI files
that later moved to #2263 and #2264). Adapted to main's UnitVec3 thread
axis: `*k` in thread_groove and `*cylinder.axis` in the new flange test.
* Fix icon review revisions for same-timestamp edits
* claim: #2430
* feat: copy saved designs between documents as linked components
* fix: keep occurrence movement controls with their document tab
* test: cover linked design clipboard across tabs
* fix: place linked design pastes at the viewport pointer
---------
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
#1949 joined the straight runs of a profile into one side face for straight
extrudes; tapered extrudes (extrude_tapered, extrude_tapered_between) still
sloped each piece as its own face, so a drafted mirrored profile kept the
edge. They now join the runs the same way (NumberedLoop::merged_lines), the
sloped side generated by every piece under it.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
* claim: #2301
* fix: resolve face context actions from persistent face provenance
* test: cover feature targeting from picked faces
* fix: reveal picked face creators in the Browser
---------
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
* Boolean: a pin in a bore of its own size is clear of it, whatever its facets
A pin whose wall is in other faces than the bore's (turned from a profile: three
faces; the bore two) is faceted at other angles. Where one body's faces end on
the shared cylinder (the pin's wall through the end face of the tube, the pin's
end inside the bore) the facets cross by the facetings' error, so the sampled
shortcut doubted its own "apart" answer, every retry failed on the coincident
walls, and the result came back unverified after seconds: inspect.clearance
left such pairs unchecked ("could not be intersected there").
The shortcut now leaves out crossings that lie on a cylinder both bodies have
faces on (axes and radii within the kernel's same-surface tolerance, where both
faces run). A pin a hair off its hole's axis, or one pushed into a narrower
bore, is still not taken for apart.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Boolean: skip only the shared cylinder's own crossings, not a flat along it
The shortcut left out every crossing near a cylinder both bodies have faces
on, from any face: a pin through a D-bore whose flat is 0.022 mm inside it
came back as contact. Mesh::crosses_except now hands the predicate the two
triangles' faces, and only crossings between the faces on the shared
cylinder, or between one of them and an end face across its axis that keeps
to its body's side of the cylinder (within the facetings' error), are left
out. The D-bore's end face reaches into the cylinder to the flat, so the
pin's wall through it counts again; a flat, key or rib along the axis always
counts.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Boolean: a pin in a bore of its size is clear at every placement
The sampled shortcut's check that no facet has material under it on the
wrong side of the other body still caught the facetings of a shared
cylinder. A facet of a pin's end inside the bore (or of the bore part's
end face round the pin) with all three corners on the rim has its middle
just inside the cylinder. Where the other body's chords reach further in
at that angle, the point under it lies inside the other mesh, though the
bodies only touch. Which facets do so depends on the face splits and the
placement, so the same pair read as clear at one placement and as "not
verified" at another.
Such points are no evidence now when they lie on the shared cylinder,
under a facet of one of its end faces: the end faces and band the
crossing check already leaves out. A pin 0.02 mm larger is no shared
cylinder, and its overlap is still found.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
An M8 modelled thread in a 6 mm hole, narrower than M8's minor diameter, was
refused as "the face is too far from the thread's diameter". It now says what
is wrong and what fits, as a hole that is too wide already does (#995): "this
6 mm hole is too narrow for a modelled M8 thread (its teeth would fill the
hole); drill 6.75 mm for M8", with the thread that fits this hole when there
is one. A shaft too thick for its thread gets "make the shaft 8 mm for M8".
Fixes#2688
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
* claim: #2283
* inspect.clearance: a point straight past an edge's end no longer passes as on the face (#2283)
A witness point on a face's surface is kept only if it lies on the face
itself; near the face's boundary the nearest edge decides which side it is
on. A point straight past the end of an edge (on the line through it) was
meant to be left to the other edge meeting there, but "straight" was an
absolute 1e-9 of the body size. With a slightly tilted axis (1.5e-6 rad, as
a STEP round trip leaves it) a point 0.2 mm past the end of a knuckle's seam
edge is 4.5e-6 mm off that line, so its side came down to rounding and the
point on the knuckle's extended cylinder, level with the frame band's rim,
was taken as contact (0.000) at some angles.
Straight along is now relative to the distance from the edge (1e-3 of it),
so the rim edge meeting there decides. Regression test: a hinge knuckle
turned 30-80 degrees between two bands reads 0.2 mm at every angle, with
both witness points on the bodies (41 degrees read 0.0 before).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Joints: a point origin keeps the x it is given
`joint.origin` and `joint.create` take `x` for a point origin (a
direction, laid square to z) and the point snap saves it. Two origins
given at one point with the same z and x now meet as they are: a part
turned a quarter turn about z no longer turns back (and moves) when
rigid-joined there. A point origin without x takes its x from z alone,
as before. An x along z, or of no length, is refused.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Joint relations test: a point origin has an x too
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aac7mP9rthK3WsPpPRvCsF
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
* Browser model: find the placement a body is in
Model::body_path maps a placed body (by its world name) to the occurrence
path of the placement holding it, as the browser's occurrence rows nest.
Every placement is walked once per model, from the placements the
browser already caches, so asking it every frame stays cheap. The next
commits use it to mark the rows leading to a body.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Browser: rows can carry a mark for a lit item and its branch
A row's Mark draws a bar down its left edge: a solid one on a lit item's
own row (which also takes the hover colour unless it is selected), and a
thinner, fainter one on each row leading to it, so the bars trace the
item's branch through the tree. Nothing sets a mark yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Browser: light the rows of what is selected or hovered in the 3D view
The tree now follows the 3D view. Whatever is selected (in the tree or
in the view) or under the pointer in the view lights its row: the
selection colour when selected, the hover colour when hovered, with a
solid bar; each row leading to it gets a faint bar, also when the
folders are closed.
- A body lights for itself and for a selected or hovered face, edge or
vertex of it.
- A sketch lights for its curves, points and profiles, including those
of the sketch being edited.
- Construction planes, and the origin planes and axes, light their rows;
the Origin folder carries the faint bar.
- The rows leading to an item are the design, the placements above it
(by the placement path of a body; for a sketch or plane, every
placement of its component), its folder and its group.
A row's own selected flag, which drag and the context menu go by, is
unchanged: the selection colour for things picked in the view is drawn
separately.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #2920
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Parameters window: the add row lines up with the table's columns (#2920)
The add row was a row of its own under the User Parameters table, so
its fields did not sit under the headings, least of all before the
first user parameter, when the headings had no rows to size their
columns. It is now the table's last row. Its text boxes take the rows'
widths, since a grid cell otherwise gives them only the column's width
so far.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: kezmdor <15539254+kezmdor@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* sketch.edit_text: several texts in one edit, one rebuild
Re-texting the labels of a design took one sketch.edit_text per label, and
each one rebuilt every feature after the sketch (seconds per label in a large
assembly). sketch.edit_text {edits: [{link, text, ...}, ...]} changes them
together in one command: one rebuild and one undo step, all or nothing.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* sketch.edit_text: a live frame takes the edits list too
The live path (live: true) still read one text's link and refused a list.
Both paths now go through one helper.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Add body_name and targets to the solid.box and solid.cylinder rows, the
bodies form of solid.mirror and the names its bodies get, unit-suffixed
parameter values, and the set_parameter argument shape.
Translate 21 messages while keeping Italian partial and preserving existing translations and catalog guards.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Blacksmith's Windows runners start the app but never present a frame (no interactive desktop),
so the startup smoke failed on main after #3343. The shader job stays on Blacksmith.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* CI: merge-queue groups run on Blacksmith, test shards on 16 vCPU
The merge queue builds up to three groups at once, but every group's
checks, Clippy, doc and reference tests and all four shards ran on the one
self-hosted machine, so the groups waited for each other. Merge groups now
run on Blacksmith like pull requests (test archive built once, shards on
16 vCPU). Pushes to main keep the self-hosted runners and their warm build
directories, and a push the queue already tested still runs only the
repository checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
* CI: every job on Blacksmith: pushes to main and the Windows jobs too
Pushes to main ran on the one self-hosted machine and the Windows jobs on
GitHub's runners. Both move to Blacksmith (Windows Server 2025, 8 vCPU),
so nothing in ci.yml depends on a single box any more. With no
self-hosted job left, the local routing goes: no warm build directories,
every run builds the test archive once and the shards run it, and main
saves the build caches every other run restores.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
---------
Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #2621
* Sketch: dragging part of a selection moves the whole selection (#2621)
A drag that started on a selected sketch line or point moved only that
entity; the rest of the selection was left to the solver, so geometry that
kept only its length (the reporter's 5 mm tail) trailed behind like a string
and chamfers stretched.
- sketch.drag takes `with`: the rest of a selection dragged by `entity`.
Every free point of all of it slides by the cursor's move and the held
solves keep them all (circles and ellipses keep their radii), so the
selection moves as one (`plan_group`). `with` is checked and capped like
the other parameters.
- The viewport sends the rest of the selection when the drag starts on a
selected point or curve. A curve's own points, picked with it by a window,
are not a group: a circle grabbed on its edge still takes its radius.
- Snapping looks at all the moving points.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A sketch and the feature built from it may share a name, and the sample and
oracle designs do. `timeline.add` refused such a recorded name, so a design's
own exported script failed to replay. `Document::insert_feature` now keeps the
name, as `add_feature` does; a blank name still gets a fresh one.
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Deleting a pocket or a boss from an imported part failed with "face N cannot
close: cannot attach a plane to a wire that is not on one plane". The face
around the gap only loses an inner loop, but it was rebuilt as a new plane
through its outer boundary, and an imported boundary is often a few 1e-5 mm off
the plane: within the planar test, too far for a plane fitted to it. Such a face
now keeps its surface and only drops the loop.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Replace Face rebuilt its body with ModelBody::new and dropped the body's
lineage, so no face of the result was named exactly: a sketch on any face of
it, even one the replacement didn't touch, failed with "this face can't keep
an attachment". Each face it moves is now a lineage step, as in Offset Face
(which does the same geometry), and the result keeps the lineage.
The test of a body without history used Replace Face as its example of an
operation that reports none; it now lifts the block with Align instead, the
same geometry.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
* claim: #1985
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L15TLCjpXsQdp8ovC4ZacH
* STEP export no longer hangs on a box with a screw hole (#1985)
The export reads its own STEP text back, and meshing the read-back screw
hole never finished. On a seam cylinder the boundary walk projected points
to angles up to 5e7 rad apart and moved each by two turns at most, so the
11-point loop spanned millions of turns and the polyline closing it across
the seam had 4.3 million points. Each projected parameter is now brought to
within half a period of the point before it, whatever the gap.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L15TLCjpXsQdp8ovC4ZacH
* Second #1985 regression: the reporter's flanged enclosure
The issue's reporter delta-debugged a second design whose STEP export hung
on main: a flanged enclosure with two standoffs and no screw hole. It hangs
without the get_mindiff change and reads back with 29 faces and the same
volume with it. Both tests share the read-back checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L15TLCjpXsQdp8ovC4ZacH
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
* claim: #3052
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Sketch Trim: the hover shows the piece a click takes away (#3052)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Sketch Trim: the piece is shown in the error colour, thicker
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Trim hover: preview keeps its doc line; the piece is worked out once a frame
The new function had gone between preview's doc line and preview. The
cursor's Trim piece is now kept by design revision, sketch, curve and
cursor, so the viewport's hover and the preview no longer both intersect
the curve with every other curve each frame.
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>
* parameters: import evaluates once; parameters.change takes several names
parameters.import checked every row through change_param, which cloned the
design and evaluated every parameter per row: 2000 rows took 9.5 s, 2823 in
an assembly minutes. Rows are now checked and applied in one pass with the
values carried along, then evaluated once (2000 rows: 0.016 s), still all or
nothing; a row may name a parameter defined by a later row.
parameters.change {changes: {name: expression, ...}} sets several existing
parameters together, rebuilt once and undone in one step.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* parameters.change: the params doc names the `changed` result of `changes`
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
The self-hosted-untrusted pool is being retired for CI, so fork PRs whose
number ends in 7, 8 or 9 no longer route there. Build tests, the test shards
and the reference tests reduce to our own runners for main and the merge
queue, Blacksmith for everything else; the build's 90-minute gVisor timeout
goes. Blacksmith VMs are ephemeral and fork runs get no secrets.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
Blacksmith runners report runner.environment == 'self-hosted', and a list of
our machines' name prefixes breaks when a box is added or renamed. Select
checks already decides where each job runs; its local and untrusted outputs
(and the trusted-run condition in the repository checks) now drive the warm
build directory, cache and lavapipe steps. Same results as before on every
event.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
Re-applies #2237 (reverted in #2277 while the Blacksmith account awaited verification) on today's
ci.yml: every ubuntu-24.04 job or fallback runs on blacksmith-<N>vcpu-ubuntu-2404, sized as #2237
did (quick-compile, new since, gets 8), and steps that asked runner.environment now tell our own
machines apart by runner name, since Blacksmith runners report 'self-hosted' too. The trusted
self-hosted routing and the fork routing are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
From the review: the guidance said a pull request's run tests it merged with the latest main,
which is only main as of that run; a break between two pull requests that land close together
shows up in main's own run. It also forbade rebasing. Now: don't push only to catch up; when you
push a change anyway or GitHub reports a conflict, rebase onto main (--force-with-lease on your
own branch) or merge it in once.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
From the review: the web app embeds ATTRIBUTION.md, so the Docker image's Markdown exclusion now
keeps it; and THIRD-PARTY-LICENSES.txt also reads the vendored crates' licence files and the
fonts' OFL texts, so changes there regenerate it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
From the review: git patch-id --stable ignores whitespace, so a conflict resolution that only
respaced the change (inside a string, or Python indentation) counted as no change and was
carried; --verbatim compares every byte. And a passing run is now taken only from this pull
request (by number, or for a fork, where GitHub lists none on the run, by head repository and
branch), not from another pull request with the same head commit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
From the review of this PR:
- What counts as code lives only in scripts/ci/affected.py now: nothing selected and not full
means no code changed, on pull requests and pushes to main alike. The untested bash copy in
Select checks disagreed with it. Root files and folders a crate embeds now select that crate
(the licence texts and NOTICE: the engine; skills/: the MCP server's tests), where before a
change to them ran no test at all. Other workflows and the bots' scripts select nothing.
- A push to main skips the native jobs only when nothing a build reads changed since the last
push to main whose run passed. Comparing with the push just before was unsafe: a newer push
cancels a waiting run, so that commit may never have been tested.
- Fail fast applies to pull requests only. On main, nightly and Run workflow the test build and
the shards run regardless of the repository checks, so the action matrix and property tests
that hang off Build tests keep their signal. Doc tests (main and nightly only now) no longer
wait for the checks.
- tests/robustness/ turns on the goldens, so a change to goldens.json runs the seed tests.
- tests/oracle/ no longer selects the kernel, which never reads it.
- The workflow header and CONTRIBUTING describe what main and pull requests now run.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
A change that merged main twice in a row ran the full set on every other push: the change before
the second merge was itself a carried run, its shards skipped, so it had no passing shards to
carry. The script now walks down the first parents through consecutive merges of main that kept
the change's patch (each equal to the next), at most 10, to the run where the shards ran.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
The guidance told contributors and agents to keep their branch current with every upstream merge
and to re-run CI after integrating. Over three days that meant 1,241 merges of main into pull
requests and 730 re-runs: about 45,000 of 79,800 ci runner-minutes, on runners that capped how
fast everyone's checks ran. CI already tests each pull request merged with the latest main, and
a real failure fails again on the same commit. Now: merge main in only when GitHub reports a
conflict, re-run only a job runner trouble cancelled, and maintainers add approved-for-ci when a
pull request is ready to merge and merge approved ones in groups by area.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
Merging main into a pull request (Merge branch 'main', or GitHub's Update branch) restarted the
whole run. Over three days 1,241 such merges started 966 runs and about 21,000 runner-minutes, and
872 of the merges brought in nothing that touched the pull request's files. Select checks now
recognises the case (scripts/ci/main_merge.py): HEAD is a merge whose other parent is on main, the
change's own patch is unchanged (git patch-id), and a ci.yml run of the change before the merge
passed with its test shards. Then only the repository checks run, the ci job names the passing run
in its summary, and main tests the merged result after it lands. Anything else, or any doubt,
runs the full set as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
Doc tests move to main and nightly, beside the reference tests and the Windows jobs: on pull
requests they were the only failing job 3 times in about 7,900 runner-minutes over three days.
Build tests, the test shards and the doc tests now start only after the format and repository
checks pass; 212 attempts in those three days ran their heavy jobs beside a failed cheap check.
Also corrects the header, which still said pull requests run the Windows jobs and the reference
tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
- Pushes to main use the same code/no-code rule as pull requests (`touches_code` in Select
checks): a push changing only documentation, packaging, other workflows or the bots' scripts
runs the repository checks only. With the merge queue off, every push to main ran the full suite
and the Windows jobs, docs-only ones included.
- affected.py: test data under tests/ selects the packages that read it (robustness, hole matrix,
projection, taper: solidcraft-engine; oracle: the CLI, the kernel, xtask; action matrix: the
CLI, the engine, xtask), and a vendored crate the packages that use it (truck: the kernel and up;
egui-wgpu: the UI and the apps), instead of the whole workspace. tests/tools and unknown vendored
crates still select everything.
- Docker (the web app image) skips pushes touching only docs, test data or bot scripts; Pages skips
test data and bot scripts; Generated files runs only when the manifests, lockfile, command
registry, CLI or xtask change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
* inspect.clearance: pairs whose bodies did not change are not measured again
Each clearance check measured every close pair again, though between two
checks usually only a few bodies change or move. Results are now kept per pair,
keyed by both bodies' geometry and placement and the check's settings; a pair
whose two bodies are unchanged is taken from that cache. With 40 overlapping
parts, a check after moving one of them took 0.8 s instead of 23 s (741 of 780
pairs reused). Pairs that could not be worked out are tried again each time.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* inspect.clearance: boxes and placed keys exactly as before the reuse
The pair boxes are taken over the mesh's triangles with finite corners, as a
distance shape's are, so pruning is unchanged. A placed body's identity now
includes its model body's own frame, so two placements are never mistaken for
one another.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* inspect.clearance: a large body is meshed once for all its pairs
Each pair that touches cuts the larger body down to the pair's region with a
kernel boolean, and that boolean meshed the whole body twice: once for its
checks and once inside the intersection. A sheet with 600 faces paid about
2.5 s per pair for meshes that were the same every time.
A body can now keep its meshes for reuse (Body::keeping_meshes); its booleans
take the meshes from there, and the intersection takes its shell meshes
from there too (truck-shapeops: and_with_history_meshed). Clearance uses such
copies for one check. Results are unchanged to the bit. On a 3 x 2 m sheet
with bends and 300 holes, the clip took 0.23 s per pair instead of 2.6 s, and
24 rivets took 5.6 s instead of 16.5 s.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
* claim: #2535
* claim: #2535
* Sketch: preview all slot sides and caps while choosing width
Fix missing temporary slot geometry for all five tools and paint every preview curve with a shared sampling budget. Preserve the arc slot construction guide. Add geometry, painting, invalid-input and interactive typed-width regression coverage.
* Sketch: honor typed slot widths below the pointer minimum
---------
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
* Extrude: a profile edge short of a face is not grown through the skin beyond it (#1669)
A cut or join profile edge that lies on a face of the target is grown off it by 0.5 % of the body, so that the tool's wall does not lie in the face. Whether it lies on a face was judged half a growth step either side of it: empty just outside, material just inside. An edge 0.25 mm short of a 3.75 mm wall's far face reads the same, and the growth cut through the skin it should leave (55 050 mm³ instead of 56 130 on the test's wall). Now the space just past the edge, 1e-4 of the body out, must already be as the operation wants too.
In #1669, the pleat's window built directly at the far end of a case opened through the wall at 3.75 and 4 mm, while its mirror, built without the growth, rightly left the skin and then failed to join the loft through it. Built either way, the window now leaves the skin.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Regression tests: the issue's pleats mirror and build at the far end (#1669)
The issue's battery case at 3.5 mm: the mirror of the window and the pleat builds (it needed #1670's meshing of twisted faces), and at 3 mm the same window and pleat built directly at the far end of the finished case join it (they needed #1667's end rounds).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Far-end pleat test: the direct build matches the mirror's volume
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Erikas <erkexzcx@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
A loop made only of arcs took the all-arcs shortcut in `offset_loop`, which
moves each arc by its radius and keeps its start and sweep angles: exact for
arcs on one circle or meeting tangentially, but where arcs of different
circles meet at a corner (a circle with lobes) the moved arcs no longer
met, and the sides were built through the wrong points without an error.
`arc_corners` moves each such corner to where the two moved circles cross,
nearest the old corner (`circles_cross`, closed form), and cuts both arcs
there. Circles that no longer cross are refused ("not supported yet"), an
arc the taper turns around closes the profile. Between the sections the
corner is a straight ruling, as for every side in `taper_loop`: on the test
profile (2.5 mm at 45°) it stays within 0.015 mm of the exact corner.
Test `tapered_extrude_of_arcs_meeting_at_corners`: the volume against the
integral of the union of the shrunk circles, 18905.42 mm³ against 18907.67
(was 18975.45).
Co-authored-by: Mariusz Pacek <pacekm90@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
* Snapshot: draw a see-through appearance see-through, and give the CLI --body, --tool and --section
A headless picture dropped a body's opacity: the scene carried only a colour, so a design with
a translucent appearance was drawn opaque by `solidcraft-cli snapshot`, by MCP screenshots and
in a design's thumbnail, although the window has always drawn it as glass. There was then no
way to look at the body a cut would remove inside the material it meets: only a volume to
measure and reason about, which hides exactly the errors a picture makes obvious (a tool that
stops short, or lands in the wrong place).
- `SceneMesh` carries the body's opacity; `view::build_scene` takes it from the design
(`model_body_opacity`), and a selected body stays solid so the selection colour reads.
- The GPU capture routes a see-through body into the scene's glass at its own alpha, as the
viewport does; the CPU rasterizer draws the solid bodies first (they own the depth buffer),
then the see-through ones blended over them, far sides before near sides and without writing
depth, so what lies inside shows and a line between the two is not hidden.
- `snapshot --body NAME:opacity=0.3,color=#rrggbb`, `--tool NAME` (that body solid in the tool
colour, the rest see-through) and `--section z=27` run the commands the app itself runs
(`appearance.assign`, `inspect.section`): the options are shortcuts, not a second renderer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Tests: measure the tool colour as it is drawn through a see-through target
The tool-colour filter was derived on paper and demanded a red more than twice either other
channel, which the target in front of it tints away (measured: (200, 108, 85) through opacity
0.3); and the colour check painted the tool, which a solid target hides.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fmt
* Snapshot options: strip the colour's # rather than slicing it; README
Slicing `value[1..]` on an input-derived string is what the house rules ask us not to write,
even where a length check has already made it safe. The error a failed assignment reports now
names the body rather than claiming it came from --body, since --tool reaches it too.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* --tool: assign the bodies around the tool in groups, not one command at a time
Each appearance.assign is an undo step with a document snapshot behind it, so one per body
made --tool cost grow with the size of the design for no reason: the bodies around the tool
all get the same opacity, and the ones that share a colour can be named in one call. A design
whose bodies all wear the default grey now costs two commands whatever its size.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Rasterizer: say where its glass and the GPU's differ
Splitting each see-through body by facing orders its own two layers; it does not sort across
bodies, which the GPU does. Worth stating rather than leaving a reader to find it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
`joint.as_built` with `at` an origin on A or B keeps that origin on its
side (found again by its face's name on each solve) and holds the frame
where it is now on the other, so the joint follows the face or hole as
the design changes. Without `at`, or with an origin on another part,
both sides hold their frames as before.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
* Fillet fit: a round counts only what lies between its edge and its arc
The fit check refused a round when any other face of the body reached into
the wedge between its edge and its two setback lines. A round changes only
the part of that wedge between the edge and its arc: a face on the ball's
side of the arc is left as it is. An outside round concentric with the
inside round of a 3 mm wall (R18 over R15) was refused as too large
(largest 14.21 mm) although it keeps the wall 3 mm thick.
The face check now tests a round's fan of prisms from its edge to its arc
(Frame::pieces, already used for blends that cross), after the wedge as a
quick filter. Chamfers keep the wedge, which is what they change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L15TLCjpXsQdp8ovC4ZacH
* Fillet fit: a curved face clears a round's thin end pieces only by a chord (#324)
The fan test shrank each piece by the curved faces' tolerance; the thin pieces at the
arc's ends vanished, so a hole wall 0.3 mm into a round beside it was missed and the
round failed to build instead of offering the radius that fits (issue_324_tests).
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: #3001
* fix(ui): Convert Mesh asks for the mesh body and its tolerance (#3001)
It had no dialog, so the toolbar ran mesh.convert with no body and it always
reported "select a mesh body". A dialog now takes the mesh body (a selected one
fills it) and the tolerance.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #2943
* fix(update): release-note links open in the browser (#2943)
The notes kept a link's text and dropped its target, so the blue text could not
be clicked. Links keep their web address, and each paragraph or item is
followed by its links as buttons that open them, as the agent chat shows links.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #2823
* fix(view): lines thinner than a pixel no longer vanish without multisampling (#2823)
A line quad narrower than a pixel covers no pixel centre when it falls between
two rows, so the grid's 0.7 px lines came and went as the view zoomed where the
viewport draws without 4x MSAA (some browsers and devices). Such lines are now
drawn a pixel wide, faded by the width they lack.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #2700
* Two-sided extrudes: the second distance goes the other way from the first side, either way round (#2700)
extent_sides ignored `direction` when an extrude had `distance2`, so through all with
`direction: negative` and a second distance went the positive way. The second distance now goes
opposite the first side wherever that went (direction: negative, a negative distance, a To
Object below the sketch). A pending format step makes older designs' reversed two-sided
extrudes positive, which keeps their shape.
The Extrude dialog's Two Sides shows Distance 2 for every extent type (it was hidden for All and
To Object but still sent as 10 mm) and offers Reverse; the second arrow points the other way
from the first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* The two-sided arrows test checks each tip against the sketch, not just their order
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #2592
* fix(agent-chat): the design bridge is named solidcraft-app (#2592)
A user's own MCP entry named `solidcraft` (as docs/mcp.md suggests) took the
bridge's place in agents that keep their own entry on a name clash, so the chat
worked on a headless session instead of the open design.
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>
On Windows, Git's default core.autocrlf checks the icon masters and the
palette out with CRLF, and `cargo xtask icons` failed four ways:
- the accepted-icon hashes (lint_icons.py and the review page's
review_state.master_hash) were taken over the files' bytes, so every
accepted icon read as changed; both now use master_hash, which hashes
the text with CRLF read as LF;
- icon_palette.revision hashed the bytes while save_colours hashed the
text it read, so every palette save was refused as stale; revision now
reads the text too;
- the tools and their tests read and wrote text in the locale's encoding
(cp1252 can't write U+2194); they now say utf-8;
- the icon inventory listed UI files with backslashes; it writes /.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
* Bill of materials: part properties on components and assembly.bom
A component definition now carries part properties (part number,
description, revision, other tools' keys) and free text properties,
saved with the component and shared by its occurrences.
component.metadata and component.metadata.set read and change them in
one undo step; Paste New copies them; parts.insert numbers standard
parts from their standard and size, and parts.resize renumbers them.
assembly.bom counts the placed components (flat: parts at any depth
through their subassemblies; top: direct children), grouped by part
number or by definition, sorted by part number, and writes CSV (fixed,
quoted columns) or JSON. The app gets a Bill of Materials window on the
INSPECT panel that lists, edits and exports the rows.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Bill of materials: name a merged row's equal definitions once
Two standard parts with the same size are separate definitions with the
same name; their row now reads "SHCS M6x20" instead of repeating it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Window widths test: close the Bill of Materials window between commands
The test closes the floating windows a command opens before starting
the next one; the Bill of Materials window was left open and measured
against the next dialogs' limits.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Bill of materials: sub-parts in their part's row, and part properties without re-evaluating
assembly.bom takes group_children: true. A part with a part number keeps
what is placed inside it (a bolt's washer and nut) in its own row, listed
under it as children (items 1.1, 1.2, with each and count) instead of rows
of their own; bolts with different contents stay different rows, and
total_occurrences is the same either way. The window has a switch for it
and shows the children under their part; the CSV lists them after their row.
Changing part properties re-evaluated the model, re-seated every joint and
rebuilt the placed model: 361 ms per change with 30 jointed bolts. A change
that only touches components' metadata and properties now takes the same
path as a render-only change (proved on the whole Document), also for its
undo and redo: 0.6 ms.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Bill of materials: look the structure up once, not per subassembly
Deciding whether a subassembly is a part (it has bodies of its own) scanned
every body of the model for each subassembly, and each body looked up its
feature by a linear search of the timeline: subassemblies × bodies × features.
assembly.bom now indexes the placements, the body owners
(Document::body_owners: one pass over the bodies with the timeline in a map,
the same rule as model_body_component) and the numbered components once per
call. 600 bodies and features with 60 subassemblies: 8.1 ms → 1.3 ms; the
old cost grows with the cube of the design's size.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
* Centralise tool modifiers with classic defaults and preview-safe Ctrl.
Route sketch/selection chords through a shared resolver. Keep Alt snap
suppress and dual toggle as defaults; optional Shift add-only and Ctrl
snap mapping. Skip preview rollback while a manipulator drag is active.
* Address #1616 review: classic snap suppress, single viewport owner, docs.
Default suppress to Ctrl/Cmd or Alt; drop navigation set_suppressed; remove unused label_key; add preview rollback gate test; document sketch modifiers.
* fix(#1616): correct Modifiers primary in tests; simplify rollback fixture.
* Treat Ctrl as primary off macOS, and rustfmt the modifier files.
* kernel: trim keeps picked regions, mutual trim, curves swept along a direction (#767)
Trim moves to surface_trim.rs: TrimKeep::Side (the old behaviour) or
Regions (the connected pieces the points pick, parted where the tool runs
along an edge). mutual_trim cuts two one-face surfaces by each other's
surface, extending one that stops short when asked. SplitTool::Curves takes
a sweep direction; a closed curve's field is signed by inside/outside
instead of extending its end segments.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM
* kernel: classify display edges of a face with a closed-edge hole without a panic
A hole cut by a closed curve is a loop of one edge that starts and ends at
one vertex; mapping the face rebuilt that edge through a check that
panics on it (the guard caught it and dropped every smooth flag of the
body). Such faces are now left out of the ruled-surface test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM
* surface.trim: keep several regions, trim by projected sketch curves, mutual trim (#767)
keep takes a list of points (the regions they pick); curves: {sketch,
curves?, direction?} sweeps sketch curves onto the surface (along the
surface's normal: not supported yet); with + keep_with trims two
surfaces by each other, extend? meets a short one, stitch? sews the
kept pieces. Trimmed faces carry their lineage, so they are named
after the faces they are pieces of. New fields noted in a pending format file.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM
* Trim dialog: sketch curves as the tool, several keep points, mutual trim (#767)
The tool input takes sketch curves too; keep points are places (several
on one face add up); another surface with its keep points makes a mutual
trim with Extend to meet and Stitch toggles. Edit Feature brings it all
back. Labels in every locale catalog.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM
* ROADMAP: M8 trim by projected curves, several regions and mutual trim (#767)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM
---------
Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
* Sketch points placed by expressions over the design's parameters
A point typed as an expression ("W", "H / 2") in sketch.point, sketch.line and
the other point arguments is fixed where its expressions come to, and moves
there again before each solve when the parameters change: an outline that
follows its parameters needs no dimensions and no solve per change. The
expressions are saved with the point (one pending design-format change), listed
with the sketch's other expressions, and renamed with their parameters; one
that cannot be worked out is refused.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Sketch point expressions: accurate params doc, out-of-range regression test
The sketch.line params said every point argument takes expressions; only those that
also take a point id do (the rectangle corners, an arc's through point and the like
are plain [x, y]). PointExprs' comment called its expressions mm. A test covers a
parameter that puts a placed point out of range and a design file whose point
expression overflows: both are errors, not crashes, and the sketch recovers.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
assets/locales/ belongs to no crate, so affected.py selected the full
workspace (every crate, all four test shards) for a catalog-only PR. Only
solidcraft-ui-egui embeds the catalogs and xtask checks them; map the folder
to those two packages and their dependents.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The window listed (and so evaluated) every parameter and re-checked the add row's
expression on every frame, and laid out every row. It now lists once per design
revision, caches the add row's check, and lays out only the rows in view.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The chat in a window of its own takes the files dropped on it and shows the drop hint while files are dragged over it; a window floating inside the app counts as the chat's place for the app's drop handling.
A PDF goes whole to agents that take embedded documents; to the others, or past 12 MB, the message names it with its type, size and place. Any file that is not a picture, text or CAD file is attached by reference instead of refused. Chips show the type's extension; the file dialog offers All files.
Files copied in the file manager attach as files, a picture on the clipboard attaches as a picture, text pastes as text. A picture-only or files-only paste is seen through the V key coming up (egui-winit sends nothing when the clipboard has no text); macOS sends no key-up while Cmd is held, so there only pastes that come with text are seen and + > Paste a picture still works.
* CI: heavy checks run after review (approved-for-ci label)
Pull requests without the approved-for-ci label run Select checks, the
format and repository checks and a quick cargo check. The label runs the
full suite, and every push after that does too. Other labels start no work
and never cancel a run. Main, merge groups, nightly and Run workflow are
unchanged.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* CI: approved fork PRs run heavy jobs on self-hosted-fast in private directories
An approved-for-ci label on a fork pull request sends Clippy, Build tests,
the test shards and Doc tests to the trusted runners, with private copies
of the build directory, Cargo home and rustup home that are deleted after
the job. Only the labeling event qualifies; later pushes use the old fork
routing. The skipped run of an unrelated label names its ci job differently.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Revert "CI: approved fork PRs run heavy jobs on self-hosted-fast in private directories"
This reverts commit 439f2f7dab.
* CI: name the ci job differently on an ignored label's skipped run
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
An orbit drag now applies its whole turn so far to the camera it began
with, instead of adding each pointer step to the camera of the step
before. The view then depends only on where the pointer is: dragging
past straight up or down and back no longer loses the pitch that the
clamp swallowed, and returning to the starting point brings back the
starting view. The pivot is still picked once, when the drag begins,
and kept still on screen by the rigid turn about it.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #2544
* S command search and the command line: names, places and matching in the current language (#2544)
The S box showed each command's English label and toolbar place in every language, and found
only what was typed in English; the command line matched English only too. Rows, places and
tooltips now go through the label lookup, and the query matches the translated label, tab and
panel as well as the English ones and the id. The pin tooltips get catalog entries.
A new sweep (i18n_painted_tests) runs every language, reads the text each frame paints, and
fails on English painted where the language has a translation, skipping the design's own names
and key names, so a painter call that skips the lookup can't come back unnoticed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* The painted-text sweep also covers every Preferences page and four right-click menus
Each surface must show (the Preferences title in that language, or new text for a menu), so the
sweep can't pass by painting nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* S box rows: labels stay 'static; the place's separator is drawn, level in every language
A font's "›" (and one centred on the row) sits on the baseline beside Japanese or Chinese words,
where it reads as a comma. The separator is now a drawn chevron (glyphs::breadcrumb) centred on
the ink of the panel's name. search() returns the place as (tab, panel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* A new design's name, Untitled, shows translated in the browser too, like the other default names
The tab and the save prompt translated "Untitled" through their own special case; the browser's
top row showed it in English. It is now one of the default names the label layer translates
(Locale::default_name, with Sketch1, Body2, Component3), so every place that shows a given name
shows it translated, and the two special cases go. The painted-text sweep no longer skips a design
name that the language translates, and splits a tab's dirty mark off, so it catches a place that
shows the raw name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #2424
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SBN24y2TcqRcm2zn8Gkj27
* Sketch Offset: constrain to the ends of an offset spline (#2424)
An associated offset spline hid all of its generated fit nodes, its two
end points included, so nothing snapped to them, and a curve drawn from
one shared the node and was refused ("invalid offset output point roles").
- An open offset's end points are shown and snapped to again; its
interior fit nodes stay private.
- After every sketch edit, a curve that took an offset node gets its own
point there, coincident with the node: the offset keeps sole ownership
and the curve follows the offset when its source moves.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SBN24y2TcqRcm2zn8Gkj27
* Scenario: lines snap to and close onto an offset spline's ends (#2424)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SBN24y2TcqRcm2zn8Gkj27
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Clearance: check a shared volume against the bodies and their union
An intersection can come out well formed and still wrong: one of a
mirrored pair of imported parts shared three times the volume of its
twin. The shared volume is now compared with vol(a) + vol(b) - vol(a ∪ b),
on the same (cut down) bodies; when they differ by more than 1 % (plus the
bodies' own measuring error), the pair is reported unchecked with both
numbers. Where the union gives no answer, the intersection stands.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Clearance: let the union cross-check allow for the bodies' measuring error
vol(a) + vol(b) - vol(a ∪ b) is a difference of measured volumes and
carries the error of all three, so correct small overlaps on large bodies
came back unchecked (0.96 mm³ where the union implied -0.75). measure
keeps a face's fine tessellation when it is within 1 % of one with a
5 times coarser chord, so a measured volume can be off by up to 0.25 %:
the allowance is now 1 % of the larger of the two answers plus 0.25 % of
the three volumes. Past it, a union that implies nothing (within that
noise of zero, or below it) while the shared part is at most a tenth of
the smaller body cannot tell, and the shared part stands. A shared part
of 412 mm³ where bodies of a few thousand imply 141 is still caught.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Clearance: tighten the union check's measuring allowance to 1e-4
The old note cited measure's 1 % gate, which compares face areas, not
volumes, and its 0.25 % was about 100 times the disagreement the bodies'
measures actually show. The allowance is now justified from volumes: a
curved face's chord shortfall cancels between a body and the union except
where the chord changes, which stays near 2e-5 of the three volumes; 1e-4
leaves five times that. A 40 mm roller pressed 0.2 mm into a 2 m plate
exercises it: with no allowance the pair is left unchecked (7.52 mm³
measured against 8.16 implied), with it the overlap is reported.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
* geom: UnitVec3; a face's analytic axes and normals are unit by type
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* geom: UnitVec3 adds to and subtracts from a Vec3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* kernel, doc, engine: cylinder axes and measured normals held as UnitVec3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* kernel, engine: functions that take a unit axis or direction say so by type
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* doc: a face's normal goes to Plane::from_normal as is
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* kernel, doc: main's new cylinder helpers take the face's axis as a UnitVec3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #2474, #2494
* fix(ui): construct tools ask for their picks instead of failing (#2474, #2494)
The 13 construct tools without a dialog of their own ran their command with no
parameters from the toolbar and reported that `a` (or `face`, `edge`...) is
required. One pick dialog now serves them: labelled inputs per tool,
preselection fills them, and each pick becomes the construction reference the
command takes; Point Along Path keeps its position.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Reword comments, docs and package descriptions so they describe SolidCraft's own behaviour instead of comparing it to another product. The trademark notice, the commercial-CAD rule and the navigation preset name stay as they were.
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
A round origin's end is kept by the name of the face across the rim. On
a face in pieces (a sheet split by a groove or a bend) that name lists
the faces round the piece, and when those change (a hole nearby moves
or goes) the name matched neither rim, so the end fell back to the
nearer end of the stored pick: a pick near one end of a thin wall that
the design moved past mid-wall turned a part through 180 degrees. The
end whose rim matches the name better now wins: the face itself, a face
merged into it, then a piece of it, then the same face by its stem.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This reverts commit cac4bccea8. Orbiting
about the point under the cursor goes back to 0.15.0's pivot choice
(the picked face or vertex point, an edge's midpoint, or the visible
geometry's centre over empty space), matching 0.16.1.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
the format and repository checks job still took a self-hosted-fast runner for every same-repo
pull request. It now follows the same rule, and the notes at the top say so.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Pull requests that can change a robustness golden check every seed but the 17 slowest, in the
same nextest run as the other tests instead of a second step after it (16-30 minutes on its own).
The slow seeds (about four fifths of the goldens' time) still run in the merge queue, on main and
nightly. A failed golden still prints how to re-record it, and junit.xml now holds the whole run.
- The slowest tests start first (nextest priority), so a shard no longer ends with one of them
running alone.
- tspline::tests::hostile_tsplines_never_panic joins the heavy tests pull requests skip (up to
12 minutes on a GitHub runner; the same kind of check as hostile_params_never_panic).
- The ci profile builds dependencies without line tables: the test archive pull requests' shards
download goes from 3.4 GB to 2.2 GB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On main and in the merge queue every test shard ran on the self-hosted machine but waited for
the GitHub-hosted Build tests job and then spent about 12.5 minutes downloading its 3.2 GB
archive (four shards at once over one connection). There, each shard now builds the tests in
the runner's warm build directory (the existing build-tests one) and runs its slice directly;
Build tests runs only where the shards run on GitHub's or the untrusted runners.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Same-repo pull requests ran their heavy jobs on the three fast runners too, and the merge
queue's test shards waited behind them for hours. Pull requests now use the GitHub-hosted path.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Orbiting about the point under the cursor (or the origin) turned the camera
about its target and then panned to put the pivot back. The pan works at the
target's depth with the parallel-view scale, so in a perspective view the
pivot slid off its pixel and the model swung about the camera.
The camera now turns rigidly about the pivot: Camera::orbit_about keeps the
pivot's coordinates in the camera's frame and rebuilds the target from the
new orientation, so the pivot stays exactly on its pixel in both projections.
The Shift snap spring and view.navigate's pivot orbit use the same.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* UI: the Interference dialog and the continuity legend's Gap are translatable
The Interference dialog's title, its Bodies input, the Compute and Compute
again button, the checked-bodies count, "could not be checked" and the
clearance error were English in every language. They now come from the
catalogs (en, ja, zh, de, ko, fr).
The continuity legend's Gap (an opening between faces) and the sheet-metal
hem's gap shared the message "Gap", translated as the hem's gap (French
"Jeu"). The hem keeps those translations under ui.sheet_gap; the legend
gets ui.continuity_gap, and both read their message by id.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L15TLCjpXsQdp8ovC4ZacH
* Polish: the sheet-metal gap is ui.sheet_gap too
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L15TLCjpXsQdp8ovC4ZacH
* Every catalog gives the continuity legend's Gap the word it showed before
The catalogs without their own ui.continuity_gap would have fallen back to
English in the legend; they now keep the translation the shared Gap gave
them. French and German keep separate words.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L15TLCjpXsQdp8ovC4ZacH
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Browser sidebar: drag its right edge to size it, and remember the width
The left browser sidebar was fixed at 250 px. Its right edge is now a drag handle: the width follows the pointer, is clamped to 170-620 px, and is kept in the preferences (browser_width) so it survives a restart and Reset Defaults returns it to 250. The few pixels of grip sit inside the panel's own margin, so the tree rows and the scroll bar keep their input.
* Browser: float the tree over the workspace on a translucent card
Preferences -> General gains "Parts browser -> Float it over the workspace": the tree then draws on a card over the 3D view instead of taking a column, so the workspace keeps the whole window width. The card hugs the tree, scrolls past 70 % of the view's height, and uses the remembered browser_width. The canvas keeps every point the card does not cover, and turning the preference off docks the browser again.
In a CJK UI, the primary CJK face goes first in Proportional, so digits, units and
symbols in mixed labels share the CJK text's metrics and baseline. A Korean UI puts
the Hangul face first. The user's ~/Library/Fonts is scanned for a CJK font on macOS.
Rebased onto main as one commit (main had added the Thai fallback).
Co-authored-by: Mithril-3d <145893541+Mithril-3d@users.noreply.github.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
* claim: #2089
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* 3MF import: read objects in other model parts (production extension) (#2089)
Slicers save multi-part projects with the 3MF Production extension: the
root model's objects are components with `p:path` naming another model
part of the package, which holds the meshes. The reader skipped those
components and build items, so such files did not open at all.
Components and build items now keep the part they name, and the parts
objects name are read once each (looked up through an index of the zip
entries), with the number of part names and their total size capped.
Each part's unit is applied as it is read, so parts in different units
fit together. Cycle detection keys on (part, object), so parts that
place each other in a loop are refused.
Tests: components and build items in another part, part names in any
case, a part in centimetres under a root in millimetres, and hostile
packages (missing part, a path out of the package, an empty path, an
unknown object, a part that is no model, a loop between parts, more
part names than a package can hold). The 3MF mutation fuzz also runs
on a production-extension package.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* 3MF import: count every placement toward the budget (#2089)
An object that only places other objects added nothing to the triangle
budget, so a few objects each placing hundreds of components multiplied
at every level up to the depth cap (300 components, 3 levels: about 1 s;
deeper never finished). That was already true for one model part and now
reaches across parts. Every placement now costs at least one unit, so
such a file stops at the budget.
Test: six levels of 300 components over an empty object (300^6
placements) stop with a budget error in well under a second; without
the change the test does not finish.
Suggested in review by @jyviko.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Tyler Chiriboga <18270301+Tyler-Chiriboga@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
The control port opens before the first frame, so a single ui.inspect sometimes returned no viewport and the check failed within seconds. The probe now polls ui.inspect until the viewport exists, for up to 60 s, and fails with the last reply if it never does.
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Trunk formats the content hash without leading zeros, so it is sometimes 15 hex characters. The immutable-cache rule and the smoke test's wasm match now accept 8 to 16.
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
The kernel will move from truck to solidcore (#1597). The tests that must
catch regressions across that switch are the ones that don't depend on the
kernel: the reference tests and the robustness goldens. cargo-mutants checks
whether they would notice a change in the kernel's behaviour.
- apps/solidcraft-cli/tests/oracle_cases.rs: the reference tests as a cargo
test (ignored; CI keeps replaying them through cargo xtask oracle), run
over one process per core.
- .cargo/mutants.toml: mutants are judged by that test and the pull-request
sample of the robustness goldens, not by the kernel's own unit tests.
- A mutants profile (ci with incremental builds): about 15 s to rebuild after
a kernel change instead of about 3 minutes.
- docs/mutation-testing.md: how to run it, its cost, two cargo-mutants 27.1
problems and the pilot's results (15% of sampled kernel mutants caught).
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Joints: a face or circle origin keeps its turn as the face tilts
A face- or circle-centre joint origin worked out its frame's x from the normal alone
(`Vec3::any_perp`), which switches axis where the normal crosses 0.9 along X: a part on a face
tilted past about 64° by a parameter spun a right angle about the face. New origins keep the x
they have when picked (`x`, component coordinates) and each solve lays it onto the face as it is
now, so the part tilts with the face without spinning. Origins saved without it are placed as
before.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Joints: origins on an edge of a face, at its middle or a corner
`joint.origin` and `joint.create` take `edge: [x,y,z]` (a point on an edge of a planar face),
with `on` (which face; default the larger planar face beside it) and `corner` (the end near that
point). The origin sits at the edge's middle (z out of the face, y into it) or at the corner (x
and y along its two edges into the face, right-handed whichever way the edges run). The edge, the
face and the corner's other edge are kept by name and found again on every solve, so a part set
40 mm in from a panel's corner stays there when the panel changes size; a name that finds nothing
leaves the joint out with the reason.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
The trusted runners' guard refuses issue and pull_request_target events by design (they carry a
write token and handle untrusted input), so the bots failed there. They go back to GitHub's runners.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
While a sketch is open its grid is drawn on the sketch's own plane, centred
near the view target and snapped to the major step, whatever the Grid switch,
the sketch's Sketch Grid option or Grid follows the view say. Headless window
pictures follow. Outside a sketch nothing changes.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
On a busy CI machine (several fork jobs under gVisor at once) a web
offline launch took over 90 s and the icon review server took over ~2 s
to answer. Launches now get 240 s and one retry; the review server gets
up to 60 s to start.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 00:11:14 +00:00
1953 changed files with 548049 additions and 25912 deletions
What you clicked or typed, in order. For a problem on a particular design, `solidcraft-cli script design.solidcraft --out repro.json` turns its history into a script that `solidcraft-cli eval repro.json` replays; attach it, trimmed to the failing step if you can.
Using an AI agent with SolidCraft's MCP server? The `solidcraft-reporting` skill in [`skills/`](https://github.com/tangency-labs/solidcraft/tree/main/skills) has it reproduce the problem in a fresh design and write these steps for you.
# PRs test what they touch; main and nightly test everything. On a pull request the "Select
# checks" job maps the changed files to workspace packages (scripts/ci/affected.py), adds the
# packages that depend on them, and Clippy, the test archive and the test shards cover only
# those. Changes to the lockfile, root manifest, toolchain/cargo/nextest config, .github/,
# vendor/, tests/, scripts/ or any file no package owns select the whole workspace, as do
# pushes to main, the nightly run and Run workflow.
# Tests are built once and run in 4 shards. A few heavy tests (see .config/nextest.toml) are
# those. Test data under tests/, vendored crates under vendor/ and root files a crate embeds
# select the packages that read them. Changes to the lockfile, root manifest,
# toolchain/cargo/nextest config, this workflow, scripts/ or any file no package owns select the
# whole workspace, as do pushes to main that change code, the nightly run and Run workflow.
# Documentation, packaging, the other workflows and the GitHub bots' scripts select nothing:
# no native job runs for them, on pull requests or on main.
# Tests are built and run in one job on a big runner, slowest first, starting beside the repository
# checks (no archive to upload and download). A few heavy tests (see .config/nextest.toml) are
# skipped on pull requests and run on pushes to main and nightly. The Windows jobs (D3D12 shaders
# and app startup) run on pull requests only when what they test changes (the GPU crate, the app,
# the viewport's GPU glue, the graphics dependencies, their scripts or this file); main and
# nightly always run them. Only main saves the build caches; pull requests restore them.
# The merge queue runs the full suite on exactly the commit it then puts on main, so a push to
# main that a passing merge-queue run already tested runs only the repository checks; a push
# that skipped the queue still runs everything. Pull requests run the reference tests when the
# command line (or anything it depends on) changes, and doc tests for the affected packages.
# and app startup), the reference tests and the doc tests run on main and nightly only, not on
# pull requests (the fast lane, in Select checks); a break there is fixed forward on main. Only
# main saves the build caches; pull requests restore them. sccache is on a Blacksmith sticky disk that
# only main commits.
# A push to main runs only the repository checks when a passing merge-queue run tested that very
# commit, or when nothing a build reads changed since the last push to main whose run passed.
# Otherwise it runs everything.
# Heavy checks wait for review: a pull request without the `approved-for-ci` label runs only
# Select checks, the format and repository checks and a quick compile. Adding the label (after
# review) runs everything above, and every later push to the PR does too, except a push that only
# merges main into a change that already passed (scripts/ci/main_merge.py): it carries that
# result and runs the repository checks. Merge groups, the nightly run and Run workflow are not
# pull requests and always run the full suite.
# GitHub cannot filter `labeled` events by label name, so any other label starts a run whose jobs
# are all skipped; it gets its own concurrency group (below) so it never cancels the real run. Its
# `ci` job still runs and reports the commit's real result (see the `ci` job): the merge queue
# reads `ci` from the newest run of a commit, so a run without it would block a PR that passed.
# Cross-compilation is opt-in through Run workflow; release builds remain in release.yml.
# Trusted runs use both pools: pushes to main, merge-queue groups and pull requests from a branch
# of this repository (people with write access; fork pull requests never run on this machine).
# There, Select checks and ci run on the self-hosted light runner (`self-hosted-light`, quick
# jobs, so they never wait behind a build), and the repository checks, Clippy, the doc tests,
# the reference tests and all four test shards on the self-hosted runners (`self-hosted-fast`, warm
# build directories via scripts/ci/warm-target.sh). Fork PRs' Linux jobs run on the isolated, ephemeral
# `self-hosted-untrusted` runners (no warm caches) when the PR number ends in 7, 8 or 9 (the rest of
# the fork PRs, and everything else, run on GitHub's runners, which have far more free slots).
# The test archive is always built on GitHub's runners: the self-hosted machine has a newer
# glibc, so its binaries wouldn't start on ubuntu-24.04, while the archive runs there unchanged
# (same /home/runner/work checkout path). Fork pull requests, the nightly run and Run workflow
# use GitHub's runners only; the self-hosted runners refuse any other job.
# Every Linux job runs on Blacksmith's Ubuntu 24.04 runners (blacksmith-<N>vcpu-ubuntu-2404), sized
# per job: 32 vCPU for the tests, 16 for Clippy, the archive build (nightly) and the merge queue's, 8 for compiles, doc and reference tests, 4 for the light checks, 2 for Select checks and ci.
# The Windows shader job runs on Blacksmith's Windows Server 2025 runners (blacksmith-8vcpu-windows-2025);
# the Windows app startup smoke stays on windows-latest, which has a desktop session that presents frames.
# Pull requests, forks included, the merge queue, pushes to main and the nightly run all use them:
# the runners are ephemeral, fork runs get no secrets, and main saves the build caches that every
# other run restores read-only. Nightly and by hand, Build tests also makes an archive the matrix
# jobs run; Blacksmith's image matches ubuntu-24.04, so the archive and the recorded goldens behave the
# same.
name:CI
on:
# Draft pull requests run no CI (see `changes`, `checks` and `ci`): it starts when the PR is
# marked ready for review, and on every push after that.
# marked ready for review, and on every push after that. `labeled` is there for approved-for-ci
# only (other labels are skipped, see the top of this file).
if ! cargo nextest run --archive-file tests.tar.zst --workspace-remap . --extract-to . --extract-overwrite \
--profile ci --ignore-default-filter --partition "slice:$SHARD/$SHARDS" --no-tests=pass \
-E 'package(solidcraft-engine) & test(/^tests::robustness_goldens::/)'; then
echo "::error::A robustness golden changed. If the new result is intended, re-record it in this pull request and say why:"
echo "::error::ROBUSTNESS_GOLDENS_BLESS=\"why it changed\" cargo test --profile ci -p solidcraft-engine --lib robustness_goldens:: (see docs/robustness.md)"
if [ "$EVENT_NAME" != pull_request ]; then
# The boolean property tests draw new random inputs every run; they run only in the
# nightly property-tests job, so a known kernel bug cannot turn main red at random.
# Pull requests test the affected packages (scripts/ci/affected.py).
scope=(--workspace)
if [ "$FULL" != true ]; then scope=(); for p in $PACKAGES; do scope+=(-p "$p"); done; fi
status=0
cargo nextest run "${scope[@]}" --cargo-profile ci --locked --profile ci --no-tests=pass \
--ignore-default-filter -E "$base" || status=1
if [ "$status" != 0 ]; then
# A failed golden gets the hint to re-record it (junit.xml lists the failures).
if [ -f "$junit" ] && tr '\n' ' ' < "$junit" | grep -q '<testcase name="tests::robustness_goldens::[^"]*"[^>]*>[[:space:]]*<failure'; then
echo "::error::A robustness golden changed. If the new result is intended, re-record it in this pull request and say why:"
echo "::error::ROBUSTNESS_GOLDENS_BLESS=\"why it changed\" cargo test --profile ci -p solidcraft-engine --lib robustness_goldens:: (see docs/robustness.md)"
# An unapproved pull request: the heavy jobs are skipped on purpose.
if [ "$QUICK" = true ]; then
test "$QUICK_COMPILE" = success
fi
if [ "$NATIVE" = true ]; then
test "$CLIPPY" = success
test "$BUILD" = success
# The archive for the action matrix and property tests exists nightly and by hand only.
if [ "$EVENT_NAME" = schedule ] || [ "$EVENT_NAME" = workflow_dispatch ]; then
test "$BUILD" = success
fi
test "$TESTS" = success
fi
if [ "$DOCTESTS" = true ]; then
test "$DOCS" = success
fi
if [ "$ORACLE_RUN" = true ]; then
@@ -661,11 +846,17 @@ jobs:
test "$WINDOWS_FXC" = success
test "$WINDOWS_STARTUP" = success
fi
- name:Note a carried result
if:needs.changes.outputs.carried != ''
env:
CARRIED:${{ needs.changes.outputs.carried }}
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"
@@ -14,8 +14,33 @@ Editable drawing code, native projects and shipped exports are collected under `
| File | Author | Source | Licence |
|---|---|---|---|
| assets/render/catalog/capabilities.json | SolidCraft contributors | Byte-identical runtime export of docs/render-materials/capabilities.json; generated and checked by tools/render-library/generate.py | MIT OR Apache-2.0 |
| assets/render/catalog/environments.json | SolidCraft contributors | Byte-identical runtime export of docs/render-materials/environments.json; generated and checked by tools/render-library/generate.py | MIT OR Apache-2.0 |
| assets/render/catalog/lighting.json | SolidCraft contributors | Byte-identical runtime export of docs/render-materials/lighting.json; generated and checked by tools/render-library/generate.py | MIT OR Apache-2.0 |
| assets/render/catalog/materials.json | SolidCraft contributors | Byte-identical runtime export of docs/render-materials/materials.json; generated and checked by tools/render-library/generate.py | MIT OR Apache-2.0 |
| assets/render/catalog/quality-presets.json | SolidCraft contributors | Byte-identical runtime export of docs/render-materials/quality-presets.json; generated and checked by tools/render-library/generate.py | MIT OR Apache-2.0 |
| assets/render/catalog/scene-presets.json | SolidCraft contributors | Byte-identical runtime export of docs/render-materials/scene-presets.json; generated and checked by tools/render-library/generate.py | MIT OR Apache-2.0 |
| examples/chebyshev-motion-trace.json | SolidCraft contributors | Original parametric Chebyshev lambda mechanism and associative-trace demo, constructed from analytic circle intersections; classical link proportions documented at https://doi.org/10.1002/rcs.70095; no external CAD geometry or artwork | MIT OR Apache-2.0 |
| examples/material-card.json | SolidCraft contributors | original analytic box and explicitly synthetic engineering card; invented illustrative constants, no external material dataset | MIT OR Apache-2.0 |
| examples/studio/author_sweep.py | SolidCraft contributors | original editable CAD scene and authoring metadata; no external assets | MIT OR Apache-2.0 |
| examples/studio/author_scene.py | SolidCraft contributors | original editable CAD scene and authoring metadata; no external assets | MIT OR Apache-2.0 |
| examples/studio/materials.json | SolidCraft contributors | original editable CAD scene and authoring metadata; no external assets | MIT OR Apache-2.0 |
| examples/studio/coffee-exploded-sweep.json | SolidCraft contributors | original editable CAD scene and authoring metadata; no external assets | MIT OR Apache-2.0 |
| examples/studio/coffee-exploded-render.json | SolidCraft contributors | original editable CAD scene and authoring metadata; no external assets | MIT OR Apache-2.0 |
| examples/studio/coffee-exploded.json | SolidCraft contributors | original editable CAD scene and authoring metadata; no external assets | MIT OR Apache-2.0 |
| examples/studio/stage1-sweep.json | SolidCraft contributors | original tangent circular-arc CAD cyclorama; no external assets | MIT OR Apache-2.0 |
| examples/studio/stage1.json | SolidCraft contributors | original editable CAD studio scene; no imported geometry or external image assets | MIT OR Apache-2.0 |
| examples/studio/stage1-render.json | SolidCraft contributors | original studio camera and lighting parameters | MIT OR Apache-2.0 |
| examples/render-studio.json | SolidCraft contributors | original procedural box/cylinder enclosure script for the integrated Render workspace | MIT OR Apache-2.0 |
| examples/render-opaque-studio.json | SolidCraft contributors | original procedural sphere study varying opaque metal fraction and roughness independently; no external assets or engineering data | MIT OR Apache-2.0 |
| crates/gpu/src/path/denoise.wgsl | SolidCraft contributors | original bounded edge-aware a-trous spatial filter; GPU2 donor d710e31, no external shader source | MIT OR Apache-2.0 |
| crates/gpu/src/path/denoise_guides.wgsl | SolidCraft contributors | original deterministic centre-ray material and geometry guide pass; GPU2 donor d710e31, no external shader source | MIT OR Apache-2.0 |
| crates/gpu/src/path/shadows.wgsl | SolidCraft contributors | original bounded area, sun and environment visibility transport; GPU2 donor d710e31, no external shader source | MIT OR Apache-2.0 |
| crates/gpu/src/path/textures.wgsl | SolidCraft contributors | original planar colour and normal map sampling; GPU2 donor d710e31, no external shader source | MIT OR Apache-2.0 |
| assets/appearances/starter_pack.rs | SolidCraft contributors | original illustrative optical recipes, authored scalar values and future procedural resource descriptors; no measured or external material dataset | MIT OR Apache-2.0 |
| examples/material-finishes.json | SolidCraft contributors | original procedural sphere study of six illustrative opaque finishes with generic material densities; no external assets or measured optical data | MIT OR Apache-2.0 |
| examples/render-path-studio.json | SolidCraft contributors | original procedural opaque enclosure, teal insert and metal fasteners for progressive rendering; no imported models | MIT OR Apache-2.0 |
| crates/gpu/src/path/path.wgsl | SolidCraft contributors | original software BVH traversal and path integration using the existing SolidCraft surface/studio equations; no external shader source | MIT OR Apache-2.0 |
| crates/gpu/src/pbr.rs | SolidCraft contributors | original WGSL translation of SolidCraft's opaque surface and fixed studio reference, embedded as Rust source; no external shader code | MIT OR Apache-2.0 |
| assets/icons/app/macos/source/SolidCraft.icon/icon.json | SolidCraft contributors | approved Icon Composer composition and appearance settings for the original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/app/macos/source/SolidCraft.icon/Assets/02-grid.svg | SolidCraft contributors | original perspective grid with elliptical radial alpha, exported from VectorCraft | MIT OR Apache-2.0 |
@@ -24,14 +49,23 @@ Editable drawing code, native projects and shipped exports are collected under `
| assets/icons/app/macos/exports/Assets.car | SolidCraft contributors | Apple actool compilation of assets/icons/app/macos/source/SolidCraft.icon for macOS; contains original artwork only | MIT OR Apache-2.0 |
| assets/archive/icons/macos-v1/source/icon.vectorcraft | SolidCraft contributors | retained editable historical VectorCraft source, with named face and background layers; redundant exports retired | MIT OR Apache-2.0 |
| assets/icons/app/macos/exports/icon.icns | SolidCraft contributors | macOS fallback compiled by Apple's actool from the approved assets/icons/app/macos/source/SolidCraft.icon composition | MIT OR Apache-2.0 |
| docs/screenshots/class-a-blend.png | SolidCraft contributors | screenshot of SolidCraft itself: two boxes bridged by `surface.blend` (a 40x10 sheet, `solidcraft-cli snapshot` of plan/evidence script ev_blend) | MIT OR Apache-2.0 |
| docs/screenshots/class-a-crown.png | SolidCraft contributors | screenshot of SolidCraft itself: a patch crowned with `surface.patch crown=0.3` over a saddle rim (`solidcraft-cli snapshot`) | MIT OR Apache-2.0 |
| docs/screenshots/class-a-boundary.png | SolidCraft contributors | screenshot of SolidCraft itself: a patch through two sketch lines built by `surface.boundary` with `crown: 0.4` (`solidcraft-cli snapshot`) | MIT OR Apache-2.0 |
| docs/screenshots/sample-plate.png | SolidCraft contributors | screenshot of SolidCraft itself (`solidcraft --sample`) | MIT OR Apache-2.0 |
| docs/screenshots/demo-0.8.gif | SolidCraft contributors | original screen recording of the actual SolidCraft 0.8 English desktop UI modelling an original Raspberry Pi 4 case (no logos or branding; the engraved text is set in the system's DejaVu Sans Bold), generated with examples/demo/record_ui.sh and its original animated pointer overlay (9× playback) | MIT OR Apache-2.0 |
| docs/screenshots/demo-0.8.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.8 English desktop UI modelling an original Raspberry Pi 4 case (no logos or branding; the engraved text is set in the system's DejaVu Sans Bold), generated with examples/demo/record_ui.sh and its original animated pointer overlay (9× playback) | MIT OR Apache-2.0 |
| docs/screenshots/rover-build.gif | 0xlillith (model); SolidCraft contributors (recording) | screen recording of the actual SolidCraft desktop UI replaying the timeline of the SELENE rover (`Rover-recorded.solidcraft` from https://github.com/0xlillith/solidcraft-rover, an original model by 0xlillith built through SolidCraft MCP); the model's lettering follows Ubuntu Light outlines (Canonical Ltd., Ubuntu Font Licence 1.0, as noted in that repository); the emblem is the SolidCraft project icon (2× playback) | MIT OR Apache-2.0 |
| docs/screenshots/rover-build.mp4 | 0xlillith (model); SolidCraft contributors (recording) | screen recording of the actual SolidCraft desktop UI replaying the timeline of the SELENE rover (`Rover-recorded.solidcraft` from https://github.com/0xlillith/solidcraft-rover, an original model by 0xlillith built through SolidCraft MCP); the model's lettering follows Ubuntu Light outlines (Canonical Ltd., Ubuntu Font Licence 1.0, as noted in that repository); the emblem is the SolidCraft project icon | MIT OR Apache-2.0 |
| assets/models/test_boat.bin | 3DBenchy.com | 3DBenchy.com | Public Domain |
| assets/models/make_test_boat.py | SolidCraft contributors | original script that packs a model into the compact file above | MIT OR Apache-2.0 |
| docs/screenshots/class-a-blend.png | SolidCraft contributors | screenshot of SolidCraft itself: a 24 mm and a 10 mm box bridged by a `surface.blend` sheet (G1 at both edges), rendered by the app's `ui.render` in the dark theme with examples/demo/class_a_shots.py | MIT OR Apache-2.0 |
| docs/screenshots/class-a-crown.png | SolidCraft contributors | screenshot of SolidCraft itself: a patch crowned with `surface.patch crown=0.3` over a skew four-line rim, rendered by the app's `ui.render` in the dark theme with examples/demo/class_a_shots.py | MIT OR Apache-2.0 |
| docs/screenshots/class-a-boundary.png | SolidCraft contributors | screenshot of SolidCraft itself: a surface through two 3D sketch splines built by `surface.boundary`, rendered by the app's `ui.render` in the dark theme with examples/demo/class_a_shots.py | MIT OR Apache-2.0 |
| docs/screenshots/sample-plate.png | SolidCraft contributors | screenshot of SolidCraft 0.22.1 itself (`solidcraft --sample`, default dark theme, 1600x1000) | MIT OR Apache-2.0 |
| assets/samples/exports/0.png | SolidCraft contributors | original Sample Plate from `crates/engine/src/sample.rs`, exported by the SolidCraft thumbnail renderer | MIT OR Apache-2.0 |
| assets/samples/exports/1.png | SolidCraft contributors | original Sheet Metal Bracket from `crates/engine/src/sample.rs`, exported by the SolidCraft thumbnail renderer | MIT OR Apache-2.0 |
| assets/samples/exports/2.png | SolidCraft contributors | original Hinge Assembly from `crates/engine/src/sample.rs`, exported by the SolidCraft thumbnail renderer | MIT OR Apache-2.0 |
| assets/samples/exports/3.png | SolidCraft contributors | original Plastic Enclosure from `crates/engine/src/sample.rs`, exported by the SolidCraft thumbnail renderer | MIT OR Apache-2.0 |
| assets/samples/exports/4.png | SolidCraft contributors | original Bolted Flange from `crates/engine/src/sample.rs`, exported by the SolidCraft thumbnail renderer | MIT OR Apache-2.0 |
| assets/samples/exports/5.png | SolidCraft contributors | original Vent Plate from `crates/engine/src/sample.rs`, exported by the SolidCraft thumbnail renderer | MIT OR Apache-2.0 |
| assets/samples/exports/scripts.json | SolidCraft contributors | SHA-256 fingerprints of the original sample scripts, generated with the shipped thumbnails | MIT OR Apache-2.0 |
| docs/screenshots/demo.gif | SolidCraft contributors | original screen recording of the actual SolidCraft 0.22.1 English desktop UI (default dark theme, software renderer) modelling an original Raspberry Pi 4 case (no logos or branding; the engraved text is set in the system's DejaVu Sans Bold), generated with examples/demo/record_ui.sh and its original animated pointer overlay (6× playback, idle pauses cut) | MIT OR Apache-2.0 |
| docs/screenshots/demo.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.22.1 English desktop UI (default dark theme, software renderer) modelling an original Raspberry Pi 4 case (no logos or branding; the engraved text is set in the system's DejaVu Sans Bold), generated with examples/demo/record_ui.sh and its original animated pointer overlay (6× playback, idle pauses cut) | MIT OR Apache-2.0 |
| docs/screenshots/rover-build.gif | 0xlillith (model); SolidCraft contributors (recording) | screen recording of the actual SolidCraft 0.22.1 desktop UI (default dark theme, software renderer) replaying the timeline of the SELENE rover (`Rover-recorded.solidcraft` from https://github.com/0xlillith/solidcraft-rover, an original model by 0xlillith built through SolidCraft MCP); the model's lettering follows Ubuntu Light outlines (Canonical Ltd., Ubuntu Font Licence 1.0, as noted in that repository); the emblem is the SolidCraft project icon (15× playback, idle pauses cut, constant-rate camera orbit) | MIT OR Apache-2.0 |
| docs/screenshots/rover-build.mp4 | 0xlillith (model); SolidCraft contributors (recording) | screen recording of the actual SolidCraft 0.22.1 desktop UI (default dark theme, software renderer) replaying the timeline of the SELENE rover (`Rover-recorded.solidcraft` from https://github.com/0xlillith/solidcraft-rover, an original model by 0xlillith built through SolidCraft MCP); the model's lettering follows Ubuntu Light outlines (Canonical Ltd., Ubuntu Font Licence 1.0, as noted in that repository); the emblem is the SolidCraft project icon | MIT OR Apache-2.0 |
| docs/design/ux-redesign/model-floating.png | SolidCraft contributors | original UX mockup (Model tab, floating layout) rendered from an HTML mockup that uses the original Technical Accent icon set; no third-party artwork, and not a screenshot of any other CAD product | MIT OR Apache-2.0 |
| docs/design/ux-redesign/model-compact.png | SolidCraft contributors | original UX mockup (Model tab, compact docked layout) rendered from an HTML mockup that uses the original Technical Accent icon set; no third-party artwork, and not a screenshot of any other CAD product | MIT OR Apache-2.0 |
| docs/design/ux-redesign/sketch.png | SolidCraft contributors | original UX mockup (Sketch tab) rendered from an HTML mockup that uses the original Technical Accent icon set; no third-party artwork, and not a screenshot of any other CAD product | MIT OR Apache-2.0 |
@@ -45,8 +79,29 @@ Editable drawing code, native projects and shipped exports are collected under `
| docs/book/src/generated/enclosure.png | SolidCraft contributors | rendered by `cargo xtask book` (`solidcraft-cli snapshot docs/book/scripts/enclosure.json`; gitignored) | MIT OR Apache-2.0 |
| docs/book/src/generated/revolve.png | SolidCraft contributors | rendered by `cargo xtask book` (`solidcraft-cli snapshot docs/book/scripts/revolve.json`; gitignored) | MIT OR Apache-2.0 |
| docs/book/src/generated/sheet-metal.png | SolidCraft contributors | rendered by `cargo xtask book` (`solidcraft-cli snapshot docs/book/scripts/sheet-metal.json`; gitignored) | MIT OR Apache-2.0 |
| assets/fonts/ibm-plex-mono/IBMPlexMono-Regular.ttf | IBM Corp. (IBM Plex Mono) | https://github.com/IBM/plex (via google/fonts), unmodified; licence text in assets/fonts/ibm-plex-mono/OFL.txt; the interface monospace font | SIL OFL 1.1 |
| assets/fonts/ibm-plex-sans/IBMPlexSans-Variable.ttf | IBM Corp. (IBM Plex Sans, variable wdth and wght) | https://github.com/IBM/plex (via google/fonts), unmodified; licence text in assets/fonts/ibm-plex-sans/OFL.txt; the default interface font | SIL OFL 1.1 |
| docs/book/src/generated/tool-preview.png | SolidCraft contributors | rendered by `cargo xtask book` (`solidcraft-cli snapshot docs/book/scripts/tool-preview.json`; gitignored) | MIT OR Apache-2.0 |
| assets/fonts/liberation-sans/LiberationSans-Regular.ttf | Red Hat, Inc. (Liberation Fonts 2.x; digitized data Google) | https://github.com/liberationfonts (Debian fonts-liberation), unmodified; licence text in assets/fonts/liberation-sans/OFL.txt; used for sketch Text outlines | SIL OFL 1.1 |
| assets/fonts/biz-udp-gothic/BIZUDPGothic-Regular.ttf | The BIZ UDGothic Project Authors | https://github.com/googlefonts/morisawa-biz-ud-gothic (Google Fonts), unmodified; licence text in assets/fonts/biz-udp-gothic/OFL.txt; used for web UI Japanese font | SIL OFL 1.1 |
| tests/step-conformance/cone_point.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
| tests/step-conformance/cone_point_vertex.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
| tests/step-conformance/cylinder.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
| tests/step-conformance/cylinder_inch.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
| tests/step-conformance/cylinder_nurbs.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
| tests/step-conformance/cylinder_placed.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
| tests/step-conformance/cylinder_seam.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
| tests/step-conformance/cylinder_twice.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
| tests/step-conformance/dome.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
| tests/step-conformance/dome_nurbs.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
| tests/step-conformance/far_cylinder.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
| tests/step-conformance/grooved_shaft.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
| tests/step-conformance/plate_100_holes.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
| tests/step-conformance/sheet_disc.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
| tests/step-conformance/tiny_pin.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
| tests/step-conformance/torus_ring_nurbs.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
| tests/step-conformance/washer.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
| tests/step-regressions/rod_doubled_edge.step | SolidCraft contributors | written by SolidCraft from the commands of #4598 (a bored rod moved 100 mm and exported); import regression fixture | MIT OR Apache-2.0 |
| tests/oracle/17-loft/reference.stl | SolidCraft contributors | triangulation of our own recipe (tests/oracle/17-loft/recipe.json) built with FreeCAD as an external tool by tools/freecad-oracle | MIT OR Apache-2.0 |
| tests/oracle/23-bracket-multifeature/reference.stl | SolidCraft contributors | triangulation of our own recipe (tests/oracle/23-bracket-multifeature/recipe.json) built with FreeCAD as an external tool by tools/freecad-oracle | MIT OR Apache-2.0 |
| tests/oracle/28-blind-pocket-fillets/reference.stl | SolidCraft contributors | triangulation of our own recipe (tests/oracle/28-blind-pocket-fillets/recipe.json) built with FreeCAD as an external tool by tools/freecad-oracle | MIT OR Apache-2.0 |
@@ -67,9 +122,11 @@ Editable drawing code, native projects and shipped exports are collected under `
| tests/oracle/61-shaft-keyway/reference.stl | SolidCraft contributors | triangulation of our own recipe (tests/oracle/61-shaft-keyway/recipe.json) built with FreeCAD as an external tool by tools/freecad-oracle | MIT OR Apache-2.0 |
| tests/oracle/63-hinge-knuckle/reference.stl | SolidCraft contributors | triangulation of our own recipe (tests/oracle/63-hinge-knuckle/recipe.json) built with FreeCAD as an external tool by tools/freecad-oracle | MIT OR Apache-2.0 |
| tests/oracle/65-sketch-gear-tooth/reference.stl | SolidCraft contributors | triangulation of our own recipe (tests/oracle/65-sketch-gear-tooth/recipe.json) built with FreeCAD as an external tool by tools/freecad-oracle | MIT OR Apache-2.0 |
| assets/appearances/library.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/appearances/opaque_finishes.rs | SolidCraft contributors | original illustrative opaque finish values; no external dataset, textures or measured engineering data | MIT OR Apache-2.0 |
| assets/fonts/biz-udp-gothic/OFL.txt | The BIZ UDGothic Project Authors | Unmodified accompanying font licence | SIL OFL 1.1 |
| assets/fonts/ibm-plex-mono/OFL.txt | IBM Corp. | Unmodified accompanying font licence | SIL OFL 1.1 |
| assets/fonts/ibm-plex-sans/OFL.txt | IBM Corp. | Unmodified accompanying font licence | SIL OFL 1.1 |
| assets/fonts/liberation-sans/OFL.txt | Red Hat, Inc.; Liberation Fonts contributors | Unmodified accompanying font licence | SIL OFL 1.1 |
| assets/icons/app/linux/exports/hicolor/128x128/apps/ai.storyteller.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
| assets/icons/app/linux/exports/hicolor/16x16/apps/ai.storyteller.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
@@ -79,7 +136,7 @@ Editable drawing code, native projects and shipped exports are collected under `
| assets/icons/app/linux/exports/hicolor/48x48/apps/ai.storyteller.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
| assets/icons/app/linux/exports/hicolor/512x512/apps/ai.storyteller.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
| assets/icons/app/linux/exports/hicolor/64x64/apps/ai.storyteller.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
| assets/icons/app/shared/source/app_icon.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/icons/app/shared/source/app_icon.rs | SolidCraft contributors | Original SolidCraft drawing code: the borderless mark, drawn from the faces and gradients of assets/icons/app/macos/source/SolidCraft.icon/Assets/03-solid-blue-cutout.svg | MIT OR Apache-2.0 |
| assets/icons/app/shared/source/windows_ico.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/icons/app/windows/exports/solidcraft-executable.ico | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
| assets/icons/app/windows/exports/solidcraft.ico | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
@@ -88,20 +145,31 @@ Editable drawing code, native projects and shipped exports are collected under `
| assets/icons/ui/source/navigation.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/icons/ui/source/pointer.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/locales/cs.json | potuzai | Czech translation of the English catalog, contributed upstream | MIT OR Apache-2.0 |
| assets/locales/da.json | SolidCraft contributors | Original Danish translation of SolidCraft's English catalog; no commercial CAD text used | MIT OR Apache-2.0 |
| assets/locales/de.json | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/locales/en-GB.json | SolidCraft contributors | English (UK): the English catalog's messages whose spelling differs, in UK spelling | MIT OR Apache-2.0 |
| assets/locales/en.json | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/locales/es.json | jawler | Spanish (Spain) translation of the English catalog, contributed upstream | MIT OR Apache-2.0 |
| assets/locales/fr.json | rlartot | French translation of the English catalog, contributed upstream | MIT OR Apache-2.0 |
| assets/locales/gl.json | Pablo López (pablopeza) | Galician translation of the English catalog, contributed upstream | MIT OR Apache-2.0 |
| assets/locales/hu.json | SolidCraft contributors | Original Hungarian translation of SolidCraft's English catalog; no commercial CAD text used | MIT OR Apache-2.0 |
| assets/locales/id.json | SolidCraft contributors | Original Indonesian translation of SolidCraft's English catalog; no commercial CAD text used | MIT OR Apache-2.0 |
| assets/locales/it.json | SolidCraft contributors | Original Italian translation of SolidCraft's English catalog; no commercial CAD text used | MIT OR Apache-2.0 |
| assets/locales/ja.json | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/locales/ko.json | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/locales/ms.json | A.Hakim (a-hakim) | Malay translation of the English message catalog | MIT OR Apache-2.0 |
| assets/locales/nb.json | SolidCraft contributors | Original Norwegian Bokmål translation of SolidCraft's English catalog; no commercial CAD text used | MIT OR Apache-2.0 |
| assets/locales/nl.json | SolidCraft contributors | Original Dutch translation of SolidCraft's English catalog; no commercial CAD text used | MIT OR Apache-2.0 |
| assets/locales/pl.json | cyryllo | Polish translation of the English catalog, contributed upstream | MIT OR Apache-2.0 |
| assets/locales/ru.json | Alex Pro (del000) | Russian translation of the English catalog, contributed upstream | MIT OR Apache-2.0 |
| assets/locales/sv.json | SolidCraft contributors | Original Swedish translation of SolidCraft's English catalog; no commercial CAD text used | MIT OR Apache-2.0 |
| assets/locales/pt-BR.json | DuBearte | Brazilian Portuguese translation of the English catalog, contributed upstream | MIT OR Apache-2.0 |
| assets/locales/ro.json | SolidCraft contributors | Original Romanian translation of SolidCraft's English catalog; no commercial CAD text used | MIT OR Apache-2.0 |
| assets/locales/tr.json | macrulz54 | Turkish translation of the English catalog, contributed upstream | MIT OR Apache-2.0 |
| assets/locales/uk.json | Igor Bronovskyi (BrunIF) | Ukrainian translation of the English catalog, contributed upstream | MIT OR Apache-2.0 |
| assets/locales/vi.json | vandang890615 | Vietnamese translation of the English catalog, contributed upstream | MIT OR Apache-2.0 |
| assets/locales/zh.json | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/locales/zh-TW.json | SolidCraft contributors | Original Traditional Chinese (Taiwan) translation of SolidCraft's English catalog; no commercial CAD text used | MIT OR Apache-2.0 |
| assets/materials/library.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
@@ -118,6 +186,22 @@ Editable drawing code, native projects and shipped exports are collected under `
| assets/styles/ui/themes/gruvbox-light.json | SolidCraft contributors | original SolidCraft theme file mapping SolidCraft's UI colours to the Gruvbox palette's colour values (Gruvbox by Pavel Pertsev, https://github.com/morhetz/gruvbox, MIT) | MIT OR Apache-2.0 |
| assets/styles/ui/themes/gruvbox-light-hard.json | SolidCraft contributors | original SolidCraft theme file mapping SolidCraft's UI colours to the Gruvbox palette's colour values (Gruvbox by Pavel Pertsev, https://github.com/morhetz/gruvbox, MIT) | MIT OR Apache-2.0 |
| assets/styles/ui/themes/gruvbox-light-soft.json | SolidCraft contributors | original SolidCraft theme file mapping SolidCraft's UI colours to the Gruvbox palette's colour values (Gruvbox by Pavel Pertsev, https://github.com/morhetz/gruvbox, MIT) | MIT OR Apache-2.0 |
| assets/styles/ui/themes/solidcraft-95.json | SolidCraft contributors | original SolidCraft theme file | MIT OR Apache-2.0 |
| assets/styles/ui/themes/platinum-9.json | SolidCraft contributors | original SolidCraft theme file | MIT OR Apache-2.0 |
| assets/styles/ui/themes/cupertino.json | SolidCraft contributors | original SolidCraft theme file | MIT OR Apache-2.0 |
| assets/styles/ui/themes/green-phosphor.json | SolidCraft contributors | original SolidCraft theme file | MIT OR Apache-2.0 |
| assets/styles/ui/themes/amber-phosphor.json | SolidCraft contributors | original SolidCraft theme file | MIT OR Apache-2.0 |
| assets/styles/ui/themes/blueprint.json | SolidCraft contributors | original SolidCraft theme file | MIT OR Apache-2.0 |
| assets/styles/ui/themes/one-dark.json | SolidCraft contributors | original SolidCraft theme file with colours adapted from the One Dark palette (Atom One Dark by GitHub Atom, https://github.com/atom/atom, MIT) | MIT OR Apache-2.0 |
| assets/styles/ui/themes/tokyo-night.json | SolidCraft contributors | original SolidCraft theme file adapting the colours of a published palette (Tokyo Night by enkia, https://github.com/enkia/tokyo-night-vscode-theme, MIT) | MIT OR Apache-2.0 |
| assets/styles/ui/themes/solarized-light.json | SolidCraft contributors | original SolidCraft theme file adapting the colours of a published palette (Solarized by Ethan Schoonover, https://github.com/altercation/solarized, MIT) | MIT OR Apache-2.0 |
| assets/styles/ui/themes/nord.json | SolidCraft contributors | original SolidCraft theme file adapting the colours of a published palette (Nord by Sven Greb and the Nord contributors, https://github.com/nordtheme/nord, MIT) | MIT OR Apache-2.0 |
| assets/styles/ui/themes/high-contrast.json | SolidCraft contributors | original SolidCraft theme file | MIT OR Apache-2.0 |
| assets/styles/ui/themes/graphite.json | SolidCraft contributors | original SolidCraft theme file | MIT OR Apache-2.0 |
| assets/styles/ui/themes/nosferatu.json | SolidCraft contributors | original SolidCraft theme file adapting the colours of a published palette (Dracula by Zeno Rocha and the Dracula Theme contributors, https://github.com/dracula/dracula-theme, MIT) | MIT OR Apache-2.0 |
| assets/styles/ui/themes/catppuccin-mocha.json | SolidCraft contributors | original SolidCraft theme file adapting the colours of a published palette (Catppuccin by the Catppuccin organisation, https://github.com/catppuccin/catppuccin, MIT) | MIT OR Apache-2.0 |
| assets/styles/ui/themes/okibi.json | SolidCraft contributors (Kai Evans) | original SolidCraft theme file mapping SolidCraft's UI colours to the Okibi palette (dark) of the Okibi design system by Kai Evans (https://kaievns.github.io/okibi-akiri-design-system-spec/) | MIT OR Apache-2.0 |
| assets/styles/ui/themes/akari.json | SolidCraft contributors (Kai Evans) | original SolidCraft theme file mapping SolidCraft's UI colours to the Akari palette (light) of the Okibi design system by Kai Evans (https://kaievns.github.io/okibi-akiri-design-system-spec/) | MIT OR Apache-2.0 |
| assets/styles/web/loading-logo.svg | SolidCraft contributors | Animation of the SolidCraft mark, built from the paths and gradients of assets/icons/app/macos/source/SolidCraft.icon/Assets/03-solid-blue-cutout.svg; original animation | MIT OR Apache-2.0 |
| assets/styles/web/loading.css | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| crates/io/src/native_cad/fixtures/block.FCStd | SolidCraft contributors | original 10 × 20 × 30 mm test box saved by FreeCAD 1.1.4 run as an external program (`make_block_fcstd.py` beside it); no third-party geometry | MIT OR Apache-2.0 |
@@ -126,6 +210,10 @@ Editable drawing code, native projects and shipped exports are collected under `
| assets/icons/ui/design/palette.toml | SolidCraft contributors | icon colour roles (light and dark) and accents, original SolidCraft data | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/bug_report.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/chevron.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/chevron_right.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/close.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/disclosure_closed.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/disclosure_open.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/fit.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/folder.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/help.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
@@ -134,11 +222,17 @@ Editable drawing code, native projects and shipped exports are collected under `
| assets/icons/ui/design/svg/flat/moon.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/nav_settings.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/orbit.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/plus.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/pan.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/perspective.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/redo.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/save.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/settings.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/settings_keyboard.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/settings_monitor.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/settings_mouse.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/settings_palette.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/settings_plug.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/spin.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/style.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/undo.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
@@ -219,7 +313,7 @@ Editable drawing code, native projects and shipped exports are collected under `
| assets/icons/ui/design/svg/model/export.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/extend.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/extrude.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/eye.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/eye.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/fastener.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/fillet.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/filter.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
@@ -269,6 +363,18 @@ Editable drawing code, native projects and shipped exports are collected under `
| assets/icons/ui/design/svg/model/pattern_rect.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/perforate.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/pipe.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/silhouette.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/assistant.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/agent_link.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/offset_two_sides.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/offset_symmetric.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/import_tray.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/export_tray.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/send.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/stop.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/shield.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/status_recovery.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/frame_member.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/placeholder.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/plane.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/plane_angle.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
@@ -320,7 +426,7 @@ Editable drawing code, native projects and shipped exports are collected under `
| assets/icons/ui/design/svg/model/status_error.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/status_warning.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/stitch.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/suppress.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/suppress.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/surface.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/svg.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/sweep.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
@@ -404,10 +510,13 @@ Editable drawing code, native projects and shipped exports are collected under `
| assets/icons/ui/design/tools/lint_icons.py | SolidCraft contributors | icon lint, status page or preview tool (standard-library Python), original SolidCraft code | MIT OR Apache-2.0 |
| assets/icons/ui/design/tools/preview.py | SolidCraft contributors | icon lint, status page or preview tool (standard-library Python), original SolidCraft code | MIT OR Apache-2.0 |
| assets/icons/ui/design/tools/review_state.py | SolidCraft contributors | icon review state helper (standard-library Python), original SolidCraft code | MIT OR Apache-2.0 |
| assets/icons/ui/design/requested_icons.txt | SolidCraft contributors | Runtime icon requests collected by the headless menu audit, original SolidCraft data | MIT OR Apache-2.0 |
| assets/icons/ui/design/review.json | SolidCraft contributors | icon review state (accepted icons and notes), original SolidCraft data | MIT OR Apache-2.0 |
| assets/icons/ui/design/tools/author/lib.py | SolidCraft contributors | icon authoring script (standard-library Python), original SolidCraft code | MIT OR Apache-2.0 |
| docs/book/src/og.png | SolidCraft contributors | copy of assets/branding/web/source/og.png staged by cargo xtask book (gitignored) | MIT OR Apache-2.0 |
| vendor/truck-shapeops | truck-shapeops 0.4.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | robust meshing and nearest-parameter fallback in booleans; degenerate intersection-curve rejection; face and edge history of booleans; trace macro; corner choice at vertices several intersection curves end on (vendor/README.md) |
| vendor/truck-shapeops | truck-shapeops 0.4.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | robust meshing and nearest-parameter fallback in booleans; degenerate intersection-curve rejection; face and edge history of booleans; trace macro; corner choice at vertices several intersection curves end on; reuse of an operand's meshes (vendor/README.md) |
| vendor/truck-geometry | truck-geometry 0.5.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | intersection curves divided from their neighbours' surface parameters; surface second derivatives (vendor/README.md) |
@@ -485,3 +594,453 @@ A frame whose meshes the GPU refuses to stage (device lost or out of memory) is
skipped with a warning instead of panicking (#1469).
Paint callbacks are prepared only after a drawable is acquired, so a skipped
surface frame cannot strand cached meshes without their GPU initialization.
## Runtime studio lighting mathematics
`crates/gpu/src/path/path.wgsl` implements the power-heuristic estimator from the
mathematical description in [PBRT fourth edition, section 13.4](https://pbr-book.org/4ed/Light_Transport_I_Surface_Reflection/A_Better_Path_Tracer).
No PBRT code or assets were copied. `crates/render/src/job/lighting.rs` evaluates
[Krzysztof Narkowicz's published 2016 ACES rational fit](https://knarkowicz.wordpress.com/2016/01/06/aces-filmic-tone-mapping-curve/)
using its published coefficients (author licenses the fit under CC0/MIT).
These original implementations are under SolidCraft's MIT OR Apache-2.0 licence.
## Original coffee framing variant
| File | Author | Provenance | Licence |
|---|---|---|---|
| examples/studio/author_coffee_framing.py | SolidCraft contributors | original editable composition, camera and documentation; no external assets | MIT OR Apache-2.0 |
| examples/studio/coffee-framing.json | SolidCraft contributors | original editable composition, camera and documentation; no external assets | MIT OR Apache-2.0 |
| examples/studio/coffee-framing-sweep.json | SolidCraft contributors | original editable composition, camera and documentation; no external assets | MIT OR Apache-2.0 |
| examples/studio/coffee-framing-render.json | SolidCraft contributors | original editable composition, camera and documentation; no external assets | MIT OR Apache-2.0 |
| examples/studio/coffee-framing.md | SolidCraft contributors | original editable composition, camera and documentation; no external assets | MIT OR Apache-2.0 |
## Original cylindrical coffee housing variant
| File | Author | Provenance | Licence |
|---|---|---|---|
| examples/studio/author_coffee_cylinder.py | SolidCraft contributors | original parametric cylindrical CAD construction and editable command recipes; no external assets | MIT OR Apache-2.0 |
| examples/studio/coffee-cylinder.json | SolidCraft contributors | original cylindrical housing on the original exploded coffee composition; no external assets | MIT OR Apache-2.0 |
| examples/studio/coffee-cylinder-sweep.json | SolidCraft contributors | original cylindrical housing and continuous CAD cyclorama; no external assets | MIT OR Apache-2.0 |
| examples/studio/coffee-cylinder-render.json | SolidCraft contributors | original camera, studio lighting and optical settings retained from the framing variant | MIT OR Apache-2.0 |
| examples/studio/coffee-cylinder.md | SolidCraft contributors | original dimensional recipe, provenance and validation limitations | MIT OR Apache-2.0 |
## Runtime environment sampling
`crates/render/src/lighting/environment.rs` and the environment/sun portions of
`crates/gpu/src/path/path.wgsl` are original SolidCraft implementations of
solid-angle importance sampling and MIS, informed by the mathematical description
in [PBRT fourth edition, sections 12.5](https://pbr-book.org/4ed/Light_Sources/Infinite_Area_Lights)
and [13.4](https://pbr-book.org/4ed/Light_Transport_I_Surface_Reflection/A_Better_Path_Tracer).
The procedural gradient and inline RGBE test fixtures are original mathematical
colours, not downloaded imagery. No third-party environment images or shader code
are included. Licence: MIT OR Apache-2.0.
## Original tumbler showcase assets
The CAD room, tumblers, window opening, wood/weave patterns and RGBE sky below
are original SolidCraft contributor work. No third-party scene, photograph, HDRI,
texture, or AI-generated artwork was used. Python authoring sources are included.
All listed files use the repository MIT OR Apache-2.0 licence.
| File | Source / author | Use | License |
| --- | --- | --- | --- |
| examples/studio/author_showcase.py | Original SolidCraft contributors | Editable scene / runtime resource / reproducible authoring metadata | MIT OR Apache-2.0 |
| examples/studio/author_tumblers.py | Original SolidCraft contributors | Editable scene / runtime resource / reproducible authoring metadata | MIT OR Apache-2.0 |
| examples/studio/textures/ATTRIBUTION.md | Original SolidCraft contributors | Editable scene / runtime resource / reproducible authoring metadata | MIT OR Apache-2.0 |
| examples/studio/textures/author_textures.py | Original SolidCraft contributors | Editable scene / runtime resource / reproducible authoring metadata | MIT OR Apache-2.0 |
| examples/studio/textures/environment-manifest.json | Original SolidCraft contributors | Editable scene / runtime resource / reproducible authoring metadata | MIT OR Apache-2.0 |
| examples/studio/textures/manifest.json | Original SolidCraft contributors | Editable scene / runtime resource / reproducible authoring metadata | MIT OR Apache-2.0 |
| examples/studio/tumblers-hdr-textures-blocked-render.json | Original SolidCraft contributors | Editable scene / runtime resource / reproducible authoring metadata | MIT OR Apache-2.0 |
| examples/studio/tumblers-hdr-textures-render.json | Original SolidCraft contributors | Editable scene / runtime resource / reproducible authoring metadata | MIT OR Apache-2.0 |
| examples/studio/tumblers-materials.json | Original SolidCraft contributors | Editable scene / runtime resource / reproducible authoring metadata | MIT OR Apache-2.0 |
| examples/studio/tumblers-render.json | Original SolidCraft contributors | Editable scene / runtime resource / reproducible authoring metadata | MIT OR Apache-2.0 |
| examples/studio/tumblers-sun-render.json | Original SolidCraft contributors | Editable scene / runtime resource / reproducible authoring metadata | MIT OR Apache-2.0 |
| examples/studio/tumblers-sun-textures-blocked-render.json | Original SolidCraft contributors | Editable scene / runtime resource / reproducible authoring metadata | MIT OR Apache-2.0 |
| examples/studio/tumblers-sun-textures-render.json | Original SolidCraft contributors | Editable scene / runtime resource / reproducible authoring metadata | MIT OR Apache-2.0 |
| examples/studio/tumblers-sun.json | Original SolidCraft contributors | Editable scene / runtime resource / reproducible authoring metadata | MIT OR Apache-2.0 |
| examples/studio/tumblers-textures-blocked-render.json | Original SolidCraft contributors | Editable scene / runtime resource / reproducible authoring metadata | MIT OR Apache-2.0 |
| examples/studio/tumblers-textures-render.json | Original SolidCraft contributors | Editable scene / runtime resource / reproducible authoring metadata | MIT OR Apache-2.0 |
| examples/studio/tumblers.json | Original SolidCraft contributors | Editable scene / runtime resource / reproducible authoring metadata | MIT OR Apache-2.0 |
The original studio generators produce local ignored PNG maps and an RGBE gradient.
Generated image bytes are not source assets; their authorship, licences and expected
hashes are documented under `examples/studio/textures`. Rendered evidence belongs
on the results branch. No photographs, scans or external image assets are included.
## Additional render and showcase resources
| File | Author | Source | Licence |
|---|---|---|---|
| assets/render/starter/tools/check_all.py | SolidCraft contributors | original read-only authored/native resource validation orchestration and evidence receipts | MIT OR Apache-2.0 |
| assets/render/starter/tools/test_check_all.py | SolidCraft contributors | original validation-runner regression fixtures and subprocess tests | MIT OR Apache-2.0 |
| assets/render/starter/tools/requirements.txt | SolidCraft contributors | pinned offline authoring tool requirements; no application dependency changes | MIT OR Apache-2.0 |
| crates/render/examples/validate_starter.rs | SolidCraft contributors | original native resource conformance checker using public SolidCraft decoding, normalization, sampling and mip APIs | MIT OR Apache-2.0 |
| .gitattributes | SolidCraft contributors | original canonical line-ending policy for the existing runtime icon-discovery manifest; no artwork or icon-assignment changes | MIT OR Apache-2.0 |
| assets/render/.gitattributes | SolidCraft contributors | original canonical line-ending policy for SHA-256-pinned renderer authoring resources | MIT OR Apache-2.0 |
| docs/render-materials/.gitattributes | SolidCraft contributors | original canonical line-ending policy for SHA-256-pinned renderer authoring resources | MIT OR Apache-2.0 |
| assets/render/starter/environments/exports/assembly-bench-soft.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
| assets/render/starter/environments/exports/automotive-turntable.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
| assets/render/starter/environments/exports/black-metal-strips.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
| assets/render/starter/environments/exports/cnc-overhead.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
| assets/render/starter/environments/exports/esd-bench-neutral.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
| assets/render/starter/environments/exports/fdm-neutral-inspection.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
| assets/render/starter/environments/exports/garage-door.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
| assets/render/starter/environments/exports/hangar-skylight.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
| assets/render/starter/environments/exports/home-workbench.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
| assets/render/starter/environments/exports/makerspace-worklights.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
| assets/render/starter/environments/exports/mechanical-bay.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
| assets/render/starter/environments/exports/neutral-comparison.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
| assets/render/starter/environments/exports/neutral-side-softbox.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
| assets/render/starter/environments/exports/paint-inspection-bands.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
| assets/render/starter/environments/exports/printed-part-photo.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
| assets/render/starter/environments/exports/rework-offset-strips.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
| assets/render/starter/environments/exports/robot-led-editorial.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
| assets/render/starter/environments/exports/showroom-ribbons.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
| assets/render/starter/environments/exports/spacecraft-clean-bay.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
| assets/render/starter/environments/exports/sun-free-overcast.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
| assets/render/starter/environments/exports/vacuum-black.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
| assets/render/starter/environments/exports/warm-table-window.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
| assets/render/starter/environments/exports/white-enveloping-studio.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
| assets/render/starter/environments/exports/woodshop-window.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
| assets/render/starter/environments/manifest.json | SolidCraft contributors | original clean-room renderer resource recipe or manifest; authored/unbound scope and source receipts in assets/render/starter/README.md | MIT OR Apache-2.0 |
| assets/render/starter/environments/source/recipes.json | SolidCraft contributors | original clean-room renderer resource recipe or manifest; authored/unbound scope and source receipts in assets/render/starter/README.md | MIT OR Apache-2.0 |
| assets/render/starter/lighting/exports/rigs.json | SolidCraft contributors | original clean-room renderer resource recipe or manifest; authored/unbound scope and source receipts in assets/render/starter/README.md | MIT OR Apache-2.0 |
| assets/render/starter/lighting/recipe.json | SolidCraft contributors | original clean-room renderer resource recipe or manifest; authored/unbound scope and source receipts in assets/render/starter/README.md | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-001/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-001/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-001/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-001/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-001/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-002/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-002/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-002/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-002/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-002/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-003/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-003/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-003/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-003/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-003/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-004/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-004/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-004/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-004/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-004/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-005/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-005/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-005/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-005/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-005/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-006/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-006/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-006/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-006/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-006/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-007/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-007/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-007/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-007/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-007/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-008/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-008/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-008/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-008/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-008/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-009/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-009/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-009/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-009/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-009/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-010/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-010/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-010/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-010/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-010/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-011/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-011/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-011/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-011/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-011/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-012/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-012/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-012/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-012/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-012/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-013/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-013/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-013/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-013/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-013/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-014/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-014/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-014/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-014/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-014/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-015/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-015/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-015/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-015/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-015/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-016/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-016/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-016/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-016/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-016/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-017/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-017/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-017/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-017/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-017/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-018/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-018/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-018/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-018/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-018/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-019/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-019/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-019/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-019/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-019/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-020/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-020/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-020/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-020/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-020/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-021/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-021/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-021/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-021/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-021/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-022/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-022/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-022/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-022/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-022/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-023/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-023/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-023/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-023/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-023/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-024/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-024/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-024/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-024/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-024/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-025/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-025/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-025/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-025/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-025/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-026/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-026/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-026/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-026/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-026/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-027/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-027/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-027/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-027/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-027/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-028/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-028/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-028/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-028/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-028/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-029/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-029/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-029/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-029/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-029/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-030/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-030/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-030/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-030/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-030/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-031/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-031/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-031/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-031/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-031/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-032/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-032/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-032/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-032/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-032/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-033/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-033/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-033/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-033/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-033/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-034/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-034/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-034/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-034/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-034/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-035/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-035/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-035/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-035/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-035/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-036/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-036/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-036/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-036/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-036/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-037/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-037/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-037/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-037/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-037/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-038/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-038/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-038/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-038/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-038/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-039/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-039/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-039/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-039/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-039/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-040/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-040/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-040/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-040/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-040/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-041/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-041/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-041/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-041/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-041/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-042/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-042/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-042/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-042/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-042/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-043/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-043/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-043/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-043/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-043/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-044/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-044/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-044/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-044/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-044/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-045/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-045/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-045/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-045/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-045/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-046/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-046/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-046/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-046/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-046/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-047/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-047/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-047/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-047/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-047/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-048/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-048/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-048/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-048/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-048/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/manifest.json | SolidCraft contributors | original clean-room renderer resource recipe or manifest; authored/unbound scope and source receipts in assets/render/starter/README.md | MIT OR Apache-2.0 |
| assets/render/starter/materials/source/recipes.json | SolidCraft contributors | original clean-room renderer resource recipe or manifest; authored/unbound scope and source receipts in assets/render/starter/README.md | MIT OR Apache-2.0 |
| assets/render/starter/pack.json | SolidCraft contributors | original clean-room renderer resource recipe or manifest; authored/unbound scope and source receipts in assets/render/starter/README.md | MIT OR Apache-2.0 |
| assets/render/starter/tools/environments.py | SolidCraft contributors | original clean-room offline resource authoring, numerical verification or validation tool; no application runtime dependency | MIT OR Apache-2.0 |
| assets/render/starter/tools/lighting.py | SolidCraft contributors | original clean-room offline resource authoring, numerical verification or validation tool; no application runtime dependency | MIT OR Apache-2.0 |
| assets/render/starter/tools/materials.py | SolidCraft contributors | original clean-room offline resource authoring, numerical verification or validation tool; no application runtime dependency | MIT OR Apache-2.0 |
| assets/render/starter/tools/test_environments.py | SolidCraft contributors | original clean-room offline resource authoring, numerical verification or validation tool; no application runtime dependency | MIT OR Apache-2.0 |
| assets/render/starter/tools/test_lighting.py | SolidCraft contributors | original clean-room offline resource authoring, numerical verification or validation tool; no application runtime dependency | MIT OR Apache-2.0 |
| assets/render/starter/tools/test_materials.py | SolidCraft contributors | original clean-room offline resource authoring, numerical verification or validation tool; no application runtime dependency | MIT OR Apache-2.0 |
| assets/render/starter/tools/validate_pack.py | SolidCraft contributors | original clean-room offline resource authoring, numerical verification or validation tool; no application runtime dependency | MIT OR Apache-2.0 |
| assets/render/selene/licenses/UFL.txt | Canonical Ltd. | exact Ubuntu Font Licence 1.0 notice retained from https://github.com/0xlillith/solidcraft-rover/blob/b32437915372d32b71419dc8f5143fe22ff1e869/UFL.txt for the modelled Ubuntu Light lettering | Ubuntu Font Licence 1.0 |
| assets/render/selene/materials/appearance-pack.json | SolidCraft contributors | original clean-room English appearance authoring data: 48 illustrative opaque PBR projections with separate advanced target specifications; no measured dataset or external assets | MIT OR Apache-2.0 |
| assets/render/selene/materials/appearance-swatches.json | SolidCraft contributors | original analytic command script for 48 opaque box swatches and a support base, derived only from the adjacent original appearance pack; no external artwork or engineering properties | MIT OR Apache-2.0 |
| assets/render/selene/models/SELENE-functional.solidcraft | SolidCraft contributors (native enhancements); 0xlillith (original rover); SychPL / Mateusz Sych (supplied showcase assembly); Canonical Ltd. (lettering outlines) | native-command derivative of the unchanged author-supplied source, with original bearing, suspension, gripper, electronics and presentation work; source and final hashes in assets/render/selene/SOURCE.md and VALIDATION.md | Model and showcase contributions: MIT OR Apache-2.0 (explicit SychPL grant https://github.com/tangency-labs/solidcraft/pull/5494#issuecomment-6375788360); lettering: Ubuntu Font Licence 1.0, notice retained in assets/render/selene/licenses/UFL.txt |
| assets/render/selene/motion/interference-pairs.json | SolidCraft contributors | original authored exact-body pair selection for bounded native clearance checks on the functional SELENE derivative; not a generated collision result | MIT OR Apache-2.0 |
| assets/render/selene/motion/mechanism-acceptance.json | SolidCraft contributors | original kinematic acceptance criteria, inspected source-container bindings and analytic planning checks from cited public SolidCraft documentation and the admitted native source; derivative runtime acceptance is recorded separately | MIT OR Apache-2.0 |
| assets/render/selene/motion/preflight-commands.json | SolidCraft contributors | original read-only inventory script using existing project commands; no source-model selectors or state mutations | MIT OR Apache-2.0 |
| assets/render/selene/motion/sample-sequence.json | SolidCraft contributors | original future lunar sampling and sealed-vial dynamics state specification; no simulation or physical calibration result | MIT OR Apache-2.0 |
| assets/render/selene/scenes/roles.json | SolidCraft contributors | original semantic role and candidate-material authoring specification for the prepared SELENE demo; no native geometry or external assets bundled | MIT OR Apache-2.0 |
| assets/render/selene/scenes/shots.json | SolidCraft contributors | original shot, framing and staged-sequence authoring specification for the prepared SELENE demo; no render or simulation result claimed | MIT OR Apache-2.0 |
| assets/render/selene/scenes/stages.json | SolidCraft contributors | original stage composition and light-ownership authoring specification derived from the project render catalog; scientific context linked in scenes/README.md | MIT OR Apache-2.0 |
| assets/render/selene/source-lock.json | SolidCraft contributors | original source-admission metadata and public-reference inventory pinning the supplied improved SELENE native file; original-model and font notices preserved in SOURCE.md | MIT OR Apache-2.0 |
| assets/render/selene/source/SELENE-showcase.solidcraft | 0xlillith (original rover); SychPL / Mateusz Sych (showcase assembly and helpers); SolidCraft contributors (emblem); Canonical Ltd. (lettering outlines) | unchanged author-published native file from https://github.com/SychPL/solidcraft/blob/32ec1720447e3fcc6bb6cc53a63fb5b39cfa21b9/SELENE-showcase.solidcraft; source hash and license evidence in assets/render/selene/SOURCE.md | Model and showcase contributions: MIT OR Apache-2.0 (explicit SychPL grant https://github.com/tangency-labs/solidcraft/pull/5494#issuecomment-6375788360); lettering: Ubuntu Font Licence 1.0, notice retained in assets/render/selene/licenses/UFL.txt |
| assets/render/selene/tools/bearings.py | SolidCraft contributors | original native-command authoring of grooved bearings, rolling-element kinematics, clamp hubs and arm/head clearances; analytic geometry, no copied CAD source | MIT OR Apache-2.0 |
| assets/render/selene/tools/build.py | SolidCraft contributors | original Python development tool for pinned-source native command replay, staged checkpoints, command-trace and geometry health checks; no application runtime dependency | MIT OR Apache-2.0 |
| assets/render/selene/tools/electronics.py | SolidCraft contributors | original native-command Raspberry Pi 5 visual reconstruction using public manufacturer mechanical dimensions; original package envelopes and connector internals, no manufacturer CAD or artwork copied | MIT OR Apache-2.0 |
| assets/render/selene/tools/export_views.py | SolidCraft contributors | original native MCP preview export tool for three reusable showcase views; changes only transient visibility and cameras and verifies the source master hash | MIT OR Apache-2.0 |
| assets/render/selene/tools/finishes.py | SolidCraft contributors | original native physical-material proxy and opaque PBR assignment generator preserving the supplied rover palette | MIT OR Apache-2.0 |
| assets/render/selene/tools/mechanisms.py | SolidCraft contributors | original native-command gripper sleeves, pads, wheel ownership, joint limits and inspection studies with independent analytic suspension relation | MIT OR Apache-2.0 |
| assets/render/selene/tools/prepare.py | SolidCraft contributors | original Python development tool for bounded native-container inspection and reviewed opaque body-assignment preparation; no application runtime dependency | MIT OR Apache-2.0 |
| assets/render/selene/tools/presentation.py | SolidCraft contributors | original native-command browser organization, camera/render setups and closed sample-vial glass preparation | MIT OR Apache-2.0 |
| assets/render/selene/tools/suspension.py | SolidCraft contributors | original native-command trailing-arm suspension, telescoping coilover loops, plain bushings, carrier gussets and local motion clearances derived from the admitted rover geometry; independent analytic pin-distance relation, no copied external CAD | MIT OR Apache-2.0 |
| assets/render/selene/tools/validate_native.py | SolidCraft contributors | original bounded native MCP verification tool for sampled SELENE kinematics, independent analytic follower and world-coordinate checks, optional exact-name interference pairs and unchanged input hashes; no application runtime dependency | MIT OR Apache-2.0 |
| assets/render/selene/tools/validate_prepared.py | SolidCraft contributors | original Python development validator for prepared SELENE resources, source status, references, hashes and command projections; no application runtime dependency | MIT OR Apache-2.0 |
| assets/render/selene/tools/yaw_supports.py | SolidCraft contributors | original native-command plain yaw journals, flanged brass thrust bushings, recessed axial retaining pins and mounting-head clearances derived from the admitted rover geometry; no copied external CAD | MIT OR Apache-2.0 |
| assets/render/selene/ui/demo-flow.json | SolidCraft contributors | original progressive-disclosure UI integration specification for the existing Render workspace; no copied UI artwork or runtime registration | MIT OR Apache-2.0 |
| docs/render-materials/baseline-setup-commands.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
| docs/render-materials/capabilities.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
| docs/render-materials/environment-presets.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
| docs/render-materials/environments.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
| docs/render-materials/lighting.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
| docs/render-materials/lunar-physics-fixtures.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
| docs/render-materials/materials.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
| docs/render-materials/provenance.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
| docs/render-materials/quality-presets.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
| docs/render-materials/roadmap.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
| docs/render-materials/scene-presets.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
| docs/render-materials/selene-bindings.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
| docs/render-materials/simulation-requirements.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
| docs/render-materials/studio-presets.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
| docs/render-materials/ui-integration.json | SolidCraft contributors | original English UI integration specification and exact source-revision mappings from SolidCraft's own implemented shell, UI kit and accepted icons; no copied artwork or runtime registration | MIT OR Apache-2.0 |
| docs/render-materials/ux.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
| docs/render-materials/validate_catalog.py | SolidCraft contributors | original Python development validator for catalog references, preset composition and analytic specification fixtures; no application runtime dependency | MIT OR Apache-2.0 |
The original adapters in `crates/engine/src/cmd/render/library.rs`, the generated lookup `library_data.rs` and its generator `tools/render-library/generate.py` project the already attributed starter and SELENE resources without replacing their bytes or authoring manifests. The added scalar texture channels, explicit color/V conventions and immutable-source/GPU-payload sharing are original contributor work under MIT OR Apache-2.0. No external code, commercial CAD data or new third-party imagery was imported. Source metadata and the original authoring attribution remain authoritative; pending runtime acceptance is not a photometric or engineering qualification.
## Physical reference resource additions
| File | Author | Source | Licence |
|---|---|---|---|
| assets/render/physical/bindings.json | SolidCraft contributors | original physical-reference curation, associations or derived schema-1 export; per-fact public sources retained without relicensing their publications | MIT OR Apache-2.0 |
| assets/render/physical/exports/coverage.json | SolidCraft contributors | original physical-reference curation, associations or derived schema-1 export; per-fact public sources retained without relicensing their publications | MIT OR Apache-2.0 |
| assets/render/physical/exports/native-cards.json | SolidCraft contributors | original physical-reference curation, associations or derived schema-1 export; per-fact public sources retained without relicensing their publications | MIT OR Apache-2.0 |
| assets/render/physical/profiles.json | SolidCraft contributors | original physical-reference curation, associations or derived schema-1 export; per-fact public sources retained without relicensing their publications | MIT OR Apache-2.0 |
| assets/render/physical/quantities.json | SolidCraft contributors | original physical-reference curation, associations or derived schema-1 export; per-fact public sources retained without relicensing their publications | MIT OR Apache-2.0 |
| assets/render/physical/sources.json | SolidCraft contributors | original physical-reference curation, associations or derived schema-1 export; per-fact public sources retained without relicensing their publications | MIT OR Apache-2.0 |
| assets/render/physical/tools/check_physical.py | SolidCraft contributors | original offline physical-reference validation and projection tool | MIT OR Apache-2.0 |
| assets/render/physical/tools/physical.py | SolidCraft contributors | original offline physical-reference validation and projection tool | MIT OR Apache-2.0 |
| assets/render/physical/tools/test_physical.py | SolidCraft contributors | original offline physical-reference validation and projection tool | MIT OR Apache-2.0 |
| assets/render/physical/tools/export_library.py | SolidCraft contributors | original explicit reference-library planner and native no-loss export verifier; no external code | MIT OR Apache-2.0 |
| assets/render/physical/tools/test_export_library.py | SolidCraft contributors | original regressions for native capability refusal, source identity and physical-card preservation | MIT OR Apache-2.0 |
| tools/render-library/selene_studio.py | SolidCraft contributors | original bounded native MCP studio orchestration with pinned editable master, explicit GPU policy and request/export receipts; no generated model or image substitutes | MIT OR Apache-2.0 |
| tools/render-library/selene_recipe.py | SolidCraft contributors | original full-detail SELENE material/light composition and analytic camera fit; unchanged pinned native master, no external imagery or measured optical dataset | MIT OR Apache-2.0 |
| tools/render-library/test_selene_recipe.py | SolidCraft contributors | original independent projection, scaling, nonmutation and input-refusal fixtures for the SELENE studio recipe | MIT OR Apache-2.0 |
| assets/render/selene/scenes/studio-hero.json | SolidCraft contributors | original executable render intent and verified provenance for the selected native SELENE studio showcase; authored RGB appearance and relative light units, not measured optics or simulation results | MIT OR Apache-2.0 |
| tools/render-library/publish_selene.py | SolidCraft contributors | original bounded native-frame publication checks for explicit visual review, source identity, resource hashes, PNG CRCs and scanline limits; copies completed native bytes without image synthesis or resampling | MIT OR Apache-2.0 |
| tools/render-library/test_publish_selene.py | SolidCraft contributors | original tiny analytic PNG fixtures for source-size, CRC, expansion, dimensions and explicit-publication-consent regressions | MIT OR Apache-2.0 |
| tools/render-library/package_selene.py | SolidCraft contributors | original byte-preserving packaging of the reviewed native SELENE PNG, public provenance and checksums; bounded resource/PNG verification, no fonts, executables or new render execution | MIT OR Apache-2.0 |
| .github/workflows/selene-showcase.yml | SolidCraft contributors | original read-only artifact-delivery workflow for the reviewed native SELENE export; existing pinned repository actions, no compilation, package installation, secret use or renderer claims | MIT OR Apache-2.0 |
| tools/render-library/verify_linear_master.py | SolidCraft contributors | original independent validation driver for native linear EXR output using the official OpenEXR Python reader as an external test oracle; no codec implementation copied or production dependency added | MIT OR Apache-2.0 |
## Aging, effects and backgrounds showcase, 2026-10-23
| File | Author | Source | Licence |
|---|---|---|---|
| tools/render-showcase/instrument.py | SolidCraft contributors | original native-scene authoring, checksum-verified editorial assembly or bounded delivery tool; no third-party source or font binary copied | MIT OR Apache-2.0 |
| tools/render-showcase/compose.py | SolidCraft contributors | original native-scene authoring, checksum-verified editorial assembly or bounded delivery tool; no third-party source or font binary copied | MIT OR Apache-2.0 |
| tools/render-showcase/package.py | SolidCraft contributors | original native-scene authoring, checksum-verified editorial assembly or bounded delivery tool; no third-party source or font binary copied | MIT OR Apache-2.0 |
| distribution/SolidCraft-Render-Showcase.zip | SolidCraft contributors and the explicitly credited original model/UI/resource authors | byte-verified selected collaborator package of reviewed native media, editable sample, recipes and public provenance; contained attribution and existing SELENE lettering notices retained; no fonts or executable binaries | MIT OR Apache-2.0 for new contributions; referenced original notices retained |
| distribution/SolidCraft-Render-Showcase.json | SolidCraft contributors | original verified delivery archive manifest with exact SHA-256, member count and bounded scope | MIT OR Apache-2.0 |
artwork from commercial CAD products — ever.** Every UI icon is drawn in collected code
(`assets/icons/ui/source/`, compiled through `crates/ui-egui/src/icons.rs`). Any file asset must be original or openly licensed and **must have a row in ATTRIBUTION.md**
(`cargo xtask assets` enforces it). Screenshots of commercial CAD software are never committed.
Breaking this rule is the most serious mistake you can make in this repo.
- **Everything is a command.** User-visible behaviour = a command in `crates/engine/src/cmd/*`
(`CommandSpec`: id = our own dotted, lower-case id `<area>.<thing>[.<variant>]` such as
`solid.extrude` or `sketch.rectangle.two_point`, never another product's command id; label;
toolbar tab and panel; icon; shortcut; params doc; `enabled`; JSON `run`) + tests. The toolbar,
dialogs, sketch tools, palette, scripts, CLI and control channel all reach the same commands.
- **Programmatic calls never open dialogs.** `engine.execute` runs the JSON form; only toolbar
clicks (`ui.start`) start interactive tools and dialogs, which then run commands.
- **Layering** is enforced by `cargo xtask layers`: L0 `geom` → L1 `sketch`, `kernel`, `render`
-`crates/sketch`: our own constraint solver, plus profile finding.
@@ -148,6 +157,8 @@ the most complete description of the house style.
A geometry change that changes the robustness goldens re-records them in the same pull request:
`ROBUSTNESS_GOLDENS_BLESS="why it changed" cargo test --profile ci -p solidcraft-engine --lib robustness_goldens::`
(see [docs/robustness.md](docs/robustness.md)); CI runs every seed on pull requests that touch geometry.
[docs/mutation-testing.md](docs/mutation-testing.md) shows how to find kernel behaviour these
black-box suites miss.
**UI scenarios** drive the real app headless: start a tool, click a plane or a sketch point,
press keys, then assert on the result.
@@ -175,31 +186,39 @@ no LTO, and debug assertions off (truck has debug-only assertions that reject va
builds several times faster than `--release`. If a test fails only in a plain debug build, say so
in the PR. `cargo xtask test` builds the tests with it and then runs every test binary at once
(`cargo test` runs them one after another); arguments after `--` go to each binary, e.g.
`cargo xtask test -- shell`. Tests run beside each other on a busy machine, so a test must not
depend on wall-clock timing.
`cargo xtask test -- shell`. Set `SOLIDCRAFT_TEST_JOBS` to a positive integer to opt into a
shared bound on top-level test/side commands for `cargo xtask test` and `cargo xtask ci`.
For example, `SOLIDCRAFT_TEST_JOBS=2` allows two commands at once; the sequential wasm/doc
worker consumes one of those slots. Every binary and side check still runs after failures.
Without the variable the existing concurrency is unchanged. `RUST_TEST_THREADS`,
`ACTION_MATRIX_JOBS` and Cargo's build-job setting remain independent caller overrides;
this limit does not choose their values or change test filters, timeouts or baselines.
Tests run beside each other on a busy machine, so a test must not depend on wall-clock timing.
**Ready pull requests test what they touch.** CI maps changed files to workspace packages and
runs Clippy, the test build and tests for those and their dependents. The
[package selector](scripts/ci/affected.py) and [CI workflow](.github/workflows/ci.yml) define
which changes require native checks; documentation, packaging and bot-script-only PRs can run
repository checks only. Workspace-wide changes, merge-queue groups, nightly runs and Run
workflow select the full workspace. A push to `main` reuses a successful merge-queue run for
the same commit; otherwise it runs the full suite.
which changes require native checks; documentation, packaging, other-workflow and bot-script-only
changes run the repository checks only, on pull requests and on `main`. Workspace-wide changes,
merge-queue groups, nightly runs and Run workflow select the full workspace. A push to `main`
reuses a successful merge-queue run for the same commit, and runs only the repository checks
when no code changed since the last push to `main` that passed; otherwise it runs the full
suite. Doc tests, reference tests and the Windows jobs run on `main` and nightly only.
**CI shards the tests.** One job builds them into a cargo-nextest archive; two shards run on
pull requests and four during full validation. Every test runs in its own process: don't rely
on state left by another test.
Pull requests skip a few heavy tests (one to four minutes each), which run in full validation
Pull requests skip a few heavy tests (one to twelve minutes each), which run in full validation
(the merge queue, nightly, manual runs and pushes to `main` not already tested by the queue).
Check that validation after merging. They are listed in
`.config/nextest.toml` and the [testing page](docs/book/src/dev/testing.md):
-`solidcraft-engine`: `tests::hostile_params_never_panic`, everything in `tests::robustness` and
the seeds of `tests::robustness_goldens` (`seed_00` … `seed_95`; pull requests matching the workflow's geometry paths run all seeds),
the seeds of `tests::robustness_goldens` (`seed_00` … `seed_95`; pull requests thatchange the kernel, truck or the goldens run all but the slowest, `SLOW_SEEDS` in `ci.yml`),
`cmd::plastic::tests::enclosure_snap_fit_follows_resized_front_wall`, the `atomic_commands` contract tests
-`solidcraft-kernel`: `tests::boolean_identity_on_tangent_primitives`, and the `boolean_props` property tests
-`solidcraft-kernel`: `tests::boolean_identity_on_tangent_primitives`,`tspline::tests::hostile_tsplines_never_panic`, and the `boolean_props` property tests
(these run only in the nightly "Property tests" job: their inputs are random, so main skips them too)
after each merge by the Generated files workflow (`cargo xtask licences`, `cargo xtask
features`). Don't regenerate or edit them in a pull request.
- **Design format**: one file per change, never an edit to a shared number (above).
- **New UI strings and attribution rows**: one new file per change, `assets/locales/pending/<slug>.json`
and `attribution/pending/<slug>.md`, instead of editing the catalogs or the ATTRIBUTION.md table;
`cargo xtask fold-pending` folds them on `main` (see the README in each folder).
## Sending a change
@@ -283,6 +305,11 @@ and a fresh CI run:
2. Branch from `main` and keep the PR to one topic.
3. Run `cargo xtask ci` before pushing. It runs fmt, the asset, licence and layer checks,
clippy (`-D warnings`), then the tests with the wasm build check alongside. A PR has to pass it.
Heavy CI runs after review: a pull request gets the formatting and repository checks and a
quick compile (`cargo check`) when you push. Once a maintainer has reviewed it they add the
`approved-for-ci` label when the PR is ready to merge, which runs the full CI (build, tests,
goldens, Clippy); every push after that runs the full set again. Maintainers merge approved
PRs in groups by area, so PRs that touch the same code meet a conflict at most once.
4. Reference the issue (`Fixes #12`). In the PR, say what you tested and how. For geometry work, include the measured numbers (and
the expected ones).
@@ -307,18 +334,25 @@ Changes with no useful observable behavior do not need screenshot or video evide
small example without private designs or sensitive details; attach the media or link to an
accessible recording rather than committing generated review artifacts.
Keep checking upstream commits and the relevant PR's merge status throughout the work and
before handoff. Integrate upstream merges, resolve conflicts without losing others' changes,
and validate the integrated result. Before pushing, run `git pull --rebase origin main`, re-run
CI if it brought changes, and push without force. Report the upstream/PR state you checked,
any conflicts resolved and validation results. While a task remains active, follow an open
PR through its status changes; at handoff, clearly identify anything still awaiting merge.
Keep an eye on your PR's status, but don't push to it only to catch up with `main` (no merge of
`main`, no Update branch button, no rebase for that alone): every push restarts the whole CI
run for everyone waiting behind it, and `main`'s own run catches what lands in between. When you
push a change anyway, or GitHub reports a conflict, rebase onto the latest `main` (push your
branch with `--force-with-lease`) or merge `main` in once, and resolve conflicts without losing
others' changes. Don't re-run CI on an unchanged commit either: a real failure fails again.
Re-run only a job that runner trouble cancelled, with "Re-run failed jobs". While a task remains
active, follow an open PR through its status changes; at handoff, clearly identify anything
still awaiting merge.
Much of SolidCraft so far was written by AI coding agents (Claude) working in parallel, each
owning one area, with a human setting direction and reporting bugs. Their working notes are in
[AGENTS.md](AGENTS.md). Human contributors are just as welcome. Use whatever tools you like, as
long as the change meets the rules above.
Two quick checks run on every pull request (`.github/workflows/pr-checks.yml`, not required): it
fails when a lower-numbered open PR by someone else already closes the same issue (the label
`not-a-duplicate` skips it), or when the description lacks the `Changes a default: no` / `Changes a default: yes — <what>, old behaviour kept behind preference <name>` line (docs-only and bot PRs are exempt).
## Labels and triage
The triage bot labels every new issue and PR (`.github/workflows/triage.yml`). It only adds labels;
@@ -357,7 +391,21 @@ In order of how much they'd help users:
3. **Drawings, CAM and mesh tools.** These are out of scope for now (see
[ROADMAP.md](ROADMAP.md)), but anyone who wants to own one is welcome.
## Updating the generated feature list
After adding or renaming commands, run `cargo xtask features`. The registry is the
source of truth for `docs/features.md`; do not hand-edit the generated rows.
The generator sorts commands by ID and omits global and per-section counters,
so additions to unrelated areas no longer rewrite shared summary lines.
If two branches still conflict in this file (for example, adjacent additions in
the same area), first resolve the command registry and source conflicts, then run
`cargo xtask features` again and stage the regenerated file. Do not use Git's
`union` merge driver: it can silently retain stale or duplicate command rows.
The generator tests cover stable ordering and additions that leave existing lines
unchanged. The initial switch to sorted rows is a one-time larger diff.
## Licence
By contributing you agree that your contribution is licensed as MIT OR Apache-2.0, like the rest
of SolidCraft. SolidCraft isn't affiliated with Autodesk. Fusion is a trademark of Autodesk, Inc.
of SolidCraft. SolidCraft isn't affiliated with any CAD vendor; product names are trademarks of their owners (see NOTICE).

*A Raspberry Pi 4 case from an empty design: a sketch dimensioned with the board's parameters, extrude, fillet, shell, the rim rounded in one pick, an M2.5 standoff patterned on the 58 × 49 mm hole rectangle, port cut-outs, vent slots, "Pi 4" engraved in DejaVu Sans Bold and a PETG material defined in the design, with its mass in Properties, then height 24 → 32 mm in Parameters and it all rebuilds. 9× playback. [Full video](https://github.com/tangency-labs/solidcraft/raw/refs/heads/main/docs/screenshots/demo-0.8.mp4), recorded with the [demo script](examples/demo/record_ui.sh).*

*A Raspberry Pi 4 case from an empty design: a sketch dimensioned with the board's parameters, extrude, fillet, shell, the rim rounded in one pick, an M2.5 standoff patterned on the 58 × 49 mm hole rectangle, port cut-outs, vent slots, "Pi 4" engraved in DejaVu Sans Bold and a PETG material defined in the design, with its mass in Properties, then height 24 → 32 mm in Parameters and it all rebuilds. 6× playback with the idle pauses cut. [Full video](https://github.com/tangency-labs/solidcraft/raw/refs/heads/main/docs/screenshots/demo.mp4), recorded with the [demo script](examples/demo/record_ui.sh).*
## Download
- **Windows:** download and run the [installer][windows-msi].
Also for [ARM64][windows-arm64-msi] and [32-bit][windows-x86-msi] PCs, or as a
[portable zip][windows-portable] that runs without installing.
With [WinGet](https://learn.microsoft.com/windows/package-manager/winget/):
`winget install bherbruck.SolidCraft`.
- **macOS:** open the [disk image][macos-dmg] and drag SolidCraft into Applications
(Apple silicon and Intel).
- **Linux:** download the [AppImage][linux-appimage], allow it to run as a program and open it.
Or install the [.deb][linux-deb] (Ubuntu, Debian, Mint) or [.rpm][linux-rpm] (Fedora,
openSUSE), or use [Flatpak](https://docs.solidcraft.io/guide/getting-started.html#linux-flatpak).
With Snap: `sudo snap install solidcraft` ([Snap Store][snap-store], or the [.snap file][linux-snap]).
For ARM64: [AppImage][linux-arm64-appimage], [.deb][linux-arm64-deb], [.rpm][linux-arm64-rpm].
- **Windows:** the [installer][windows-msi], or `winget install bherbruck.SolidCraft`.
- **macOS:** the [disk image][macos-dmg] (Apple silicon and Intel).
- **Linux:** the [AppImage][linux-appimage], or `sudo snap install solidcraft` ([Snap Store][snap-store]).
- **Web:** [try it in your browser][web-app], no install.
The downloads aren't signed yet. On Windows, if SmartScreen warns you, choose **More info**, then
**Run anyway**. On macOS, the first time, right-click SolidCraft in Applications and choose **Open**.
ARM64 and 32-bit builds, portable zip, .deb, .rpm, Flatpak and the other packages are on the
[downloads page](docs/downloads.md). The downloads aren't signed yet: on Windows choose
**More info**, then **Run anyway** in SmartScreen; on macOS right-click SolidCraft and choose **Open**
the first time. On SolidCraft 0.11 to 0.14, download once by hand; see
**Documentation:** the [SolidCraft book](https://docs.solidcraft.io/) (user guide,
developer guide and command reference; sources in [docs/book](docs/book)).
**Community:** chat with users and contributors on [Discord](https://discord.gg/FtU4NQNVDH).
**Status: early but usable for simple parts.** 75 of 76 public reference parts
match their analytic or FreeCAD-computed volume, area, centre of mass and bounding box
([docs/oracle.md](docs/oracle.md)); the other is a tracked bug.
These checks do not establish complete geometric or workflow equivalence. The commands SolidCraft has
are listed by area in [docs/features.md](docs/features.md); status and plans are in [ROADMAP.md](ROADMAP.md).
Open work and known bugs are in [GitHub issues](https://github.com/tangency-labs/solidcraft/issues),
and help is welcome: see [CONTRIBUTING.md](CONTRIBUTING.md).
## Built with SolidCraft
<a href="https://github.com/0xlillith/solidcraft-rover"><img src="docs/screenshots/rover-build.gif" alt="SELENE, a scientific rover by 0xlillith, assembling itself as the timeline plays through its 3,323 steps in SolidCraft: tub, wheels, electronics, suspension, sampling arm, solar wings, dish and camera turret" loading="lazy"></a>
*SELENE, a scientific rover built entirely through SolidCraft's MCP server by
[@0xlillith](https://github.com/0xlillith/solidcraft-rover): 3,323 editable timeline steps that
rebuild with no errors, replayed here at 2× speed.
configuration and an example session: [docs/mcp.md](docs/mcp.md).
## Architecture
<!-- ANCHOR: architecture -->
| Layer | Crates | Role |
|---|---|---|
| L0 | `geom` | vectors, planes, profiles, meshes and their measures |
| L1 | `sketch`, `kernel`, `render` | constraint solver and profiles; the B-rep kernel boundary (truck); view math and the headless scene description |
| L2 | `doc` | parameters, expressions, the feature timeline and its incremental evaluation |
| L3 | `io` | design files, STL/OBJ/STEP/3MF export, STEP and 3MF/STL/OBJ/glTF import features |
| L4 | `engine` | the session and the command registry (everything is a command) |
| L5 | `gpu`, `ui-egui`, `mcp` | compute-shader evaluation of display tessellation plans; the swappable desktop front end; the MCP server (headless or bridged to the app) |
| Time to a useful alpha (M0–M6) | ≈ **82 agent hours** remain (sum of the M1–M6 estimates below) |
Optional GPU viewport ambient occlusion is available in View settings and Graphics preferences,
with persistent settings, Performance/Quality graphics presets (Quality combines AO, Fine tessellation and adaptive 2× supersampling; manual combinations show Custom), and the
`view.ambient_occlusion` and `view.graphics_preset` commands. See [display settings](docs/ambient-occlusion.md).
## Scope (owner decision, 2026-10-07)
The issue batch adds preservation of blind pockets in through cuts, associative projected
| M1 | Sketch depth: command coverage implemented — projection/intersect/include as linked geometry (auto-project on faces, lost-reference warnings, break link), full SKETCH create/modify set (slots, polygons, tangent arcs/circles, ellipse, fit/control splines, conic, text, trim/extend/break, fillet/chamfer, offset, mirror, patterns, move/scale, blend curve, centerline), constraints incl. curvature and polygon with over-constraint refusal, driven/arc-length/linear-diameter dimensions, AutoConstrain, DXF/SVG insert, 3D sketch curves (Project To Surface, Intersection Curve, Isoparametric Curve, Spun Profile), exact free-form profiles (B-spline/NURBS edges, any spline degree, curves cut by crossings), sweeps and pipes along 3D sketch curves, canvases and decals, curvature combs. Exact free-form Extend and certified associative offsets now preserve geometry and IDs through edits, deletion and reopen. Occurrence-frame 3D wires now store explicit coordinate spaces and retain points and analytic fits through moves, refresh, undo and reopen. Constructive sketch solving (Owen 1991, Fudos & Hoffmann 1997) ahead of the numerical solver: points and rigid clusters are placed on the branch the sketch had, so dimension edits and drags keep their solution; edits that would turn a sketch over are refused; drags re-solve only the sketch per frame. Remaining workflow repairs: mixed offset junctions, legacy natural-spline seams, and damaged legacy wire links lacking frame provenance | in progress | 4 |
| M1 | Sketch depth: command coverage implemented — projection/intersect/include as linked geometry (auto-project on faces, lost-reference warnings, break link), full SKETCH create/modify set (slots, polygons, tangent arcs/circles, ellipse, fit/control splines, conic, text, trim/extend/break, fillet/chamfer, offset, mirror, patterns, move/scale, blend curve, centerline), constraints incl. curvature and polygon with over-constraint refusal, Shift-picked line midpoints in Horizontal/Vertical using persisted points and ordinary constraints (#2087), driven/arc-length/linear-diameter dimensions, AutoConstrain, DXF/SVG insert, 3D sketch curves (Project To Surface, Intersection Curve, Isoparametric Curve, Spun Profile), exact free-form profiles (B-spline/NURBS edges, any spline degree, curves cut by crossings), sweeps and pipes along 3D sketch curves, canvases and decals, curvature combs. Exact free-form Extend and certified associative offsets now preserve geometry and IDs through edits, deletion and reopen. Occurrence-frame 3D wires now store explicit coordinate spaces and retain points and analytic fits through moves, refresh, undo and reopen. Constructive sketch solving (Owen 1991, Fudos & Hoffmann 1997) ahead of the numerical solver: points and rigid clusters are placed on the branch the sketch had, so dimension edits and drags keep their solution; edits that would turn a sketch over are refused; drags re-solve only the sketch per frame. Remaining workflow repairs: mixed offset junctions, legacy natural-spline seams, and damaged legacy wire links lacking frame provenance | in progress | 4 |
| M2 | Robust modelling: coplanar booleans in the kernel, holes (placed on faces, at distances from reference edges, and on sketch points), threads (cosmetic, then modelled), patterns, mirror, shell, draft, split, construction geometry | in progress | 16 |
| M1 | Sketch depth: command coverage implemented — projection/intersect/include as linked geometry (auto-project on faces, lost-reference warnings, break link), full SKETCH create/modify set (slots, polygons, tangent arcs/circles, ellipse, fit/control splines, conic, text, trim/extend/break, fillet/chamfer, offset, mirror, patterns, move/scale, blend curve, centerline), constraints incl. curvature and polygon with over-constraint refusal, Shift-picked line midpoints in Horizontal/Vertical using persisted points and ordinary constraints (#2087), drawing snaps to where two lines, circles or arcs cross (#4967), driven/arc-length/linear-diameter dimensions, AutoConstrain, DXF/SVG insert, 3D sketch curves (Project To Surface, Intersection Curve, Isoparametric Curve, Spun Profile), exact free-form profiles (B-spline/NURBS edges, any spline degree, curves cut by crossings), sweeps and pipes along 3D sketch curves, canvases and decals, curvature combs. Exact free-form Extend and certified associative offsets now preserve geometry and IDs through edits, deletion and reopen. Occurrence-frame 3D wires now store explicit coordinate spaces and retain points and analytic fits through moves, refresh, undo and reopen. Constructive sketch solving (Owen 1991, Fudos & Hoffmann 1997) ahead of the numerical solver: points and rigid clusters are placed on the branch the sketch had, so dimension edits and drags keep their solution; edits that would turn a sketch over are refused; drags re-solve only the sketch per frame. Remaining workflow repairs: mixed offset junctions, legacy natural-spline seams, and damaged legacy wire links lacking frame provenance | in progress | 4 |
| M2 | Robust modelling: coplanar booleans in the kernel, holes (placed on faces and on sketch points), threads (cosmetic, then modelled), patterns, mirror, shell, draft, split, construction geometry | in progress | 16 |
| M3 | General fillets and chamfers (curved edges, chains, vertex blends); saddle rolling-ball surfaces now use continuous seam sections and arc-length fitting with shared contact boundaries and bounded accuracy checks | in progress | 30 |
| M4 | Files: STEP import (done — [docs/step-import.md](docs/step-import.md)), FreeCAD/Rhino solids through cadmpeg (done, desktop and CLI), 3MF export and 3MF/STL import as mesh bodies (done), 3MF with B-rep face tags ([docs/formats/3mf-face-tags.md](docs/formats/3mf-face-tags.md), done), constant-section parametric mesh reconstruction, DXF sketches, OBJ import | in progress | 6 |
| M3 | General fillets and chamfers (curved edges, chains, vertex blends); saddle rolling-ball surfaces now use continuous seam sections and arc-length fitting with shared contact boundaries and bounded accuracy checks; tiny rim edges remain selected instead of nearby wall seams when filleting (#2498) | in progress | 30 |
| M4 | Files: STEP import (done — [docs/step-import.md](docs/step-import.md)), FreeCAD/Rhino solids through cadmpeg (done, desktop, browser and CLI), 3MF export and 3MF/STL import as mesh bodies (done), 3MF with B-rep face tags ([docs/formats/3mf-face-tags.md](docs/formats/3mf-face-tags.md), done), constant-section parametric mesh reconstruction, DXF sketches, OBJ import | in progress | 6 |
| M5 | Persistent naming of faces and edges (docs/book/src/dev/naming.md): faces named from their exact kernel-reported history (sketch curve sides, caps, revolve sections, primitive sides, blends, shell inner walls, pattern/mirror copies; split pieces told apart by the faces around them, lumps of a cut body named after their faces), edges by their faces; fillets/chamfers, sketches on faces, shell/draft/offset/replace faces, hole placements, joint origins and face appearances resolve by name first and warn when the entity split or vanished: done. sheet metal edges by base sketch line and flange side, shell inner faces, sweep and loft roles: done. kernel-reported face history: done (merged faces answer to every name; operations without history yet fall back to geometric names, flagged unverified; format 14 keeps older designs' geometric names). Left: history through sheet metal, plastic, surface and emboss features; FaceSource certificates through history | in progress | 20 |
| M6 | UI depth: selection feedback and origin (done), timeline editing (done: hover and selection highlight modifier faces, including fillets and chamfers; edit feature, history marker drag, dependency-checked single and multi-item reorder, suppress, rename, delete with dependents, re-resolved references; redefining selections refreshes persistent edge/face references while retaining feature identity and dimension names; optional compact named left feature history with rollback bar, multi-selection and persistent folders above the parts browser), right-docked dialogs (done), live previews for every feature dialog (done), keyboard-first commands with on-canvas value boxes and sketch inline dimensions (done), drag manipulators with automatic join/cut (done: extrude incl. two sides, fillet, shell, hole depth, move, pattern spacing, revolve rotator, extrude taper), dark theme (done), right-click menu with Repeat (done), measure tool (done), section analysis (done), selection filter (done), drag-solve of sketch points (done); appearance controls now preserve body/face/component targeting, named transparent looks, reuse/search and override removal; preferences | in progress | 6 |
| M6 | UI depth: selection feedback and origin (done), timeline editing (done: hover and selection highlight modifier faces, including fillets and chamfers; edit feature, history marker drag, dependency-checked single and multi-item reorder, suppress, rename, delete with dependents, re-resolved references; redefining selections refreshes persistent edge/face references while retaining feature identity and dimension names; responsive feature history with width-driven icons/names/title, snapping width resize, bottom/left/browser-stack docking, shared-column collapse, rollback bar, multi-selection and persistent folders), right-docked dialogs (done), live previews for every feature dialog (done), keyboard-first commands with on-canvas value boxes and sketch inline dimensions (done), drag manipulators with automatic join/cut (done: extrude incl. two sides, fillet, shell, hole depth, move, pattern spacing, revolve rotator, extrude taper), dark theme (done), right-click menu with Repeat (done), measure tool (done), section analysis (done), selection filter (done), drag-solve of sketch points (done); appearance controls now preserve body/face/component targeting, named transparent looks, reuse/search and override removal; Browser Expand All / Collapse All (`browser.expand_all`, `browser.collapse_all`) on the design and component menus; preferences | in progress | 6 |
| M7 | Thin extrude (walls along open or closed curves: centre, inside, outside or two sides; mitred corners, crossing walls joined; #721), sweep, loft (incl. to a point), rib and web (to next / depth), emboss (planar faces and exact radial emboss/deboss of bounded orthogonal profiles on complete exterior cylinders/sleeves), thread (cosmetic, and modelled M3–M20 outside and inside: the ISO profile screwed in as a faceted cut), coil (smooth B-spline helical walls), pipe, pattern on path, patterns of bodies and components, replace face (parallel), align, remove, feature copy/paste: done. Unique eligible cylindrical targets are inferred for new Emboss features. Left: general curved-face emboss and exact (unfaceted) modelled threads | in progress | 12 |
| M8 | Surface workspace (#772). Done: Patch (sketch regions; edge loops filled G0, plus `crown`), Stitch, Thicken (planes and cylinders), Extend (straight edges of planar surfaces), Trim (by a plane or a face), Delete Face (healing), Boundary Fill, zebra/draft/curvature-map analysis, Surface Extrude/Revolve/Sweep/Loft from open and closed curves, Offset, Unstitch and Reverse Normal; the continuity-driven core (`kernel/src/bridge.rs`, G0–G3 Hermite rows) with **Blend** (`surface.blend`, #764), **Match** (`surface.match`), **Boundary** (`surface.boundary`, #763 — two sketch curves exact with G0–G3, three or four as the flat patch of their loop, and curve networks: Coons and Gordon surfaces through sketch, 3D-sketch or edge curves, a point for a three-sided side, G1/G2 against the faces along one direction's edges, joined into their body on request; G1/G2 on two sides that meet at a corner waits for the Gregory-type corner), **Fair** (`surface.fair`, CV fairing), **continuity analysis** (`inspect.continuity`, #770; a toolbar tool with edge markers and a legend, and curvature combs along face isocurves) and a multi-section **loft** with per-end continuity and crown. Also in the Class-A pass: G1…G4 blend curves in the sketch, chord and variable-radius fillets on curved edges (with honest refusals where the fit cannot hold), and a G2 corner patch where filleted edges meet (kernel; the command refuses until the patch's sections can be sewn to the blends' contacts). Left: bias/form-factor fillet sections (design recorded), a Gregory-type corner, surface Fillet between separate surface bodies (#769), Ruled (#762), Untrim (#761), curved-face Extend (#760), loft guide rails (#201; sweep guide rails and two-rail sweeps are done, #771), and every remaining #757–#772 item | deferred | — |
| M9 | Components and assemblies: New Component / Create Components from Bodies, active component, per-component origin/bodies/sketches/timeline entries, occurrences with transforms, Copy/Paste vs Paste New, nested Browser tree with visibility and isolate; Ground, Move/Copy of occurrences, joints (rigid, revolute, slider, cylindrical, pin-slot, planar, ball) by joint origins with limits and Drive Joints, closed kinematic loops (four-bar, slider-crank: the loop's other joints follow the driven one on its branch, and driving stops where a loop can no longer close), cams (`joint.cam`: a revolute cam drives a slider or revolute follower along a closed sketch profile or the cam's edges, knife, roller or flat followers, worked out again when the profile changes; drives, studies, closed loops, contact sets), As-built Joint, Rigid Group, Contact Sets (driving and motion studies stop where bodies in a set would pass through each other), Motion Study (joint values keyed along a timeline, playback, export of occurrence positions as JSON/CSV), motion analysis (joint values and a point's path charted over a study as value, velocity and acceleration; the parts that collide along a study, with the steps where they do), Exploded views (automatic or by hand, shown without changing placements); interference, per-component physical properties; STEP with assembly structure; URDF export for robot simulators (links with meshes, mass and inertia; joint chains whose values match Drive Joints; docs/urdf.md). Physical properties now measure material-aware component/occurrence placements, descendants and repeated instances, including pending moves and body reassignment offsets; contact geometry and automatic exploded centres respect those offsets. Groundwork done: component tree, occurrences with transforms (instances, Paste New), world placement, active component as authoring frame, ground, Capture/Revert Position, moving bodies and sketches. UI: ASSEMBLE panel dialogs for New Component, Joint (snaps on faces, edges, circles and vertices; animated motion preview; offsets, flip, limits), As-built Joint, Joint Origin, Rigid Group, Drive Joints (live slider), Motion Link, Cam and Interference (overlaps shown red); joints in the timeline and the Browser | in progress | 32 |
| M10 | Sheet metal: flange (base, edge, contour), hem, unfold/refold, convert, flat pattern + DXF, rules: done. PLASTIC enclosure subset: Boss, Rib, Web, Lip/Groove, Snap Fit, Rest, Plastic Rules (library, edit, assign; rule draft and clearance as feature defaults): done, tested against hand-computed geometry. UI: SHEET METAL and PLASTIC tabs in Fusion's layout with dialogs (live preview, Edit Feature, pre-selection) for Flange, Hem, Unfold/Refold, Convert, Sheet Metal Rules, Create Flat Pattern (flat view with bend lines, bend table CSV/JSON and DXF export with exact circle/arc/line cutouts), Boss, Lip/Groove, Snap Fit, Rest, Manage and Assign Plastic Rules; Fold (bend along a sketch line); flanges meeting at a corner closed at any angle with the rule's gap; edge flanges with their own angle and height per edge in one feature; corner reliefs (notch, round, rectangular, tear) chosen by the rule; Convert to Sheet Metal from a solid or shelled body by its bend edges (flat faces, sharp bends) | in progress | 36 |
| M8 | Surface workspace (#772). Done: Patch (sketch regions; edge loops filled G0, plus `crown`), Stitch, Thicken (planes and cylinders), Extend (straight edges of planar surfaces), Trim (by a plane, a face or sketch curves swept onto the surface, keeping a side or the picked regions; mutual trim of two surfaces, extended to meet and stitched, #767), Delete Face (healing), Boundary Fill, zebra/draft/curvature-map analysis, Surface Extrude/Revolve/Sweep/Loft from open and closed curves, Offset, Unstitch and Reverse Normal; the continuity-driven core (`kernel/src/bridge.rs`, G0–G3 Hermite rows) with **Blend** (`surface.blend`, #764: edge chains along one or several faces, closed rims paired seam to seam or from picked seams, a dialog), **Match** (`surface.match`), **Boundary** (`surface.boundary`, #763 — two sketch curves exact with G0–G3, three or four as the flat patch of their loop, and curve networks: Coons and Gordon surfaces through sketch, 3D-sketch or edge curves, a point for a three-sided side, G1/G2 against the faces along one direction's edges, joined into their body on request; G1/G2 on two sides that meet at a corner waits for the Gregory-type corner), **Fair** (`surface.fair`, CV fairing), **continuity analysis** (`inspect.continuity`, #770; a toolbar tool with edge markers and a legend, and curvature combs along face isocurves) and a multi-section **loft** with per-end continuity and crown, and guide rails on solid and surface lofts (one or two, scaling or stretching the sections between; #201). Also in the Class-A pass: G1…G4 blend curves in the sketch, chord and variable-radius fillets on curved edges (with honest refusals where the fit cannot hold), and a G2 corner patch where filleted edges meet (kernel; the command refuses until the patch's sections can be sewn to the blends' contacts). Left: bias/form-factor fillet sections (design recorded), a Gregory-type corner, surface Fillet between separate surface bodies (#769), Ruled (#762), Untrim (#761), curved-face Extend (#760), and every remaining #757–#772 item | deferred | — |
| M9 | Components and assemblies: New Component / Create Components from Bodies, active component, per-component origin/bodies/sketches/timeline entries, occurrences with transforms, Copy/Paste vs Paste New, nested Browser tree with visibility and isolate; Ground, Move/Copy of occurrences, joints (rigid, revolute, slider, cylindrical, pin-slot, planar, ball) by joint origins with limits and Drive Joints, closed kinematic loops (four-bar, slider-crank: the loop's other joints follow the driven one on its branch, and driving stops where a loop can no longer close), cams (`joint.cam`: a revolute cam drives a slider or revolute follower along a closed sketch profile or the cam's edges, knife, roller or flat followers, worked out again when the profile changes; drives, studies, closed loops, contact sets), As-built Joint, Rigid Group, Contact Sets (driving and motion studies stop where bodies in a set would pass through each other), Motion Study (joint values keyed along a timeline, playback, export of occurrence positions as JSON/CSV), motion analysis (joint values and a point's path charted over a study as value, velocity and acceleration; the parts that collide along a study, with the steps where they do), Exploded views (automatic or by hand, shown without changing placements, and exported as an animation coming apart and back together); interference, per-component physical properties; STEP with assembly structure; URDF export for robot simulators (links with meshes, mass and inertia; joint chains whose values match Drive Joints; docs/urdf.md). Physical properties now measure material-aware component/occurrence placements, descendants and repeated instances, including pending moves and body reassignment offsets; contact geometry and automatic exploded centres respect those offsets. Groundwork done: component tree, occurrences with transforms (instances, Paste New), world placement, active component as authoring frame, ground, Capture/Revert Position, moving bodies and sketches. UI: ASSEMBLE panel dialogs for New Component, Joint (snaps on faces, edges, circles and vertices; animated motion preview; offsets, flip, limits), As-built Joint, Joint Origin, Rigid Group, Drive Joints (live slider), Motion Link, Cam and Interference (overlaps shown red); joints in the timeline and the Browser | in progress | 32 |
| M10 | Sheet metal: flange (base, edge, contour), hem, unfold/refold, convert, flat pattern + DXF, rules: done. PLASTIC enclosure subset: Boss, Rib, Web, Lip/Groove, Snap Fit, Rest, Plastic Rules (library, edit, assign; rule draft and clearance as feature defaults): done, tested against hand-computed geometry. UI: SHEET METAL and PLASTIC tabs with dialogs (live preview, Edit Feature, pre-selection) for Flange, Hem, Unfold/Refold, Convert, Sheet Metal Rules, Create Flat Pattern (flat view with bend lines, bend table CSV/JSON and DXF export with exact circle/arc/line cutouts), Boss, Lip/Groove, Snap Fit, Rest, Manage and Assign Plastic Rules; Fold (bend along a sketch line); flanges meeting at a corner closed at any angle with the rule's gap; edge flanges with their own angle and height per edge in one feature; corner reliefs (notch, round, rectangular, tear) chosen by the rule; Convert to Sheet Metal from a solid or shelled body by its bend edges (flat faces, sharp bends) | in progress | 36 |
| M11 | 2D drawings | deferred | — |
| M12 | Release: cross-platform packages, verification and deliberate publication implemented; original current-UI demo refreshed. Trusted distribution signing and broader performance work remain | in progress | 16 |
| M12 | Release: cross-platform packages, verification and deliberate publication implemented; original current-UI demo refreshed. macOS Developer ID signing and notarization are in the release workflow (#5749; they switch on when the Apple secrets exist). Windows installer signing and broader performance work remain | in progress | 16 |
| M14 | Configurations: a table of rows (parameter expressions and feature suppressions per row) switched with config.activate (undoable, all or nothing), kept through parameter renames and feature deletes (config.configure, config.table, config.column/row/activate). Per-row body materials and appearances (library looks, custom colour/opacity and clearing overrides), undoable activation and save/reopen: done. Ordered typed configuration rules, atomic validation and structured rule editing: done. Component appearance and certified semantic face style columns, atomic preview/apply and independent-copy remapping: done. Exact constructed sketch-side and immutable imported-face lineage now preserve legacy names and reject retargeting. Left: ambiguous shifted/two-sided caps | in progress | 4 |
| M13 | Never lose work: atomic saves (temporary file, flush, rename; a crash at any moment leaves the old file or the new one) with the replaced version kept as `<file>.bak`; autosave of unsaved changes to the recovery folder every 5 minutes (preference) and after big operations, only when something changed; "Recover unsaved design?" on the next launch (`doc.recovery_list`, `doc.recover`, `doc.recovery_discard`); a kill-mid-save test (process killed while saving a 0.5 MB design 25 times). File format: `"format": "solidcraft/12"`, older formats upgraded step by step (`doc::format`), newer ones refused with a message, out-of-range numbers and broken placements refused; a corpus of designs saved by M0, components-era, joints-era and current builds (apps/solidcraft-cli/tests/corpus) that must open and evaluate as saved; damaged and hostile variants (truncation, bit flips, hostile values, removed keys, repeated features, deep nesting) never panic | done | 6 |
| M15 | Frames and weldments (#4003): structural members along 2D and 3D sketch lines, butt and mitre corners, trim to faces and bodies, end caps, gussets, cosmetic welds and a cut list. Done: the profile library (`frame.profile.list`, `frame.profile.properties`, `frame.profile.insert`): bar, round bar, rectangular and round tube, angle, channel and I-beam, each SolidCraft's own parametric definition with exact fillet arcs and exact section properties (area, centroid, second moments), tested against hand-computed values; members (`frame.member`): one body per straight line of a 2D or 3D frame sketch, following parameter edits, with a built-in profile (its dimensions parameter expressions) or a profile sketch (its drawn material by alternating nested loops), anchor, angle and mirror; each member's name kept with its line. Left: corners, trims, end caps, gussets, welds, cut list, FRAME tab and dialogs | in progress | 60 |
M6 navigation preferences include **Touchpad**: Shift + pointer movement pans, Alt/Option +
pointer movement orbits without a mouse button, and vertical scrolling zooms. Other presets
retain their saved two-finger scroll preference; camera motion is available through
`view.navigate` and modifier-aware `ui.move` automation.
With the **Fusion** preset and two-finger pan enabled, two fingers pan and Shift + two fingers
orbit freely, with pinch zoom and mouse bindings unchanged. The shared navigation handler
applies the same gestures to model and Render views and blocks orbit on flat pages.
M11 precursor: File → Export Current View as DXF exports shown placed bodies from the current
editable, and Automatic size returns to the face's suggestion. Cosmetic/modelled and
@@ -94,6 +112,8 @@ completed drawings publish atomically. The browser still exports synchronously.
drag continues, then catch up to the exact final value. Changes to the selected geometry,
operation or feature still discard obsolete results. Distance arrows and their value boxes
follow the current frame's input.
- Large fine modelled threads use pitch-based helix resolution and tighter faceted
reconstruction fallbacks, preserving thin crests and closed shells on large shafts.
## Physical material data
@@ -125,6 +145,12 @@ completed drawings publish atomically. The browser still exports synchronously.
## Render foundation
- Original starter appearances (#1722): 34 opaque recipes through existing commands, with six explicitly unavailable glass recipes and preserved legacy looks; see [the contract](docs/render-starter-appearances.md).
- Bounded linear film accumulation (#1696) supplies caller-admitted mean/variance storage and complete-pass views; actual progressive consumers remain separate. See [the film contract](docs/render-film.md).
- Definition-local visual mapping (#1214) supplies a bounded numerical frame, physical repeat coordinates and a deterministic one-time grain-axis suggestion. It adds no saved record or texture consumer; see [the mapping contract](docs/render-visual-mapping.md).
- Explicit texture normalization (#1218) produces completed float RGBA with
role-aware linear color/data semantics, bounded matrix/TRC ICC conversion and
opt-in EXIF orientation. It retains high-precision raw samples and records lossy
@@ -180,6 +206,11 @@ completed drawings publish atomically. The browser still exports synchronously.
## Dimensioning and error reporting
- Operation length fields share design-unit defaults and input evaluation, including secondary
attachments and ambiguous coincident sources remain unsupported.
- G2 fillets: exact quintic blends of straight edges between planar faces, with one perpendicular
planar end face at each end. The UI and `solid.fillet` expose`curvature` (alias `g2`); the size
is an equivalent circular setback. Curved faces/edges, sloped ends and corner junctions remain unsupported.
- G2 fillets: exact quintic blends of straight edges between planar faces, with one planar
end face at each end, including sloped ends (#2708). The UI and `solid.fillet` expose
`curvature` (alias `g2`); the size is an equivalent circular setback. Curved faces/edges,
multi-face ends and corner junctions remain unsupported.
- Fillets and chamfers: constant, variable-radius and chord-length fillets; a radius per edge,
rounds next to earlier rounds of another radius, and smooth corner patches where three rounds
meet (a sphere for equal radii); perpendicular corner fillets after a closed ring of earlier
rounds preserve the ring's radii and the enclosed planar face (#1557);
mitres between separate rounds of the same radius keep their endpoint tangent, so a later
fillet can select an upright and its adjoining bottom mitre as one chain (#2422);
straight edges (convex or concave) between planar faces with perpendicular
planar end faces (equal-distance chamfers also meet sloped planar end faces, including inner
basin rims within the rim width); convex fillets also meet sloped planar ends and reflex
@@ -453,10 +510,13 @@ completed drawings publish atomically. The browser still exports synchronously.
and author occurrence paths retain the chosen placements; missing/split faces refuse rather
than retarget. General free-form profiles, seams, stepped cylinders and other curved surfaces
remain unsupported. STEP round-trip and independent radial/volume checks cover this subset.
- Shell and offset faces work on bodies of planes, cylinders, cones and spheres (convex or not:
- Shell and offset faces work on bodies of planes, cylinders, cones, spheres and tori (convex or not:
filleted boxes, bosses, domes, countersinks; corners where four or more planes meet, such as a
floor falling to a drain, as long as the corner's edges are all convex or all concave); draft on convex planar bodies and on walls (planes, cylinders → cones) between caps; lofts through three or more sections are smooth (B-spline sides through every section), two
sections ruled.
sections ruled. Circular-meridian barrels retain curved inner walls through either axial
opening (#2421). Solid lofts preserve holes through sections with matching hole counts
(#2435), including inner walls and caps with openings; a hole cannot appear or disappear
between sections, and sections with holes cannot collapse to a point.
- Sketch corner tools: fillet and all three chamfer modes pick a connected corner even when
its endpoint overlaps the fixed origin or a loose point. Ordinary drawing still snaps to
@@ -543,6 +603,10 @@ completed drawings publish atomically. The browser still exports synchronously.
continuous collision detection; local thin walls and small details remain limitations.
See [contact-set limits and recovery](docs/contact-sets.md).
## Assembly dragging
The design's first part is grounded when it is made, and a left-button drag repositions a free part (`occurrence.drag`: one undo step, exact occurrence paths including repeated jointed definitions, any floating-chain member carries the chain, grounded parts and joints hold where present, Escape cancels).
## 3D sketch bridges
3D sketch drawing snaps to points on other visible sketch planes without flattening them onto
"timing_scope":"wall time from render.start through completed PNG encoding; includes job preparation and readback, excludes CAD generation, preflight and initial CPU/GPU host setup; GPU setup timing includes adapter selection and pipelines when requested",
//! prints a table of the results; `--markdown` writes the issue's comment template filled in,
//! `--json` every task's rules, conversation and design before and after.
usestd::io::{BufRead,BufReader,Write};
usestd::net::{SocketAddr,TcpStream};
usestd::time::{Duration,Instant};
useserde_json::{Value,json};
/// Standard test v1, as #4851 states it. Changing a prompt or a rule makes it another test.
constTASKS: &str=r#"[
{"id": "01-cube","prompt": "Make a 20 mm cube.","check": {"bodies": 1,"volume": 8000,"bbox_size": [20,20,20]}},
{"id": "02-cylinder","prompt": "Make a cylinder 30 mm in diameter and 50 mm tall.","check": {"bodies": 1,"volume": 35342.92,"bbox_size": [30,30,50]}},
{"id": "03-block","prompt": "Create a block 60 mm long, 40 mm wide and 10 mm thick.","check": {"bodies": 1,"volume": 24000,"bbox_size": [10,40,60]}},
{"id": "04-sketch-extrude","prompt": "Sketch a 50 by 30 mm rectangle on the XY plane and extrude it 15 mm.","check": {"bodies": 1,"volume": 22500,"bbox_size": [15,30,50],"feature_kinds": ["Extrude"]}},
{"id": "05-plate-hole","prompt": "Make a plate 80 x 60 x 10 mm with a 20 mm diameter hole straight through the middle.","check": {"bodies": 1,"volume": 44858.41,"bbox_size": [10,60,80],"note": "80*60*10 - pi*10^2*10"}},
{"id": "06-fillet-box","prompt": "Round all the edges of the box with a 3 mm radius.","setup": [{"command": "solid.box","params": {"length": 40,"width": 40,"height": 40}}],"check": {"bodies": 1,"volume": 63108.53,"tol": 0.003,"feature_kinds": ["Fillet"],"note": "all 12 edges of a 40 mm cube rounded r=3"}},
{"id": "07-move-body","prompt": "Move the box 50 mm along the X axis.","setup": [{"command": "solid.box","params": {"length": 20,"width": 20,"height": 20}}],"check": {"bodies": 1,"volume": 8000,"bbox_min": [50,0,0]}},
{"id": "08-rename-body","prompt": "Rename the body to Bracket.","setup": [{"command": "solid.box","params": {"length": 30,"width": 20,"height": 10}}],"check": {"bodies": 1,"volume": 6000,"body_name": "Bracket"}},
{"id": "09-volume-question","prompt": "What is the volume of the part?","setup": [{"command": "solid.box","params": {"length": 30,"width": 20,"height": 10}}],"check": {"bodies": 1,"volume": 6000,"unchanged": true,"reply_number": "6[ ,. ]?000(?![0-9])|6(\\.0+)? ?cm"}},
{"id": "10-two-holes","prompt": "Make a 100 x 50 x 5 mm plate and drill two 10 mm holes through it, each 15 mm in from the short ends and centred across the width.","check": {"bodies": 1,"volume": 24214.6,"bbox_size": [5,50,100],"note": "100*50*5 - 2*pi*5^2*5"}},
{"id": "11-any-change","prompt": "can you use the software to make a change to the current shape, any change at all","setup": [{"command": "solid.box","params": {"length": 30,"width": 20,"height": 10}}],"check": {"changed": true,"note": "verbatim user prompt from #4846; pass = the design changed"}}
]"#;
/// The agent the test adds. A fixed id replaces it on every run instead of adding another.
constAGENT_ID: &str="api-bench-assistant";
constAGENT_NAME: &str="bench";
/// Longest control reply read (the channel's own request limit).
constMAX_REPLY: usize=16<<20;
/// Most result data kept for `--json`.
constMAX_RESULTS: usize=64<<20;
/// Deadline of the requests that only set up or read (each is answered at once by the app).
/// One command the CLI takes: the name to type, the usage it prints for `--help`,
/// and whether the top-level usage lists it (an alias answers to another command's name).
structCommand{
name: &'staticstr,
usage: &'staticstr,
listed: bool,
}
/// `run`'s lines in the usage.
constUSAGE_RUN: &str=" solidcraft-cli run <script.json|design.solidcraft|part.step> [--in design.solidcraft|part.step] [--out FILE]... [--save FILE] [--quiet] [--results] [--timings] [--profile] [--json-out FILE] [--allow-errors]\n solidcraft-cli run --in <design.solidcraft|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";
/// `eval`'s lines in the usage.
constUSAGE_EVAL: &str=" solidcraft-cli eval <script.json|design.solidcraft|part.step> [--json-out FILE] (alias: inspect; or --in FILE)\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";
/// `script`'s lines in the usage.
constUSAGE_SCRIPT: &str=" solidcraft-cli script <design.solidcraft|script.json> [--out script.json]\n (the design's history as a command script that eval and run replay)\n";
/// `bench-open`'s lines in the usage.
constUSAGE_BENCH_OPEN: &str=" solidcraft-cli bench-open <design.solidcraft | part.step> [--parse-only] [--save OUT.solidcraft] [--display LEVEL [--edit NAME EXPR]] [--ops script.json]\n (--ops: after opening, run each command of the script and undo it, timing the command, the\n evaluations it caused and the view it leaves: placing and meshing what changed)\n";
constUSAGE_SNAPSHOT: &str=" solidcraft-cli snapshot <script|design> [--in design.solidcraft|part.step] --out shot.png [--width W] [--height H] [--view NAME | --camera JSON] [--no-grid] [--no-axes]\n [--body NAME:opacity=0.3,color=#rrggbb]... [--tool NAME]... [--section z=27]\n solidcraft-cli snapshot --in <design.solidcraft|part.step> --out shot.png ...\n (as with run, --in starts from a design and the script runs on top of it; an option snapshot doesn't take is an error;\n --view: iso, front, back, top, bottom, left, right, or the name of a view saved in the design with view.save;\n --camera: {target: [x,y,z], yaw, pitch, distance, fov?} in mm and radians, as the MCP screenshot takes it (docs/mcp.md);\n --no-grid and --no-axes leave the ground grid (on Z 0) and the origin's axes out of the picture;\n --body: how one body is drawn, as appearance.assign takes it: opacity 0-1 (below 1 is see-through, so what is inside\n it shows) and color #rrggbb, either or both, once per body;\n --tool: draw this body solid in the tool colour and every other body see-through, so a cut's or a join's tool body\n is visible inside the material it meets; a later --body overrides it;\n --section: cut the model as inspect.section does, so the inside shows: z=27 (the plane through that world\n coordinate, keeping the smaller side), add ,flip to keep the other side, or XY, XZ, YZ, or its own JSON)\n";
constUSAGE_GPU: &str=" solidcraft-cli gpu [--policy auto|hardware|software|cpu]\n (the GPU adapters a headless render can open, best first, the one the policy picks and the tier it\n lands on: gpu, gpu-software, or the CPU rasterizer (auto with no adapter, or cpu); the policy defaults\n to SOLIDCRAFT_GPU, else auto; exits with an error when hardware or software finds no such adapter)\n";
/// The flags every command takes, at the end of the top-level usage.
constUSAGE_DEBUG: &str=" --debug, with any command: what SolidCraft logs (why a body has no display plan, why a save left its\n model cache out), on stderr; RUST_LOG=error|warn|info|debug|trace sets one level for every crate\n";
/// The commands the CLI takes, in the order the top-level usage lists them.
letr=json!({"ok": false,"running": true,"error": format!("timeout: `{method}` is still running in the app and will still apply; ask `control.pending` for its result")});
/// What the start after a lost device tells the user, `software` when it runs on the software
/// renderer.
pubfnnote(software: bool)-> String{
letmutnote=String::from(
"The graphics device was reset (the graphics driver stopped responding), so SolidCraft closed and started again. Unsaved changes were kept for recovery.",
);
ifsoftware{
note.push_str(" This start uses software rendering, which doesn't depend on the graphics driver.");
}
note
}
/// Start SolidCraft again with the same arguments, the loss stamped at `lost_at`, on the software
@@ -78,7 +78,7 @@ Its projected circle uses the same axes; do not arbitrarily flatten the ellipse.
Only use the stable `source` identifiers below in model SVGs. The renderer maps roles to
light/dark theme values. `currentColor` is the background line-art role, not a way
to add arbitrary colours. All values and accents come from `design/palette.toml`.
to add arbitrary colours. Default display values and semantic accents come from `design/palette.toml`. Theme files may override those display roles and ramps through `icon_palette`; source identifiers and accent assignments stay unchanged.
| Role | Author helper | Light value | Use |
| --- | --- | --- | --- |
@@ -96,9 +96,9 @@ The source hexes identify paint roles, not the displayed colour. **Never recolou
SVG to apply a workspace accent.** Keep these identifiers and every finished shape
unchanged; the runtime and review page resolve them through `design/palette.toml`.
That file is the single colour authority: edit `light`/`dark` in the relevant
That file is the default colour authority: edit `light`/`dark` in the relevant
`accents` ramp; `active-3` is its primary stroke and `active-1/2` are shaded faces.
Do not edit a role's `source` identifier. `variants` holds colour-vision adjustments.
Do not edit a role's `source` identifier. Theme `icon_palette` overrides affect display colours only; colour-vision variants still take precedence.`variants` holds colour-vision adjustments.
A role with `use = "active-3"` follows that stroke automatically.
| Meaning | Accent | Assignment |
@@ -207,3 +207,9 @@ All artwork must be original SolidCraft work or an explicitly licensed resource
with provenance. Commercial CAD artwork, screenshots and extracted assets are
forbidden even as hidden references. This guide and the local finished set provide
all required visual context for future agents.
Runtime discovery is also checked by `crates/ui-egui/tests/icon_audit.rs`. New menu
surfaces must extend that Harness traversal. Regenerate the committed
`design/requested_icons.txt` with `UPDATE_ICON_MANIFEST=1 cargo test -p
solidcraft-ui-egui --test icon_audit`; review the sorted id/placeholder diff. The
review tool reads this fixture in addition to source scans, masters and aliases.
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.