Commit Graph

1880 Commits

Author SHA1 Message Date
bherbruck 775e5155bb Main green: revert the start page change (#4295) and fix the format 28 test (#4335)
* 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>
2026-10-21 10:52:08 -04:00
bherbruck e570c0b4eb Windows: a restored window that is out of reach is fitted to the monitor's work area (#4299)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 09:36:36 -04:00
bherbruck d9059d8da5 Start page: sample pictures and the existence check of recent files run off the window thread (#4295)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 09:36:27 -04:00
bherbruck 463cea0615 Windows: ask for the foreground once, when the first frame is on screen (#4284)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 09:17:10 -04:00
bherbruck 0225e4d2a5 Startup: the first frame no longer waits for the system CJK/Hangul/Thai font search; SOLIDCRAFT_PROFILE_STARTUP prints startup steps (#4283)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 09:17:02 -04:00
bherbruck 034aa60d47 Release 0.18.3 (#4270)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 08:46:14 -04:00
bherbruck 971b223246 Thread: turn a shaft thicker than a modelled thread down to its major diameter (#4256)
* 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>
2026-10-21 08:27:18 -04:00
bherbruck 8c861f9f46 Face names: pieces of a split face keep their numbers across evaluations (#3902) (#3991)
* 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>
2026-10-21 08:08:08 -04:00
bherbruck e04a016e7c Align takes planes and the origin (#4248)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 08:06:32 -04:00
Irakli Kourtis 93a2922c5e Display plans and the model cache for large STEP assemblies (#2814) (#4021)
* 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>
2026-10-21 10:37:12 +00:00
Irakli Kourtis ff8b09c225 Kernel paused: label new kernel issues, PRs and discussions for a maintainer to answer (#3994)
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>
2026-10-21 10:03:47 +00:00
raykholo 975a08c992 ui-egui tests: hold OK's evaluation where a test checks it is computing (#4040)
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.
2026-10-21 09:38:14 +00:00
bherbruck 55587f4ac4 View keys: a held arrow turns the view at a steady rate, one tween at most (#4090)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
v0.18.2-nightly.20261021.63.1
2026-10-21 04:22:26 -04:00
bherbruck e94900933d IGES import: close small gaps between trim pieces (#3845) (#3846)
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>
2026-10-21 02:20:02 +00:00
SS-EN 776541b081 Agent commands compute off the window thread, so the window stays live (opt-in) (#1989) (#3340)
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>
2026-10-21 00:58:56 +00:00
raykholo 79a843c963 Transform: refuse a matrix that is not an affine placement (#3900)
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.
2026-10-21 00:58:49 +00:00
AlF 1bcc3602b4 STEP export: a merged cylinder or cone face's loop starts on the side its angle grows from (#3394) (#3557)
* 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>
2026-10-21 00:58:47 +00:00
raykholo 93684a4728 Expressions: a leading decimal comma such as ,5 is accepted (#2966) (#3797)
* 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.
2026-10-21 00:58:44 +00:00
raykholo 9db39f3d7d Holes from a sketch on a body's face drill that body (#2882) (#3770)
* 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.
2026-10-21 00:58:43 +00:00
Colin Faulkingham 29369a0bb5 Extrude to an edge or corner ends at the requested plane (#3729) (#3748)
* claim: #3729

* Fix extrusion targets at planar face boundaries (#3729)
2026-10-21 00:58:41 +00:00
kezmdor 5a9e771cc9 io: the 3MF and DXF budget tests do not check wall-clock time (#2998) (#2999)
* 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>
2026-10-21 00:58:39 +00:00
NOWN-DEV 63cb48a4ee Measure: a body whose meshes leave a gap keeps its volume wherever it is (#2069)
* 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>
2026-10-21 00:57:03 +00:00
Colin Faulkingham 36c732ddb7 Shell: show the tangent-chain face selection and explain full-body removal (#209) (#3809)
* claim: #209

* Shell: show tangent-chain faces and explain full-body removal (#209)
2026-10-21 00:54:34 +00:00
Mateusz Sych fdec948abc Control channel: control.running lists what the app is running, across connections (#3654) (#3689)
* 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>
2026-10-21 00:54:33 +00:00
bherbruck 7803b3968f Fix main: a test built Applying without the new seen field (#4001)
#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>
2026-10-20 23:41:24 -04:00
JEREMY cbac4eab3f OK off the window thread: a job that has just started waits a frame (#2929) (#3609)
* 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>
2026-10-20 23:02:34 -04:00
bherbruck 08eff4f9dd Sketch on a face: project from the model's own body, not a copy that meshes it again (#3981)
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>
2026-10-20 23:02:22 -04:00
raykholo 9b80163421 Extrudes from a nested STEP part's face join and cut it with the imported root active (#3223, #2872) (#3852)
* 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.
2026-10-21 00:54:16 +00:00
Tyler Chiriboga 164554edba 3MF import: production extension review follow-ups (#2089) (#3628)
* 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>
2026-10-21 00:54:14 +00:00
Matthew Knutson 453c6bcb14 Split schema contracts per command file and add shared field types (#2829) (#3558)
* 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>
2026-10-21 00:54:13 +00:00
phanoob f44ff18ca4 Split Body: each piece can be renamed on its own (#3510) (#3527)
* 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>
2026-10-21 00:54:11 +00:00
raykholo 98cd67f029 Joins, cuts and intersects meet their tools where they are shown, across placed components (#3383) (#3384)
* 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.
2026-10-21 00:54:09 +00:00
raykholo d9d6af51c6 Sheet metal: mirrored and copied sheet bodies stay sheet metal (#3353) (#3354)
* 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.
2026-10-21 00:54:07 +00:00
Christian Svensson 38c710e72e Save designs with large STEP imports: compress the STEP part (#3221) (#3285)
* 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.
2026-10-21 00:54:05 +00:00
raykholo 5b5b051e02 Delete in Edit Joint deletes that joint, and a refused joint delete says why (#3749) (#3776)
* 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.
2026-10-21 00:49:50 +00:00
kaltenbrunnermarkus 686576e368 Section Analysis: Edit reopens the dialog where the section was left (#3617)
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>
2026-10-21 00:49:48 +00:00
rlartot 68e4186080 fix(ai-chat): replay with reasoning_effort none when OpenAI refuses tools (#3798) (#3817)
* 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>
2026-10-21 00:44:52 +00:00
Colin Faulkingham b66ad0a9c9 Move shortcut respects the selected component (#3580) (#3588)
* claim: #3580

* fix(ui): move the selected component from its shortcut (#3580)
2026-10-21 00:44:48 +00:00
amolinari-autotex 2eacbc37f4 Keep sketches out of the Browser Features folder (#3584) 2026-10-21 00:44:43 +00:00
phanoob f2f9c6fe27 Settings › AI: Add agent opens from the Home tab too (#3483, #3513) (#3522)
* 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>
2026-10-21 00:44:41 +00:00
SS-EN d21e2b1afd Keep picked points when a type-first dimension turns horizontal or vertical (#3168)
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>
2026-10-21 00:44:40 +00:00
bherbruck 57bcc40838 Release 0.18.2 (#3970)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 22:08:36 -04:00
bherbruck 9974413a5b Ribbon: Finish Sketch follows the last sketch panel (#3958)
* 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>
2026-10-20 22:08:14 -04:00
bherbruck 818af2b856 Icon audit: regenerate the requested-icons manifest (#3969)
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>
2026-10-20 22:08:10 -04:00
bherbruck 80238a7cfb Sketch: a quick double Esc finishes the sketch (#3950)
* 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>
2026-10-20 21:57:32 -04:00
bherbruck bd65a561c8 Ribbon: New Sketch on the Surface tab (#3964)
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>
2026-10-20 21:57:10 -04:00
phanoob bdd2fb1a5c Fillet and chamfer previews keep moving while a radius or distance changes (#3150)
Co-authored-by: Phanoob <phanoob@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 00:44:38 +00:00
JZiegener e64f1e0427 Rejoin the split Files table in ATTRIBUTION.md (#3143)
A blank line left after the oracle rows ended the table, so GitHub
rendered the 339 rows after it as one paragraph of pipe-separated text.
2026-10-21 00:44:21 +00:00
CamWight ac362945cc English (UK) as a language: a plain catalogue of the 25 messages UK English spells differently (#2501) (#2502)
* 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>
2026-10-21 00:44:19 +00:00
JEREMY 382680d8e8 fix(skills): refresh Boolean checking facts that fail on current release (#1835) (#2093)
* 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>
2026-10-21 00:41:45 +00:00