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>
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>
(cherry picked from commit e88779218)
#2142 made orbiting about the point under the cursor turn the camera in place instead of turning the part about that point. Restore the 0.15.0 behaviour.
This reverts commit cac4bccea8.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
npm puts a Unix shell script with the bare name (codex-acp,
claude-code-acp, gemini) beside the .cmd shim. The lookup tried the bare
name first, so every npm-installed agent failed with "not a valid Win32
application (os error 193)". Windows now only considers .exe/.cmd/.bat,
and a command saved with the bare path runs the shim beside it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 60ea8c69d2)
On 32-bit Windows SetWindowLongPtrW is SetWindowLongW, which takes and
returns i32, so the window-procedure hook didn't compile for i686.
The macOS app bundle now signs the bundled solidcraft-cli before the
bundle itself; codesign refuses a bundle with an unsigned helper.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 94e76cd97e)
* Windows: start Direct3D 12 first, and Vulkan only without a D3D12 GPU
wgpu started Vulkan, D3D12 and OpenGL together and preferred Vulkan, so
every Windows start loaded all installed Vulkan drivers. An old Intel
Vulkan driver (igvk64.dll) crashed 0.10.1 at startup inside the driver.
The app now probes D3D12, then Vulkan, then OpenGL, one backend at a
time, and starts only the first with a hardware adapter (WARP when none
has one). WGPU_BACKEND still wins; graphics.json's "backend" sets the
order by hand; the software renderer (safe mode, the retry) uses D3D12
alone. A new Windows CI job starts the real app under several settings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* CI: a test filter YAML parses
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* CI: the startup smoke test writes graphics.json where the app reads it
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* CI: run the WARP shader tests one at a time
WARP's shader JIT (d3d10warp.dll) crashed twice in 124 runs of the gpu
tests with four test threads, with a null read inside its own optimizer
while several devices compiled compute pipelines at once. Serial runs
did not crash.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 95ea05e7c8)
* GPU tessellation: a pipeline the device cannot build falls back to CPU meshing instead of crashing (#1098)
Build the tessellation compute pipeline inside error scopes; TessGpu::new returns an error,
and the viewport logs it and meshes bodies on the CPU. A Windows CI job builds the pipeline
with D3D12's FXC on the WARP adapter.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Tessellation shader: basis functions in one-dimensional local arrays, so D3D12's FXC compiles it (#1098)
FXC has to unroll loops that write an array inside a struct (Basis.n0/n1) or a 2-D array
(ndu) by a counter, and cannot with run-time trip counts (X3511). A2.2 basis functions in a
local 1-D array returned whole; derivatives from the degree p-1 functions; the pole step
offsets computed rather than indexed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Since the model's mesh went back to the reference mesher (#940), the GPU viewport drew
opaque bodies from their display plans while hover and selection lit faces with the
reference mesh's triangles. Two triangulations of the same face at equal depth fought, so
a hovered face shimmered in wedges instead of lighting whole.
Faces lit over a body drawn from its plan now use the plan's own triangles
(ModelBody::plan_mesh, evaluated once on the CPU), and the highlight pass gets a small
depth bias toward the eye so f32 rounding between the GPU and CPU evaluations of the same
triangles cannot speckle it either.
Test: every face of a box with a pocket and a round boss, hovered and rendered offscreen
through the viewport's real pipelines on the GPU path, lights the same pixels as on the
CPU path (skipped without a GPU adapter).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 12:56:47 +00:00
935 changed files with 158910 additions and 20185 deletions
# 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 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. Pushes to main, merge
# groups, the nightly run and Run workflow are not pull requests and always run the full suite.
# 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.
# 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). For pushesto main and the merge queue only, the
# repository checks, Clippy, the doc tests, the reference tests and all four test shards run on the
# self-hosted runners (`self-hosted-fast`, warm build directories via scripts/ci/warm-target.sh);
# a pull request runs them on GitHub's runners, so it never holds up the queue. 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).
# Where the test shards run on GitHub's runners, the test archive is built there too: 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). The self-hosted shards
# (main and the merge queue) skip the archive and build the tests in their warm build directories.
# 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
# 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
@@ -746,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"
@@ -28,6 +28,13 @@ Editable drawing code, native projects and shipped exports are collected under `
| 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 |
| 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-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 |
@@ -45,8 +52,28 @@ 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/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 +94,10 @@ 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/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 |
@@ -88,21 +116,30 @@ 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/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 |
@@ -119,6 +156,20 @@ 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/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 |
@@ -127,6 +178,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 |
@@ -135,11 +190,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 |
@@ -220,7 +281,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 |
@@ -270,6 +331,17 @@ 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/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 |
@@ -321,7 +393,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 |
@@ -405,10 +477,12 @@ 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) |
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.
@@ -183,10 +192,12 @@ 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
@@ -197,7 +208,7 @@ 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 but the slowest, `SLOW_SEEDS` in `ci.yml`),
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`),

*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).*
## 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) |
| 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 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 |
| 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 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; 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 | — |
| 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); 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 | — |
@@ -70,7 +70,12 @@ pointer movement orbits without a mouse button, and vertical scrolling zooms. Ot
retain their saved two-finger scroll preference; camera motion is available through
`view.navigate` and modifier-aware `ui.move` automation.
M11 precursor: File → Export Current View as DXF exports shown placed bodies from the current
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
@@ -457,10 +490,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
@@ -11768,6 +11793,32 @@ The above copyright notice and this permission notice shall be included in all c
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
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")});
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.