Commit Graph
78 Commits
Author SHA1 Message Date
003abc2149 Issue forms and needs-info: ask short bug reports for steps to reproduce (#2416)
* Triage bot: needs-info for short bug reports without steps

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TPWBBSRPfPYWgrrhbznewR

* Issue forms for bugs and feature requests; document needs-info

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TPWBBSRPfPYWgrrhbznewR

---------

Co-authored-by: jyviko <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-18 05:44:48 -04:00
136a3e9a2f Report an Issue: open issues on tangency-labs/solidcraft (#2417)
Claude-Session: https://claude.ai/code/session_01TPWBBSRPfPYWgrrhbznewR

Co-authored-by: jyviko <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-18 05:44:23 -04:00
2e4266cb0a Triage bot and PR-based claims (#2397)
* Claim bot: a PR referencing an issue claims it

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TPWBBSRPfPYWgrrhbznewR

* Triage bot: label new issues and PRs, /triage, backfill

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TPWBBSRPfPYWgrrhbznewR

* Docs: a draft PR is the claim; labels and triage

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TPWBBSRPfPYWgrrhbznewR

* Triage bot: start a runner only for events it acts on

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TPWBBSRPfPYWgrrhbznewR

---------

Co-authored-by: jyviko <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-18 05:17:21 -04:00
83c31d77c4 surface.boundary: surfaces through boundary curves and curve networks (#763) (#2129)
* feat(geom): surfaces through curve networks, and spline spaces to build them

Adds spline_space (clamped B-spline spaces, Hermite interpolation, exact
embedding by Bézier degree elevation and knot insertion, a monotone cubic
for reparameterising) and network (loft, Coons and Gordon surfaces through
curves in one or two directions, with G1/G2 cross-boundary conditions
against a neighbouring face). Part of #763.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat(kernel): boundary surfaces from sketch curves, 3D curves and edges

kernel::boundary_network turns sketch chains, 3D sketch polylines and edge chains
into network curves, gives G1 frames of the faces along open edges
(EdgeFaces), builds the B-spline face (sides split at chain vertices so a
join shares the body's edges) and joins it into a surface body by stitching.
G2 is refused for now (not supported yet). Part of #763.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(kernel): second derivatives of B-spline surfaces in the vendored truck-geometry

BSplineSurface::uuder and vvder multiplied the second differences by p
instead of p·(p − 1), as BSplineCurve::der2 does, so every surface of
degree p ≥ 3 (NURBS too, through its homogeneous surface) reported
1/(p − 1) of its second derivative: a cubic surface's curvature read as
half. This is why a G2 boundary surface on a cylinder measured half the
curvature it carries (#763). New tests hold both to finite differences
for degrees 2 to 5.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM

* feat(kernel): G2 boundary surfaces against a body's face

The network always carried the face's curvature across a G2 edge; the
refusal came from measuring it with truck's halved surface second
derivatives (fixed in the previous commit). EdgeFaces now reads the
face's second fundamental form from the exact second derivatives
instead of differencing the normal. A new test holds the curvature
across a cylinder's edge to the cylinder's within 0.1%, also with a
sheared far curve; G3 is refused (a network holds up to G2).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM

* test(kernel): a boundary patch with straight corners measures its own area

A stadium's arcs meet its lines at 180°, so the Coons patch is singular
at its corners. From such a corner the reference mesher's boundary walk
ran onto the B-spline's extrapolation past its domain (a planar spline
passes through every point of its plane there), and the face measured
289 mm² for its 158.5 (#763). The vendored mesher's bounded projection
(nearest_projection, #2043) keeps the walk on the domain; this holds it
there, by the faces, the measuring mesh and the display mesh. The
display path (#1653) was never affected.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM

* feat(geom,kernel): three-sided boundary surfaces (one side collapsed to a point)

A network curve may be a PointCurve: the apex where the two sides beside
it meet. The curves through it cross it at the point itself, its row of
the net collapses there, and the kernel's face leaves that side out of
its loop (three edges, one vertex at the apex). One point per patch, as
a side; refused elsewhere, when it moves, or with a continuity
condition. Tests: a triangle and a quarter disc filled flat with their
analytic areas (40 mm², 25π), by geom and by the kernel's faces and
meshes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM

* feat(doc): boundary surfaces through curve networks and loops of three or four

FeatureKind::SurfaceNetwork saves a network as `u` and `v` curves, each
a sketch chain (planar or 3D), a chain of a body's edges with an
optional G1/G2 continuity, or a point (the apex of a three-sided
patch), and an `operation` that can join the surface into the body of
its edges. A SurfaceBoundary of three or four sketch curves, refused
until now, builds as the flat Coons patch of its loop (three collapse
the side opposite the first curve). The format change is one pending
file; feature-walk goldens record the new variant's sketches, bodies
and points.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM

* feat(engine): surface.boundary through loops of three or four and curve networks

The command keeps its loop form (two to four curves of one sketch; three
and four now build the flat patch of the loop, crown refused there) and
gains a network form: `u` and `v`, each curve a sketch curve id, a
{sketch?, curves} chain (planar or 3D), a {body, edges, continuity?}
edge chain (g1/g2 with the face beside it) or a {point} for a
three-sided patch, with `operation` new or join. Edits may move between
the two forms (timeline.redefine accepts the command now that it has a
dialog).

Tests: analytic areas of loop patches and of a hyperbolic paraboloid
bounded by four 3D lines (every mesh vertex on z = xy/10), a loft
across two sketches, G1 and G2 measured across a cylinder's edge with
inspect.continuity (angle < 0.1°; curvature jump 1e-16 with G2 against
0.027 with G1), 26 hostile networks refused without panicking, and u/v
added to hostile_params_never_panic.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM

* feat(ui): Boundary Surface dialog

Curves and Cross curves take sketch curves and edges (picks that meet
end to end in one sketch or body are one chain); Continuity and
Operation (New Body or Join). Two to four sketch curves of one sketch
with nothing across are the loop patch; anything else is a network.
Edit Feature brings either back (per-side orders, crown and stations
the dialog does not show are kept). Strings in en, ja and zh.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM

* docs: ROADMAP M8 and the bridge patch's notes for boundary networks (#763)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM

* test(oracle): a boundary surface through a loop, thickened (#763)

Case 83: four sketch curves (a square's three sides and a half circle
for its top) make one boundary surface, thickened 2 mm; analytic values
from tools/oracle-analytic/cases_surfaces.py (V = 2A, area 2A + 2P,
centroid by moments). Recipes gain boundary_surface, thicken and remove.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM

* refactor: boundary surfaces, review cleanups

Project onto the face once per derivative evaluation (the frame is
passed to natural() and reused by scale()), keep each track's
finite-difference step instead of rescanning its table, export the
kernel's edge-pick and chain caps and the default station count for
the engine, doc and dialog to share, map TooLarge spline errors to
KernelError::TooLarge, reuse the engine's operation parser, and skip
the dialog's sketch lookup when the picks are no loop.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM

---------

Co-authored-by: jyviko <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: bherbruck <[email protected]>
2026-10-17 21:47:38 -04:00
8c503ad1be Revert "CI: run untrusted and GitHub-hosted Linux jobs on Blacksmith's Ubuntu 24.04 runners (#2237)" (#2277)
Blacksmith isn't provisioning runners: its dashboard says the account
"requires additional verification", so every job labelled blacksmith-*
stays queued with no runner. Fork PRs' Linux jobs go back to the
self-hosted-untrusted pool (#2249/#2252) and the rest to GitHub's runners
until Blacksmith verifies the account; then #2237 can be re-applied.

This reverts commit dc40f051b1.


Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM

Co-authored-by: jyviko <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-17 20:23:57 -04:00
f7343e9161 CI: a main push the merge queue already tested runs only repository checks (#1860)
The merge queue runs the full suite on exactly the commit it then puts on
main, and main's push run then ran the full suite again on the same files.
Select checks now asks the Actions API whether a passing merge_group CI run
exists for the pushed commit; if so the push runs only the repository checks.
A push that skipped the queue, or an API that cannot answer, still runs
everything. The nightly still runs the whole suite.

Pull requests also run less: the reference tests only when solidcraft-cli (or
anything it depends on) is affected, and doc tests only for the affected
packages that have a library. The ci check requires the reference tests only
when they were selected.


Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM

Co-authored-by: jyviko <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-17 10:21:08 -04:00
Irakli Kourtis c4d2ae87ee deps: zip 3.0.0 → 9.0.4 (RUSTSEC-2026-0367, -0368, -0369) (#1971)
Thanks @jyviko.
2026-10-17 08:49:02 -04:00
Irakli Kourtis 0a66697da8 Continuity Analysis tool, legend and theme colours on #1642's inspect.continuity; curvature combs on faces (#770) (#1247)
Thanks @jyviko.
2026-10-17 08:38:55 -04:00
Irakli Kourtis 98ce6a4ee8 Sweep guide rails and two-rail sweep (#771) (#1806)
Thanks @jyviko.
2026-10-17 04:07:46 -04:00
Irakli Kourtis 7ba2d5db47 Robustness goldens: one test per seed, so CI spreads them (full runs ~33 → ~15-20 min) (#1851)
Thanks @jyviko.
2026-10-17 04:07:43 -04:00
Irakli Kourtis daf1dc4201 CI: Windows jobs only when what they test changes; only main saves caches (#1840)
Thanks @jyviko.
2026-10-17 04:07:36 -04:00
Irakli Kourtis 9de0b9d1f1 Merge pull request #1839 from jyviko/fix/goldens-seed-55 2026-10-16 18:07:05 -04:00
Irakli Kourtis f57a62deb7 Intersection edge nodes advance along the curve at fine display qualities (#1615)
Thanks @jyviko.
2026-10-16 03:00:56 -04:00
Irakli Kourtis fac3f96bdd Patch: G1/G2 to the faces beside the loop, general and N-sided loops, points (#759) (#1521)
Thanks @jyviko.
2026-10-15 23:07:24 -04:00
Irakli Kourtis 1a0b33fcf3 Headless pictures draw on the GPU, with the viewport's pipelines (CPU rasterizer as last tier) (#1555)
Thanks @jyviko.
2026-10-15 22:19:26 -04:00
Irakli Kourtis 5fa8e7d60a Sweep: orientation, twist and end scale (#1269)
Part of #771 and #772. solid.sweep and surface.sweep take orientation (perpendicular, parallel or a direction), twist (up to 4 turns) and end scale (0.01 to 100), applied about the profile's normal through the path's start; default options run the old sweep, so existing sweeps are unchanged. New fields are optional, left out when unset and noted under format 21; twist and scale are model parameters. Both sweep dialogs get Orientation, Twist and End Scale rows; labels in every catalog. Checked on main + this branch: cargo check --workspace, kernel/engine/UI sweep tests, the doc suite and the robustness goldens.
2026-10-15 06:24:31 -04:00
Irakli Kourtis b4edf05373 Extend any surface: natural, tangent and curvature, up to a target (#760) (#1166)
Extend works on any surface: natural (continuing the face's own surface), tangent (G1 run-off) or curvature (G2 run-off), by a distance, up to a plane or face, or back to the surface's bounds, with neighbouring edges lengthened to meet. Untrim and Extend share face parameter-loop helpers. The new SurfaceExtend fields are noted under format 21. Closes #760.
2026-10-14 22:46:29 -04:00
Irakli Kourtis 2a4907884b Ruled Surface: strips off an edge chain, exact where possible (#762) (#1151)
Ruled Surface: strips off an edge chain in normal, tangent, angle or direction mode, exact planes/cylinders/cones where possible, mitred or round corners, stitched into one surface body. SurfaceRuled is noted under format 21. Closes #762.
2026-10-14 21:44:56 -04:00
Irakli Kourtis 16c49c3e86 Untrim: holes removed and a face restored to its natural bounds (#1150)
Adds surface.untrim (#761); the SurfaceUntrim feature is first saved in format 21.
2026-10-14 20:29:25 -04:00
Irakli Kourtis e3459edea8 Display tessellation: trimmed faces as four-sided patches (#303) (#1163)
Trimmed faces are cut into four-sided patches (squares around holes, butterflies for round faces, motorcycle traces from concave corners) and each is meshed as a Coons grid, with a cut grid as the fallback; cells are at most 2:1 along straight directions. Folded blends are refused rather than overlapped. The README lists display meshes and the gpu crate.
2026-10-14 15:01:05 -04:00
Irakli Kourtisandjyviko 587cf865b8 Display tessellation plans (4/4): one GPU dispatch for the scene, intersection edges by Newton (#806)
Packs the scene's planned bodies into one GPU upload and dispatch, and evaluates intersection edges by seeded Newton refinement from a fitted B-spline through exactly solved leader points, falling back to the previous evaluation near tangency. Intersection nodes carry their faces' parameters. This changes speed, not what the app draws.

Part 4 of 4 of #303.

Co-authored-by: jyviko <[email protected]>
2026-10-13 22:57:08 -04:00
Irakli Kourtisandjyviko c1c813e572 Thicken any surface, across sharp edges (#765) (#932)
Thicken now moves faces through the same mover as Offset Surface. Spheres, cones, tori and B-spline faces thicken as well as planes and cylinders, and a thickness that would fold a curved face over is refused. Where thickened faces meet at a sharp edge, a wall closes the gap and corners are closed with flat patches. Thickening into an inside corner is still refused with a clear message.

Closes #765.

Co-authored-by: jyviko <[email protected]>
2026-10-13 22:55:04 -04:00
Irakli Kourtisandjyviko 2b70ed7b72 Offset Surface: faces copied out at a distance, exact or fitted (#758) (#872)
Offsets picked faces along their normals into a new surface body: planes and cylinders move exactly, other faces get a fitted B-spline within tolerance, and offsets that would fold over are refused. Adds the SurfaceOffset feature (noted under format 18), the surface.offset engine command and an Offset Surface dialog.

Closes #758.

Co-authored-by: jyviko <[email protected]>
2026-10-13 22:34:16 -04:00
Irakli Kourtis d2e409f40c Surface walls named after their sketch curves (#757) (#850)
Each wall of a surface built from sketch curves (Surface Extrude, Revolve, Sweep and Loft) is now named after the curve it comes from, exactly rather than from geometry, so references to those faces survive edits.
2026-10-13 21:47:24 -04:00
Irakli Kourtis 897b7d93b5 Surface Loft: sheets through open or closed sections (#847)
Surface Loft lofts open or closed curve chains, one section per sketch in pick order, into surface bodies. Adds the SurfaceLoft feature (format 18 note), the surface.loft command, a dialog with Edit Feature support, and kernel and engine tests.

Part of #757.
2026-10-13 21:07:16 -04:00
Irakli Kourtis aba57f1ca2 Surface Sweep: sheets carried from open curves and profiles along a path (#843)
Surface Sweep carries open curves and closed profile boundaries along a sketch path into surface bodies, with smooth walls along curved point paths. Adds the SurfaceSweep feature (format 18 note), the surface.sweep command, a dialog with Edit Feature support, and kernel and engine tests.

Part of #757.
2026-10-13 20:48:30 -04:00
Irakli Kourtis 2eb292dd4a Surface Revolve: sheets turned from open curves and profiles (#824)
Surface Revolve turns open curves and closed profile boundaries about a sketch axis into surface bodies, with cone apexes and sphere poles where a chain ends on the axis, and a closed shell made solid. Adds the SurfaceRevolve feature (format 18 note), the surface.revolve command, a dialog with Edit Feature support and analytic kernel and engine tests.

Part of #757.
2026-10-13 20:30:41 -04:00
Irakli Kourtis b52d4390a0 face.delete heal: false leaves an open surface body (#861)
Delete Face takes heal (default true, as before). With heal: false the picked faces are removed and the gap stays open, leaving the remaining faces as a surface body (kernel remove_faces); deleting every face is refused. Saved only when false, noted under format 18.
2026-10-13 19:53:17 -04:00
Irakli Kourtis b69cd2d0b7 feat: add and remove fillet spline points; handles on every edge (#855)
Double-click a fillet's radius curve to add a control point, or a handle to remove one; a spline fillet shows handles on all its edges.
2026-10-13 16:45:42 -04:00
Irakli Kourtis 753b0a2eab Unstitch and Reverse Normal (#856)
Unstitch takes a body's faces out as surface bodies; Reverse Normal turns a surface body's faces over. Both are saved in format 18. Closes #766.
2026-10-13 16:45:31 -04:00
Irakli Kourtis 5d661fa2af Export 3MF with B-rep face tags (#834)
3MF export carries each triangle's B-rep face tag.
2026-10-13 16:29:09 -04:00
Irakli Kourtis ff7a347013 Display tessellation plans (3/4): display meshes from plans, GPU evaluation in the viewport (#805)
Display meshes are built from tessellation plans, with GPU evaluation in the viewport.
2026-10-13 16:27:16 -04:00
Irakli KourtisandClaude Opus 5.5 e540832626 Surface Extrude: sheets from open curves and profile walls (#815)
surface.extrude sweeps open curve chains and profile boundaries along the sketch normal into a surface body (walls only, no caps). Saved under format 17.

Refs #757

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 14:54:38 -04:00
Irakli KourtisandClaude Opus 5.5 8dd583930e Display tessellation plans (2/4): size rules and the facet angle (#803)
Per-face target side lengths, the caller's facet angle, refined grid lines, and fixes for underflowing coordinates and straight edges ending exactly on their vertex. Nothing drawn changes yet.

Refs #303

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 14:45:54 -04:00
Irakli KourtisandClaude Opus 5.5 17b4b9a32c Reference case for a fillet with a B-spline radius (#810)
Adds oracle case 81 (a box edge rounded at a clamped cubic B-spline radius) with analytic expected values, and passes spline fillets through recipes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 14:28:49 -04:00
Irakli KourtisandClaude Opus 5.5 c8d668108a Surface toolset: icons and planned commands (#808)
Icons for the surface toolset, and its planned commands registered and disabled with the issue that tracks each one.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 14:28:38 -04:00
Irakli KourtisandClaude Opus 5.5 bfbf5f9432 Display tessellation plans (1/4): planner with shared edge nodes (#802)
Adds TessPlan and the kernel planner (shared edge nodes, structured and Coons grids, constrained Delaunay for trimmed faces) behind Body::display_plan. Nothing drawn changes yet.

Refs #303

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 14:27:44 -04:00
Irakli Kourtis 5267c96ab2 feat: drag a fillet's radius spline by its handles on the edge (#752)
A spline fillet's radius can now be dragged by handles on the edge, and the spline radius saves under format 17.
2026-10-13 12:35:42 -04:00
Irakli Kourtis 8cdefd4cd5 test: reference cases for variable, chord-length and tapered fillets (#776)
Four analytic reference cases for variable, to-zero, chord-length and taper-to-fit fillets, with the formulas in tools/oracle-analytic.
2026-10-13 12:35:16 -04:00
Irakli Kourtis 25a3dc501b docs(roadmap): list the proposed surface toolset under M8 (#773)
List the proposed surface toolset under M8 in the roadmap and link the scope discussion.
2026-10-13 12:35:02 -04:00
Irakli Kourtis 1d30e4e1d2 feat: fillet radius tapers along an edge to the room there (#749)
Adds `fit: taper` to Fillet: where a constant radius doesn't fit along a straight edge, it follows the room there as a smooth spline, down to zero where a face closes to a point. Taper to Fit is in the Fillet dialog, and the feature's warning says how far it tapered.
2026-10-13 11:45:33 -04:00
Irakli Kourtis eac4d66adc feat: variable fillet radius may run down to zero at one end (#675)
A variable fillet's radius may run down to zero at one end, where the round closes to the corner.
2026-10-13 10:17:55 -04:00
Irakli Kourtis 0f0bb09858 feat: fit reduce lowers the radius only where it doesn't fit (#496)
Fillet fit: reduce lowers the radius only where it doesn't fit: picked edges are grouped where they can limit each other (sharing a face or vertex), and each group gets the requested radius where it fits, otherwise the largest that does. Refs #293.

Co-authored-by: Iraklis Kourtis <[email protected]>
2026-10-13 05:27:03 -04:00
Irakli KourtisandClaude Opus 5.5 aa156f4a9f fix: report every fillet refusal of a radius too big as too large (#477)
`fit: reduce` acts only on refusals marked too large. Three kinds weren't:
the corner build's contact lines leaving their faces or running backwards,
and a curved chain's contact reaching past the end of the seam it trims. Two
more were hidden: a later edge's too-large refusal lost to an earlier edge's
other error, and the corner build's lost to the local blend's in `fillet`.
The messages are unchanged.

Refs #293

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 21:49:00 -04:00
Irakli KourtisandClaude Opus 5.5 ccc36dc1f9 fix: shell a body whose curved edge is rounded smaller than the wall (#476)
Moved in by more than its radius, a round collapses and its neighbours meet
along a new edge. That edge was always built as a straight line keyed by its
ends, so on a disc whose circular edge is rounded the four arcs where the floor
meets the side became one chord used by four faces, and the shell failed with
"This shell is not oriented and closed" (#319).

The new edge now runs where the edge's face meets the face across the round
(the round's far side, crossed as a strip), through the edge's middle moved
onto both: a line or a circular arc, checked to lie on both moved faces, and
refused otherwise. Twins are the new edges with the same ends through the same
middle.

Fixes #319

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 21:48:27 -04:00
Irakli KourtisandClaude Opus 5.5 201e9d5f9f Patch fills non-planar edge loops with one smooth face (#250) (#472)
* Add a one-surface fill of closed 3D loops to geom

A closed loop of curves that does not lie in a plane has no exact surface;
the usual answer is one smooth face fitted over it. fill_loop does that:

- the loop's best-fit plane (Newell normal), with a frame chosen from the
  loop itself so the result does not depend on where the loop starts, its
  direction, or where the part sits
- the loop seen along that normal must be simple and must not fold or run
  along the normal; otherwise it is refused ("not supported yet"), never
  filled with wrong geometry
- one non-rational bicubic B-spline height field over the loop's box,
  fitted by least squares to the loop's samples (The NURBS Book 9.4.1,
  9.4.3, eq. 9.62/9.65) with a thin-plate smoothness term
  lambda*A*int(h_uu^2 + 2 h_uv^2 + h_vv^2), A one grid cell's area, so the
  fit is the same at any scale; only planes are reproduced exactly
- span counts per direction (long narrow loops fit), refined until the
  loop is within tolerance, else refused with the deviation reached

bspline: bisected span search, basis functions on a stack array, one
clamping helper shared by curves and surfaces, one binomial helper.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8

* Patch fills non-planar loops of edges with one smooth face (#250)

surface.patch with edges refused any loop that is not planar ("not
supported yet: patching a loop that is not planar"). It now fills it:

- planar loops still get one exact planar face, but planarity is certified
  conservatively: lines by their ends, B-spline/NURBS edges by their control
  points, intersection curves by their samples lying on a support plane
  within tolerance; a loop that can't be certified is filled instead
- other loops get one face from geom's fill_loop, bounded by the body's own
  chained edges, whole (no split): its history is one generated face whose
  edges are the body's, and it stitches back into a closed solid
- intersection-curve edges are sampled by a seeded, guarded double
  projection shared by the planarity test and the fill (plane_fit)
- the face tolerance is 10x the planarity tolerance (documented: later
  operations take the face to contain its edges within it)
- edge picking runs inside the guard; the geom->truck conversion is shared
  with the T-spline sheets (spline_conv); the Newell plane fit is shared with
  STEP import

Tests: 3- to 8-sided, concave (dart, arrow, plus), flat-corner, two-edge and
imported rims fill as one face and stitch closed (volumes against an
independent integral); finite mesh normals, same result for any pick order
or placement; non-height-field loops and too-fine detail are refused.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8

* Test the Patch dialog filling a non-planar loop of picked edges

The Patch dialog's only test used a sketch profile. Picking the four
curved rim edges of a box cut by a sideways cylinder now makes one
surface.patch command whose result is a single smooth face over the rim.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 21:37:01 -04:00
Irakli KourtisandClaude Opus 5.5 49956c2797 Sketch on a cylinder primitive's caps; format 15 (#460)
* Sketch on a cylinder primitive's caps (#420)

A solid.cylinder's caps carried no construction certificate, so a sketch
on its top or bottom was refused unless fixed:true (#420's crossed
cylinders, built from primitives).

- kernel: FaceSource::CylinderCap { producer, high, plane };
  Body::with_cylinder_certificate turns a cylinder's two extrude caps into
  certified caps. kernel::cylinder's output is unchanged for its other
  callers.
- doc: FaceCertificate::CylinderCap { high }, named F<id>:top / F<id>:bottom
  as the cylinder's face names already are; New Body and Join cylinders
  are certified, shells carry it as their planar source.
- format 15: designs can store the new certificate kind; version 14
  designs need no change, and older builds ask for an update instead of
  meeting an unknown kind.

Tests: top and bottom attachments follow a height and base edit and a
reopen; crossed joined cylinders keep both caps attachable; a shelled
cylinder's floor keeps its cap identity; malformed certificate JSON is
refused.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YDDPmNdRoBgkHcLeUDKVfb

* Format 15 also covers sketch_axis links (#448)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YDDPmNdRoBgkHcLeUDKVfb

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 21:32:07 -04:00
Irakli KourtisandClaude Opus 5.5 1ae6e9beb4 Ctrl+left drag on empty space orbits in every navigation preset (#471)
A left press with Ctrl (or Cmd) held, no Shift or Alt, on empty space orbits
for that press, unless the preset binds Ctrl+left itself. It only orbits
once the pointer moves: a Ctrl+click still adds to the selection, and
Ctrl+drag on sketch geometry still drags it without snapping. Works with a
tool active, without adding geometry. Ctrl+box selection from empty space
becomes the orbit; Shift+box selection still adds.

Preferences › Navigation and the UI tour mention it. Test drives every
preset through real pointer events.


Claude-Session: https://claude.ai/code/session_01YDDPmNdRoBgkHcLeUDKVfb

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 21:27:20 -04:00
Irakli KourtisandClaude Opus 5.5 fc1d70cc3b docs: describe the sketch solver as it is (#475)
The README called the solver damped Gauss–Newton. It now places what the
constraints fix by construction (loci intersections and rigid-cluster
decomposition, after Owen 1991 and Fudos–Hoffmann 1997) and runs
Levenberg–Marquardt on the rest, with the rank of its Jacobian giving the
remaining degrees of freedom. The constraint list also gains the ones it was
missing (point on curve, curvature, point-to-line, arc sweep, arc length, slot
length), and live drags are described.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 21:26:54 -04:00
Irakli KourtisandClaude Opus 5.5 0dee9deec1 Sketch constraints accept x / y for the sketch's own axes (#394) (#448)
`sketch.constraint.symmetry` (line), `parallel`, `perpendicular`,
`collinear`, `tangent` and `coincident` (point on curve) now take `x` /
`y` for the sketch's own axes, matching what `solid.revolve` means by
lowercase x|y. A real curve with that id still wins.

An axis becomes a linked construction line on it: a new
`LinkSource::SketchAxis` resolved in sketch coordinates, so it stays on
the sketch's axis on any plane, is shared by every constraint on that
axis, is left out of profiles and is reported in `created`. Equal,
concentric, midpoint and horizontal/vertical keep refusing axes, since
the axis line's finite length or fixed direction would make them
meaningless.


Claude-Session: https://claude.ai/code/session_01YDDPmNdRoBgkHcLeUDKVfb

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 20:31:35 -04:00
Irakli KourtisandClaude Opus 5.5 722bd007c8 Join keeps exact face attachments on faces it leaves unchanged (#420) (#453)
A Union dropped every construction certificate, so after any join no face
could take a durable sketch: sketch.create failed with "this face has no
exact planar source" even on a face the join never touched (#420, and the
"CREATE SKETCH: operation failed" reports #344, #195).

- kernel: map planar sources through a Union from both operands, with the
  same exact-support rule Cut uses. Coplanar supports of the two operands
  carry distinct identities, so a face merged from both stays without one.
- doc: Join extrude and Join box tools are certified like new bodies; a
  cap moved off its designed height (into the target, or past a curved To
  face) is not certified. Join producers are accepted when certifying.

Tests: both operands' faces attach and follow an upstream edit and a
reopen; a merged coplanar face is refused and fixed:true works; a face a
join covers whole is lost without moving to the new roof. Tests that used
a joined face as their "uncertified" example now use a merged face; the
trimmed-face case in a_sketch_on_a_face_stays_on_that_face now stays
attached to the surviving piece.


Claude-Session: https://claude.ai/code/session_01YDDPmNdRoBgkHcLeUDKVfb

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 20:31:32 -04:00
Irakli KourtisandClaude Opus 5.5 01bf0e4eaf perf: pattern joins skip pushing apart faces that don't meet (#437)
Pattern copies of a post joined to a body already holding the original post had
their tops in its plane, so every join first pushed faces apart (about 190 ms,
then discarded). Faces now coincide only where their boxes meet, the push-apart
retry runs once per boolean instead of twice, and splitting prism outlines skips
the sampled arc projection for points off the arc's circle.
Sensor enclosure evaluation: 3.07 s -> 1.63 s, same bodies.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 19:35:43 -04:00
deb96318a1 fix: fillet fit review follow-ups (#404)
* fix: fillet fit review follow-ups

From the review of #388 (merged before these landed):
- The Fillet dialog's Applied Radius showed nothing when editing a fillet
  that was already reduced: the preview found the radius by diffing the
  states before and after. The engine's preview now keeps the command
  replies, and the dialog reads the radius of the feature they name.
- Pattern copies of a reduced fillet try the radius the original was
  reduced to first, instead of each searching again (up to 16 fillets).
- fit: reduce with a chord or variable radius is refused instead of being
  silently ignored.
- One helper appends a note to a feature's warning; too_large is imported
  like unsupported in the loop and curve blends.

Refs #293

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TWc1smHYwTSVrVc8SNvS44

* Use the solidcraft_doc crate name after the rename

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01HWLPLy7ug7URSK5WybjRNa

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: bherbruck <[email protected]>
2026-10-12 18:57:09 -04:00
c62879b00d feat: fillet fit option reduces a radius too large for its edges (#388)
* feat(kernel): typed TooLarge refusal and fillet_max search

Blend refusals caused only by the size (larger than the neighbouring faces,
taller than the walls, longer than the next edge, larger than a corner
radius) now return KernelError::TooLarge, displayed as before. fillet_max
finds the largest radius below a request at which the fillet of the edges
works, to three significant digits, by building candidates; it is an explicit
call (up to 16 fillets), not run on every refusal.

A corner edge that doesn't run square to the edge is now reported as such
instead of as a blend larger than the neighbouring faces.

Refs #293

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TWc1smHYwTSVrVc8SNvS44

* feat: fillet fit option reduces a radius too large for its edges

solid.fillet takes fit: exact (the default, as before: a radius too large
for the geometry around the edges is refused) or reduce: the radius is
reduced to the largest that works for all the picked edges (kernel
fillet_max, three significant digits, every candidate built). The feature
stores the requested radius, so it refits on every evaluation; when it
reduces, the feature's warning says so and the command returns
radius_applied. Pattern copies of the fillet use the same rule. The file
format writes fit only when it is reduce.

The Fillet dialog gets a Radius Fit row (Exact / Reduce to Fit, constant
radius) and, when the preview reduced the radius, an Applied Radius line;
editing the feature keeps the choice.

One radius for all edges for now; per-edge radii and taper follow.

Refs #293

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TWc1smHYwTSVrVc8SNvS44

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: bherbruck <[email protected]>
2026-10-12 16:21:17 -04:00
Iraklis KourtisandClaude Opus 5.5 a5cc1243e3 fix(doc): a pattern repeats a fillet on the edges it found, not its picks (#363)
A rectangular or circular pattern of a fillet or chamfer repeated it at the
points picked when the feature was made. The feature itself finds its edges
by name on every evaluation, but a parameter that moves the edges (a taller
pocket) leaves the old points on other geometry, and the copies then
blended the wrong edges or failed ("the faces at the edge ends must be
planar"). The model state now records, per fillet or chamfer, the edges it
blended in this evaluation (part of the state's digest), and the pattern
repeats those.

Fixes #219


Claude-Session: https://claude.ai/code/session_01TWc1smHYwTSVrVc8SNvS44

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 14:58:52 -04:00
Iraklis KourtisandClaude Opus 5.5 afcec4d0bd fix(kernel): fillets that end in faces at an angle or across two faces (#362)
A straight-edge fillet required each end of the edge to meet exactly one more
face, square to the edge. Now an end may meet one planar face at any angle
(the round ends in it along an exact ellipse, the cross-section slid along
the edge onto the plane) or two planar faces meeting at the end vertex: the
round ends across both and cuts back the edge between them where its
cylinder crosses it (Vida, Martin and Varady 1994: trimlines joined across
existing faces). The cylinder is swept to cover both trimmed ends. Square
ends are built as before.

This rounds the mitres where two chamfers meet at a plate's corner, and a
concave edge where a post stands on a plate (the round's end caps extend
the post's end faces); both were refused.

Fixes #215


Claude-Session: https://claude.ai/code/session_01TWc1smHYwTSVrVc8SNvS44

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 14:56:00 -04:00
Iraklis KourtisandClaude Opus 5.5 a04ef4ca59 T-spline core library and sheet bodies (#250, step 1) (#337)
* Add B-spline basis, knot insertion and NURBS surfaces to geom

Freeform faces need local refinement and gap-free joins between patches
with different knot vectors, which tensor B-spline grids cannot give;
T-splines can, and the first user is an N-sided Boundary Fill of
non-planar loops. This lays the univariate groundwork they rest on, in
the L0 geometry crate so it stays independent of the B-rep kernel:

- Cox-de Boor basis of one function from its local knot vector, on
  half-open spans with a left-limit side for the domain end, with
  derivatives (NURBS Book eq. 2.5, 2.9)
- knot insertion into a single basis function (Sederberg et al. 2004
  section 3) and Bezier extraction of one function by repeated insertion
- Boehm knot insertion for curves (NURBS Book A5.1), de Casteljau
  evaluation and sub-segments
- a rational tensor NURBS surface with evaluation, knot insertion and
  clamping, and a rational Bezier patch type

All input is checked (finite, monotone knots, positive weights, size
caps) and returns SplineError; nothing indexes unchecked.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8

* Add T-spline surfaces: T-mesh, evaluation, AS check, refinement, extraction

Tensor B-spline faces refine only by whole rows and columns and cannot
join patches with different knot vectors without gaps. T-splines allow
T-junctions in the control net, which fixes both; the first user is an
N-sided Boundary Fill. This adds the maths only (no command, no change
in behaviour):

- T-mesh in index space with knot values per line, validated (outer
  rectangle, rectangular faces, no empty lines, size caps); local knot
  vectors read off rays from each vertex, rays leaving the mesh repeat
  the boundary (Sederberg et al. 2003 section 3)
- rational T-spline evaluation and first derivatives for odd bi-degree,
  with a complete (p-1)/2-line frame and the domain inside it
- T-junction extensions and the analysis-suitability check (Scott et al.
  2012 section 2)
- local refinement that keeps the surface exactly: requested knot lines
  are cut in, then every original blending function is carried as a
  record and split (violation 1) or the mesh gains edges (violations 2
  and 3) until each record is a blending function of the refined mesh
  (Sederberg et al. 2004 section 3); analysis-suitable input stays
  analysis-suitable by carrying conflicting T-junctions across faces
- NURBS <-> T-spline conversion for meshes without T-junctions, and
  Bezier extraction over the rectangles no blending-function knot line
  crosses

Measured: regular T-spline vs NURBS < 1e-12, round trip exact, partition
of unity with T-junctions < 1e-14, refinement deviation <= 1.4e-14 over
40 random refinements, Bezier patches vs surface < 1e-14.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8

* Convert T-splines to sewn NURBS sheet bodies in the kernel

A T-spline is only useful to the modeller once it is a B-rep face set.
tspline_sheet builds an open shell: a mesh without T-junctions becomes
one clamped NURBS face; otherwise each Bezier element is a face, and
sides are split at the T-points where neighbours' corners land, so all
faces share vertices and edges and the sheet is one oriented shell
(V - E + F = 1). Every truck call runs under guard. No command yet;
Boundary Fill will be the first caller.

Tests: one face for a regular surface, sewn Bezier faces with
T-junctions, tessellated points on the surface to ~1e-9 mm, and hostile
control nets (coincident, 1e300, needle-thin) never panic.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 13:46:22 -04:00
Iraklis KourtisandClaude Opus 5.5 8e87a37511 feat(sketch): constructive solving ahead of the numeric solver (#339)
Part 1 of splitting #251: the sketch crate only. A graph-constructive front
end (Owen 1991; Fudos & Hoffmann 1997) places what it can by ruler-and-compass
loci and cluster decomposition, Levenberg-Marquardt finishes and verifies the
rest, and the rank analysis stays authoritative for degrees of freedom. Solves
can follow a reference (the previous solution), so edits and drags keep their
branch instead of flipping.

New API: solve_with (hold, reference, analysis, budget) and
solve_after_value_edit; solve and solve_holding keep their signatures as
wrappers, so no caller changes here.

When the seed moves an arc's centre, the arc's unconstructed ends start carried
along (the arc translates) instead of left behind for LM to turn; found by the
reference test 20-slot-constrained.

Refs #203


Claude-Session: https://claude.ai/code/session_01TWc1smHYwTSVrVc8SNvS44

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 13:46:08 -04:00
Iraklis KourtisandClaude Opus 5.5 fe4d171e22 feat(sketch): live drag that keeps its solution, ended by sketch.drag_end (#341)
* feat(sketch): constructive solving ahead of the numeric solver

Part 1 of splitting #251: the sketch crate only. A graph-constructive front
end (Owen 1991; Fudos & Hoffmann 1997) places what it can by ruler-and-compass
loci and cluster decomposition, Levenberg-Marquardt finishes and verifies the
rest, and the rank analysis stays authoritative for degrees of freedom. Solves
can follow a reference (the previous solution), so edits and drags keep their
branch instead of flipping.

New API: solve_with (hold, reference, analysis, budget) and
solve_after_value_edit; solve and solve_holding keep their signatures as
wrappers, so no caller changes here.

When the seed moves an arc's centre, the arc's unconstructed ends start carried
along (the arc translates) instead of left behind for LM to turn; found by the
reference test 20-slot-constrained.

Refs #203

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TWc1smHYwTSVrVc8SNvS44

* feat(doc): keep a sketch's solved state across value edits

Part 2 of splitting #251 (builds on part 1, the sketch crate):
- Sketch edits solve for the document's values continuing from the stored
  solved state (Document::solve_sketch), so a dimension edit keeps the
  sketch's branch instead of re-solving from scratch.
- A value edit (parameters, a configuration, the units) that would make a
  sketch that solved before fail or turn over is refused with the
  conflicting constraints named; otherwise the solved sketches are written
  back into the document (Model::adopt_solved_sketches), which changes what
  saved designs store.
- Later features are keyed by a sketch's solved geometry (to a 1e-6 mm
  quantum), so setting a value back reuses their earlier results.
- Solved sketches in a model state are shared (Arc) between states.

Refs #203

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TWc1smHYwTSVrVc8SNvS44

* feat(sketch): live drag that keeps its solution, ended by sketch.drag_end

Part 3 of splitting #251 (builds on part 2):
- sketch.drag follows a long move in short held steps, so the geometry keeps
  its branch, and reports how far it got (reached, at).
- live: true runs one frame of an interactive drag: the sketch is solved
  once and shown, later features keep their last result (marked stale)
  until sketch.drag_end or any command that reads the model evaluates it
  (CommandSpec::live, reads_model, doc_only).
- sketch.drag_end adds the constraint the drag let go on and evaluates the
  model once.
- Undo and redo evaluate a new document once (execute does) instead of
  twice.
- The viewport drives drags through live frames and drag_end; dimension
  text drags start only off sketch geometry.

Refs #203

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TWc1smHYwTSVrVc8SNvS44

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 13:45:48 -04:00
c33fe37798 fix(kernel): loop fillet as tall as its wall uses the wall up (#318)
* fix(kernel): name the zero-thickness rim when a shell can't open beside a round

Shelling a body through a face that a convex round meets tangentially, with
the round at least as large as the thickness, puts the cavity wall through
the rim where the round meets the open face. Both shell strategies then fail
and the first one's generic error ("a corner whose faces don't meet the same
way after the move") was reported. The shell now says why and what to change.
Successful shells are unchanged.

Found by fuzz seed 33, which reaches this case now that a disc's two edges can
be rounded to half its thickness.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TWc1smHYwTSVrVc8SNvS44

* fix(kernel): loop fillet as tall as its wall uses the wall up

Rounding a loop with a radius equal to the height of its walls was refused
("the blend is taller than the walls"): r = 2 on the top edge of a 2 mm
disc failed while 1.999 worked. When every wall seam ends exactly where the
wall-side rail would, the walls are consumed: their far edges become the
blend's rails and the walls and seams are removed. Walls used up only in
part, and radii past the limit, are still refused.

Refs #190

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TWc1smHYwTSVrVc8SNvS44

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: bherbruck <[email protected]>
2026-10-12 12:17:07 -04:00
Iraklis KourtisandClaude Opus 5.5 a9efae864d fix(doc): designs saved before format 14 keep their piece names (#136)
Pieces a cut splits a body into are named after their faces since names
follow history (lineage::order_lumps), so they keep their body names when
an upstream edit moves the cut. The kernel's shell order named them
before, and the two orders can differ: a slot along X through a 40x30x10
block swaps Body1 and Body1 (1). A design saved by an older build stores
body names meaning the old order (fillet and chamfer bodies, targets,
participants, materials, appearances, joints), so it would act on the
other piece.

Each feature now says which order names its pieces
(Feature::shell_piece_order). The v13 to v14 step sets it on every saved
feature, so an older design evaluates as it was saved; features made from
now on, in new or old designs, name their pieces after their faces.

The corpus gains split-body-pieces, saved by v0.3.2: a fillet, a chamfer
and an appearance refer to the two pieces by body name. Without the
setting the two pieces swap names (the corpus test fails); with it they
stay as saved. Migration tests that compared an upgraded design with the
original now expect the setting on its features.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8
2026-10-12 11:52:53 -04:00
Iraklis KourtisandClaude Opus 5.5 cc668668ce refactor(doc): format 14 marks the fields that store names, by schema place
The v13 to v14 step walked the whole design and marked any JSON key named
face, face_name or face_names, so any future field with one of those names
would be marked too. It now marks the fields that store a face or edge name
where the schema puts them: each feature's face_names and fillets' and
chamfers' edge_names, a sketch feature's links, joint snaps on a face in
joints and named origins, and face appearances. Face and edge references
(PlaneRef::Face, GeoRef::Face/Edge) can sit in many features, so they are
still found anywhere in a feature, by their tag and their picked point.

The test fixtures now have the shape real files have, and a new test checks
that same-named keys elsewhere are left alone.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8
2026-10-12 11:52:53 -04:00
Iraklis KourtisandClaude Opus 5.5 cfdd8a533b test(doc): drop two timing assertions on when a source's mesh is built
The world-placement tests checked that the source body's mesh was built
before a placement adopted its names. Once names follow history they no
longer need the mesh, so that is a side effect's timing, not behaviour.
What the tests protect, a placement sharing its source's names, both
still assert through the public API: Arc::ptr_eq(face_names(placed),
face_names(source)).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8
2026-10-12 11:52:53 -04:00
Iraklis KourtisandClaude Opus 5.5 08cf526fe2 feat(doc): persistent names follow the kernel's exact face history (#136)
Why: every reference a user makes to a face or edge (fillet and chamfer
edges, sketches on faces, shell/draft/offset faces, joints, appearances)
is stored as a name and found again after any upstream edit. Those names
were re-derived by sampling points of each face and voting, and pieces
of a split face were numbered by where their centres lay. After an
upstream edit that splits, merges, mirrors or shrinks a face, a fillet
or sketch could silently re-attach to the wrong face, and nothing told
the user. The kernel now reports exact history for its operations, so
names follow it and fail loudly instead of guessing.

- crates/doc/src/lineage.rs: a feature's kernel histories composed into
  one lineage over nameable operands (bodies before the feature, moved
  copies, shell cavities, tools named by role: sketch curve, cap, box
  side, imported face). Instrumented: extrude/revolve/sweep/loft tools,
  join/cut/intersect, lumps after a cut, fillet/chamfer, shell, draft,
  split face/body, pattern/mirror (bodies and features), combine,
  boundary fill, move/scale, offset/delete face, thread, import.
- crates/doc/src/history_names.rs: names from lineage; split pieces told
  apart by the faces only they touch (F2:top#[F2:side:l4,F4:side:l4]),
  merged faces answer to every name (aliases), lump bodies ordered by
  their faces' names instead of shell order.
- crates/doc/src/name_lookup.rs, piece_names.rs: references resolve by
  exact name, alias or bounding faces, else nearest piece with a
  warning; names worked out from geometry (operations without history)
  are reported when only geometry decided.
- Format 14: names stored by older formats get a legacy mark and are
  found as the build that saved them found them (some geometric names
  name a face history names differently: a tube's outer wall was named
  after its inner circle). New corpus file saved by 9a979cf.
- kernel: Body::lumps carries each piece's history over the whole.
- Adversarial tests: split, merge, mirrored twins, shrink to a sliver,
  moving cut with body pieces, history-less bodies, certified sketch.
- docs/book/src/dev/naming.md describes the real mechanism and why.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8
2026-10-12 11:52:53 -04:00
Iraklis KourtisandClaude Opus 5.5 2bda050872 feat(kernel): bodies joined apart report their history (#136)
union_apart joins bodies whose boxes are clearly apart (many pattern tools,
#261) without any intersection work: their shells side by side. Each face
and edge of the result is its piece's own, unchanged, and the result now
says so (a history over the pieces), so a pattern's cut keeps the exact
names of every copy's faces.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8
2026-10-12 11:52:53 -04:00
Iraklis KourtisandClaude Opus 5.5 08d797235d feat(kernel): history across coalesced stepped walls (#136)
A cut of stepped prisms sews a wall over one segment through successive
slabs as one face, and the generated planar walls are then joined on
their common supports. Until now such a piece carried no history.

- prism_lineage: a sewn wall records what it is over in each slab it
  spans (Spec::Wall { levels, slabs }), so a coalesced wall comes from
  the operand walls under it in every slab
- the sewn piece's history is attached before the joining and carried
  through it (Built::Traced); planar_regularize composes the history of
  the body it joins with the join itself
- a composed face is Split only where record::mark_shared_split finds an
  origin under several faces; an inner Split alone no longer makes one
- Body::same ignores history: an operation that leaves a body as it is
  gives the same body, carrying that operation's history

The two boolean history tests that waited for this run again, and the
coalesced and regularized step cuts are covered.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8
2026-10-12 11:52:53 -04:00
Iraklis KourtisandClaude Opus 5.5 97bcdb4d06 feat(kernel): settle a slab's far cap edge by operand topology (#136)
The far edge of a stepped slab (the operand cap edge where a segment's
wall ends) was recorded by testing whether each slab segment runs along
an ending cap's segment in 2D, a tolerance comparison of coordinates.
Matching by topology makes the far edge exact by construction instead:
when the wall's top is the ending level, nothing crosses it there, and it
meets the caps at that level in exactly one edge, that edge is over every
segment beside the wall, with no position compared.

A vertex walk up the wall's side edges from the segment's ends was tried
first and does not settle the cases that need it: a notched or stepped
wall continues past a segment's end (no side edge rises from it), and
walls split by earlier stations have mid-chain vertices with no side edge.
Whenever the walk succeeds, the wall is a rectangle over the segment and
the one-edge rule above already holds.

The positional match remains only where topology cannot say which edge
is over a segment: a wall that meets the ending caps in several edges (a
top split at earlier station vertices, or beside a wall crossing it at
that level) or that carries on past the level elsewhere. It is documented
in `Section::mark_far` and its pair budget no longer stops topological
marking. Far edges are also only marked on the slab that actually ends at
the level (a gap level no longer revisits an earlier slab).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8
2026-10-12 11:52:53 -04:00
Iraklis KourtisandClaude Opus 5.5 688f806682 feat(kernel): exact chamfer history after pocket, step, slot and bite cuts
Chamfer is one of the most common features and is usually applied right
after a cut. A chamfer cuts with a prism cutter, and its history came out
None after four of the six common box cuts. Without exact history, any
reference to a chamfer face (or a face it trimmed) falls back to geometric
guessing in naming, which can silently re-attach to the wrong face.

Two gaps in the stepped-prism section tags caused it, both now closed by
bookkeeping at construction, never by sampling or geometric tie-breaks:

- Cap edge at the far end of a slab. A slab's section is read off the cap
  where it starts and carried up to the cap where it ends; that cap's
  outline can run along one wall in several collinear edges, so which one
  a result edge lies on was unknown. When the stack is read, each slab
  segment that runs end to end along one segment of an ending cap's
  outline (beside the same wall) records that cap edge as `far`; the
  lineage takes it when it bounds the wall and the result cap.

- Walls an earlier cut left in coplanar pieces. A segment carried through
  a level kept the wall it was read beside, even where that wall gives
  way to another piece at the level (or within the slab), so the tag named
  the wrong face. `Stations` records the operand's edges across walls;
  at each level a carried segment keeps its wall when nothing crosses it
  there, moves to the one wall across it when it ends there, and is
  otherwise unknown (no history). A wall crossed inside a slab is unknown
  too, for simple and stepped prisms alike.

chamfer_after_box_cuts now covers all six cuts (notch, pocket, step,
slot, hole, bite), each verified against geometry and composed through
the cut over [block, tool]. The layered partial-depth cut test now
verifies its second cut, whose history was None before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8
2026-10-12 11:52:53 -04:00
Iraklis KourtisandClaude Opus 5.5 43ddde4a5d feat(kernel): an edge's meeting sides are sets of origins (merged faces)
EdgeOrigin::Meet now names each side as a Side: the sorted, de-duplicated
origins of the face there. substitute maps a side to the union of the inner
faces' origins, so a meeting with a face an inner operation merged keeps its
lineage; meeting() derives sides from whole faces. Fillets after box cuts
that leave coplanar pieces now report exact history. Prism lineage meets a
cap of several origins and resolves wall junctions away from shared
vertices by the operand's one edge between the walls. bore_union's recut
reports history only for a one-face bore (pieces of a two-arc bore's recut
wall were given both arcs).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8
2026-10-12 11:52:53 -04:00
Iraklis KourtisandClaude Opus 5.5 08d7037176 feat(kernel): modifiers report exact face and edge history
Fillets (local, loop, rolling ball, corner, variable, chord), chamfers
(cutters, local corners, loops, two sides), shell, draft, offset faces,
split face/body, delete face, patch/copy/stitch/trim/extend and
transforms record their history over [body] while they build: faces
rebuilt from the body's, blends in place of edges (corner patches in
place of the edges at the corner), shell walls as offsets, edges as
pieces of the body's or where two faces meet. Multi-step operations
compose each step. The push-face offset fallback now has history, and
the bore union composes its narrowed tool.

A reflection reorders a body's edges, so transforms now match edges by
identity (mirrored copies reported the wrong edges).

Refs #136

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8
2026-10-12 11:52:53 -04:00
Iraklis KourtisandClaude Opus 5.5 d5cb276965 feat(kernel): every boolean path reports exact face and edge history
ops::boolean's result carries its history over [a, b] whichever path made
it, recorded by the code that builds the faces:

- truck path (incl. jitter retries): shapeops history mapped through the
  order-preserving copies and the cut's turned tool, then through healing
- heal records which input faces each healed face merges and which input
  edges each joined or exact edge replaces (HealMap); opt-in, so heal_keep
  callers pay nothing
- trivial shortcuts, the void, glue, faces pushed apart (push_faces records
  its own history; the slab cut back on a union maps to the operands'
  faces in that plane), the nudged retry, by_lumps and join composition
- prism booleans: section segments carry tags (operand cap edge, the wall
  beside it, its vertices) through the 2D boolean; walls, caps and edges of
  the extruded or sewn result follow from them
- planar and faceted polygon booleans: polygons carry their operand face;
  faces join the pieces across edges, edges come from the pieces either side

History gains renumber/over (and consuming forms) for composition;
record gains regions/traced/build helpers. truck-shapeops
*_with_history now return the solid with an Option history. Paths that
cannot report (bore narrowing via offset, inexpressible lineage) leave the
result without history. Boolean results, tolerances and fallback order are
unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8
2026-10-12 11:52:53 -04:00
Iraklis KourtisandClaude Opus 5.5 d69ebbee1b refactor(kernel): number profile loops once and record face history as faces are built
One numbered loop type (crate::numbered) carries each segment's profile
numbers through orientation, snapping, circle re-seaming, re-sampling and
rotation; every builder orients its regions through it once. Builders that
make their own side faces pair each face with its provenance as it is made
(ruled walls by the wire edges each face holds, smooth lofts, coils and path
sweeps by segment), and cap_and_close appends the caps itself. Column
following is kept only for faces truck sweeps internally (rsweep, tsweep
along paths, cones, where an apex vertex stays put). Extrude derives face
sources and history in one topology pass. One attach entry point takes
operands with their edges already indexed.

Geometry, face order, face sources and recorded origins are unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8
2026-10-12 11:52:53 -04:00
Iraklis KourtisandClaude Opus 5.5 8c81bc60d0 feat(kernel): creation builders report exact face history
Extrude, revolve (partial and full turn), tapered extrude, loft (incl. to a
point), sweep, path sweep, coil, primitives, lip, emboss and thicken attach
the exact history of the bodies they build: each face's profile segment, cap
or operand face, and each edge's operand edge, recorded by topology identity
inside the building call. Nothing is sampled; a body whose faces do not follow
the expected construction carries no history. Face sources are unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8
2026-10-12 11:52:53 -04:00
Iraklis KourtisandClaude Opus 5.5 cc4955a8da feat(shapeops): face and edge history from booleans
and_with_history / or_with_history return, with the solid, the operand face
each result face is a region of (and whether it is that face untouched) and,
for each result edge, the operand edge it is (a piece of) or the pair of
operand faces whose intersection made it. Faces and edges are recorded where
they are created (face division, intersection edges, split edges, edges
rebuilt on a moved vertex), resolved to operand terms at creation, so the
passes folding in extra shells compose without bookkeeping. and / or wrap
the same pass and drop the history; a missing record never fails them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8
2026-10-12 11:52:53 -04:00
Iraklis KourtisandClaude Opus 5.5 c87efa1f35 feat(kernel): exact face and edge history model
Bodies can now carry the exact history of the operation that made them:
for each result face its origins among the operation's operands (kept,
modified, split, merged, or generated from a profile segment, cap, edge or
offset) and for each edge the faces it separates and the operand edge or
face pair it came from. History is recorded by the operation, never
sampled, and checked against the body when attached.

- history.rs: the vocabulary (moved from provenance.rs), History::check,
  identity, substitute (composes an operation with the one that made an
  operand, including bodies made from a sketch) and face-source propagation.
- One edge order for a body (body::edge_order), shared by tessellation and
  history, so history edge indices are the mesh's edge indices.
- Transforms report an identity history; placed_topology documents the
  face/edge order guarantee it relies on.
- provenance.rs keeps the geometric trace for operations that do not yet
  report history.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8
2026-10-12 11:52:53 -04:00
Iraklis KourtisandClaude Opus 5.5 a9e4830ded fix(kernel): fillet that uses up a side face makes a full round (#317)
A constant-radius fillet whose setback equals the width of a neighbouring
face was refused ("the blend is larger than the neighbouring faces"), so
r = 3 on the vertical edges of a 6 mm wide box failed while 2.999 worked.
The corner edge is now consumed: the rail ends at its far vertex and a side
face used up at both ends is removed, its remaining edge becoming the rail.
A face used up at one end only, and radii past the limit, are still refused.

Refs #199


Claude-Session: https://claude.ai/code/session_01TWc1smHYwTSVrVc8SNvS44

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 11:44:32 -04:00
Iraklis KourtisandClaude Opus 5.5 a3ff816176 fix(kernel): name the zero-thickness rim when a shell can't open beside a round (#316)
Shelling a body through a face that a convex round meets tangentially, with
the round at least as large as the thickness, puts the cavity wall through
the rim where the round meets the open face. Both shell strategies then fail
and the first one's generic error ("a corner whose faces don't meet the same
way after the move") was reported. The shell now says why and what to change.
Successful shells are unchanged.

Found by fuzz seed 33, which reaches this case now that a disc's two edges can
be rounded to half its thickness.


Claude-Session: https://claude.ai/code/session_01TWc1smHYwTSVrVc8SNvS44

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 11:44:27 -04:00
Iraklis KourtisandClaude Opus 5.5 8a38a7354c fix: join merges every body the tool meets
A Join unioned its tools into the first target only, leaving other target
bodies the tool overlapped or touched as separate, interpenetrating bodies.
Each tool now merges with every target it meets (shared material or face
contact, tested on the operands' boundaries) into one body under the first
one's name; absorbed bodies are removed and their threads follow. A tool that
meets no target still joins the first target as a separate lump. Sheet metal
and mesh bodies are never absorbed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8
2026-10-11 10:41:48 -04:00