Commit Graph
309 Commits
Author SHA1 Message Date
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
NOWN-DEVandClaude Opus 5.5 af0440aaab feat: sketches on a sheet metal face attach to its panel (#474)
A sketch on a flat face of a sheet metal body was refused ("this face has no exact planar
source"), so the only way was a plane frozen in space. When the sheet changes (a flange's
angle, the thickness), the frozen sketch stays behind: a cut from it fails on the slanted
flange or leaves a blind pocket in the thicker sheet.

`sketch.create` with a face of a sheet metal body now attaches the sketch to the panel the
face belongs to. It keeps the face's name, `sheet:<body>:<panel>:<top|bottom>` (the panel is
the base or a flange's feature tag), and takes the panel's exact plane from its flat region
and fold each time the model is evaluated, so the sketch follows the sheet through parameter
changes, unfold and refold. Its coordinates are the flat pattern's (the bottom face's y
mirrored). A face that is gone is reported as a lost attachment, never replaced by the
nearest face. The attachment follows a renamed or copied body, shows as `sheet_face` in the
command result and as its own status in Redefine Sketch Plane. `fixed: true` still freezes.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 21:36:58 -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
NOWN-DEVandClaude Opus 5.5 9ab25cfd4a fix: sketches on planes built from the model can project, intersect, include and hold text (#459)
Project, Intersect, Include 3D Geometry and Text resolved the active sketch's
plane without the model, so on a construction plane that is built from geometry
(perpendicular to a plane through a sketch line, through three points, midway
between two faces, tangent, along a path) they stopped with "this plane is built
from the model: evaluate first". Evaluation resolves the same plane against the
model before the sketch, in its component's frame; the sketch commands now do
the same, in one place, for adding a link, refreshing the links on an edit,
snapping a point onto an edge and the internal face profiles.

Tests: a sketch on a plane through a sloped sketch line projects a point and a
line of that sketch, follows a parameter that tilts the plane (also after reopen
and after another edit), holds text, intersects a body and includes an edge; the
same on a three-point plane and a midplane.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 21:27:28 -04:00
NOWN-DEVandClaude Opus 5.5 fb4c1d74b9 feat: edge flanges with their own angle and height per edge (#447)
One edge flange feature gave all its flanges one height and one angle. A
part whose flanges lean differently took one feature per angle, and
flanges of separate features don't close toward each other, so its
corners stayed open.

- sheet.flange (edge): an entry of `edges` is a point [x, y, z] as before,
  or {point, angle?, height?} whose angle and height (expressions, checked
  like the feature's) replace the feature's for that edge. Bad input is
  refused with the edge named. Before, the object form was read for its
  point and the rest was ignored without a word.
- The feature stores its edges as FlangeEdge. An edge without values of
  its own is written as the plain point it always was, so designs without
  them are read and written as before; an unknown key in an edge is
  refused, not dropped.
- The edges' values are feature inputs of their own (Edge angle, Edge
  height, after the feature's), so parameters, renames and
  parameters.change reach them. timeline.edit names the edges again when
  it changes them, so each value stays with its edge.
- SheetBody::add_flanges_each trims every edge by its own setback and
  adds the flanges. The corners between them close as between equal
  flanges: the closing already works from the two flanges it joins (each
  one's fold and leg), so the flat pattern follows by hand from the
  angles, the heights and the rule (the tests derive it).
- The Flange dialog shows a row per picked edge with an angle and a
  height box (empty: the dialog's value) once more than one edge is
  picked, and brings them back on Edit Feature.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 20:33:49 -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
MqriusandClaude Opus 5.5 7f92093cc8 fix(kernel): revolve with edges on the axis is built around the origin, then moved (#452)
A full revolve of a profile with more than one edge on the axis (a spherical shell's half ring:
both radial lines lie on it) took the generic sweep, built in place. Truck tessellates the
degenerate faces those edges sweep reliably only with the axis through the world origin, so on a
sketch plane off it the body measured about a third too large (a shell from r 18.5 to 40, 10 mm
above XY: 324 339 mm³ instead of 241 561), and booleans with it failed or removed the wrong side.

Such profiles are now swept about an axis through the origin and translated into place, as
revolve_touching_axis already does for profiles with one edge on the axis.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 20:25:46 -04:00
selune 006a23a767 Add design containers with deduplicated blobs and JSON compatibility (#327)
* Add fast design containers with lossless deduplicated payloads

* Keep display caching opt-in to avoid default save overhead
2026-10-12 19:15:41 -04:00
8e0c3d2794 feat: fold sheet metal cut-outs across bends at any angle or curved (#425)
A cut-out made in the flat pattern could cross a bend only with edges
running along the bend or square to it: a round hole or a slanted slot
over a bend line failed to fold ("not supported yet: curved cut-outs
across a bend", "... cut-out edges running across a bend at an angle").

Sheet: a flat edge in a bend zone now folds whichever way it runs: along
the bend to a line, across it to an arc, and any other way to a curve on
the bend's cylinder (a slanted line to a helix). The curve is carried as
a chain of cubic Hermite pieces fitted within 1e-7 mm on both faces of
the sheet, with breaks that depend on the flat edge only, so the top
face, the bottom face and the wall share them. The wall between the two
chains is a ruled surface with radial rulings: the cut-out's wall stays
square to the sheet. The bend's own faces stay exact cylinders, now sized
to the whole boundary of their cell (a curved edge may bulge past its
corners).

Kernel: such an edge has no line, arc or conic form, so the sewing
interface gets EdgeSpec::Chain (a chain of cubic Bézier pieces: a cubic
B-spline with triple inner knots, its ends on the shared vertices) and
SurfSpec::Ruled (two chains joined by straight lines, so a wall built
from the chains holds them exactly). The sheet module's rev_edge becomes
EdgeSpec::reversed, so the new variant is handled where edges are
reversed.

The test for "along" and "across" is in millimetres (1e-6 mm at the
edge's ends) instead of relative 1e-9, so coordinates rounded by a script
fold as drawn.

Tests. Kernel: a box with one wall bulging as a cubic (chains of two
pieces at the bottom and the top, the wall ruled between them) sews to a
valid solid of the analytic volume, 27.6 (measured 27.59965); chains with
the wrong number of control points, a NaN or too many pieces, and a ruled
wall between chains of different piece counts, are refused. Sheet: a slot
turned 30° and a round hole across 72°, 90° and 108° bends fold to valid
solids that lose the cut-out's flat area times the thickness (K = 0.5:
400 and 100.53 mm³, measured within 0.02); a slanted flat edge folds to
its exact helix within 1e-7 mm (measured 1.9e-8) on both faces; lines
along and across the bend keep their line and arc. Engine:
fold_through_cut_outs now expects a round hole across a fold to fold, and
to unfold to the same round hole; the per-bend machining guard (#296)
sews a bend that carries a round cut-out, so a hole beside it cuts and a
hole in the bend is still refused.

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: bherbruck <[email protected]>
2026-10-12 19:03:22 -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
MqriusandClaude Opus 5.5 1d7450b04c fix(doc): To Object takes the face at the pick, not the first body near it (#429)
object_height, curved_target and curved_target_any took the first solid body with any face
within its pick tolerance of the To Object point. That tolerance is 5 % of the body's size,
so a large body beside a small target took picks made on the target: a ridge extruded up
to a post 2 mm above a plate treated the plate as the target, nothing trimmed the tool, and
it ran past the post (to x 9.08 instead of -2.31).

They now share face_at: the nearest face over every solid body, still within that body's
tolerance. appearance gains nearest_face (the face and its distance) and pick_tolerance,
which face_index_at now uses as well.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 18:55:47 -04:00
MqriusandClaude Opus 5.5 968a22897c fix(doc): extrude and revolve in a moved component from another component's sketch (#403) (#407)
* fix(doc): extrude and revolve in a moved component from another component's sketch

A feature built from a sketch of another component read the sketch's plane as if it were in
the feature's own component. When the two components are placed differently (a moved or
jointed part), the tool landed transformed along with the feature's component: a join on a
part turned half a turn about Z, from a root sketch, came out on the far side of the origin.

Extrude and Revolve now see the sketch through both placements, as sweeps along 3D paths and
sketches on another component's face already do (sketch_in_feature_frame, using
profile_in_frame). The sheet-metal rotated-frames test built its cap witness from a sketch of
the moved component while the root was active, relying on the old reading; it now builds the
witness in the sketch's own component, which is what it measures against.

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

* test: To Object extrude in a moved component from a root sketch

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

* style: rustfmt

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

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 18:54:19 -04:00
NOWN-DEVandClaude Opus 5.5 0154572580 feat: close sheet flange corners at any angle (#406)
Edge flanges meeting at a corner of the base were closed only at exactly
90°; at any other angle they stayed open (a V at 72°) and the rule's gap
was not used.

The flanges of one feature now close at any convex base corner and any
bend angle: each panel end is cut square to the sheet on a line in the
flat (slanted off 90°, stored as ext plus a new per-end slope) so the
edges nearest each other, on the inner faces, run parallel the rule's gap
apart about the plane bisecting the inner faces. The construction reads
the two flanges' own angles, so it holds for unequal ones as well. At 90°
it is the extension the old code gave (reference case 69-sm-tray is
unchanged). A corner that can't close (a gap under 0.01 mm, flanges
bending opposite ways, ends that would overlap in the flat pattern) stays
open, and the flange feature reports a warning that says where and why.

A flange of a later feature leaves the corner as it was (reference case
75-sm-bracket-holes: the earlier flange keeps its length).

Tested at 72° and 90° against a hand derivation, on a trapezoid, and
every 5° from 45° to 135° on a rectangle and a trapezoid (gap exact, one
flat region without hairline edges, one valid shell holding the flat
material).

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 18:48:19 -04:00
bherbruckandClaude Opus 5.5 296683229d Rename SolveCraft to SolidCraft everywhere, with compatibility shims (#389)
* WIP deep rename (not for push)

* WIP2

* WIP3

* WIP4 wxs fix

* rustfmt

* Rename the remaining identifiers in merged crash-report code

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

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 17:17:01 -04:00
27415e4dd7 Crash reports: replayable, bounded, local, reachable by agents (#384)
Every panic (in a command, in the kernel, anywhere else) and every unclean
exit (abort, stack overflow, kill) leaves a report in a local crash folder:
message, file:line, a trimmed backtrace, version, commit, system and renderer,
the command and recent commands, and when possible the design and replay.json
that reproduce it (solvecraft-cli crashes replay <id>).

- Cost: nothing until the app installs the hook (tests, CLI and web never
  do); then one small journal line per top-level command of the design on
  screen, taken back with a truncate when the command changed nothing.
- Unclean exits: each run holds a lock on its journal; a journal nobody holds
  at the next start belongs to an app that died. Autosaves restart the
  journal from the autosaved design.
- Bounded: a crash's signature (place + message, numbers ignored) is counted,
  not rewritten; at most 5 new reports per run, 20 reports, 50 MB, 90 days;
  recovered kernel panics are pruned first.
- Agents: diagnostics.crash_reports / crash_report / crash_reports.clear
  commands, the MCP crash_reports tool, solvecraft-cli crashes, solvecraft
  --crash-log; diagnostics.crash_test (with SOLVECRAFT_CRASH_TEST) checks it.
- People: the Crash Reports window opens after a crash (Help > Crash
  Reports…) with a reviewed issue draft per report; nothing is sent.
- Release builds keep line tables for file:line in backtraces.

Tests: child processes that really panic in a command, in the kernel, on a
thread, and abort; dedup counts, limits, hostile folders, journal parsing,
the UI window and draft, the MCP tool. docs/crash-reports.md.

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: bherbruck <[email protected]>
2026-10-12 16:50:49 -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
Jonas Wrede 829912c6c5 perf: cut many clear tools of any cut in one boolean (#368) (#387)
One extrude cutting many circles from one sketch still cut the plate once per
circle, each boolean rebuilding and re-meshing a plate grown by every hole before
it (32 circles 2 s, 128 circles 60 s). The #261 fix joined clear tools with
kernel::union_apart only in pattern replay; apply_traced now does it for every
cut with 8 or more tools (patterns included), keeping each piece's face lineage.
2026-10-12 16:09:46 -04:00
Jonas Wrede f06b76e14f fix: number a profile added after a named multi-profile extrude after body_name (#373) (#385) 2026-10-12 16:04:54 -04:00
Jonas Wredeandbherbruck 4b8872e4d0 test: failing regression test for #374 (ignored until fixed) (#382)
Co-authored-by: bherbruck <[email protected]>
2026-10-12 15:43:56 -04:00
Jonas Wredeandbherbruck e7b5b2618e test: failing regression test for #373 (ignored until fixed) (#381)
Co-authored-by: bherbruck <[email protected]>
2026-10-12 15:35:32 -04:00
Jonas Wredeandbherbruck ef9bb0db09 test: failing regression test for #371 (ignored until fixed) (#379)
Co-authored-by: bherbruck <[email protected]>
2026-10-12 15:34:41 -04:00
Jonas Wrede f538eeb5fa test: failing regression test for #372 (ignored until fixed) (#380) 2026-10-12 15:25:36 -04:00
Jonas Wrede d470bfc52a test: failing regression test for #370 (ignored until fixed) (#378) 2026-10-12 15:25:32 -04:00
Jonas Wrede f82b6beedb test: failing regression test for #368 (ignored until fixed) (#377) 2026-10-12 15:25:27 -04:00
bherbruckandClaude Opus 5.5 57e6396fbb Tangent hole contacts split no faces, consistently (#307) (#364)
* Tangent hole contacts: every symmetric contact gives the same faces (#307)

Three things made the four symmetric contacts of reference case 42 differ:

- The 2D prism boolean's ray-parity test miscounted a ray grazing the bottom
  of a patterned hole, whose two halves are read back with centres a few ulps
  apart: one half counted the touch, the other missed it. The loop chaining
  then failed and those cuts fell back to the general boolean, which treats
  circles tangent within its tolerance as overlapping and splits the hole.
  The ray is now moved off vertices and arc extremes by far less than any
  modelling tolerance.
- The prism result's volume check used a coarser mesh than the operands, so
  a plate with many holes read too large and correct results were rejected.
- The merged topology did not join a hole's two pieces across the line where
  two tangent holes touch (four faces meet there).

Case 42 now gives 39 faces (33 holes, 6 planes); its expected.json pins it.

Fixes #307

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

* Kernel test: tangent hole pinch no longer splits the wall (#307)

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

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 15:01:03 -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
bherbruckandClaude Opus 5.5 882ef0e9c6 Face counts: classify sliver faces from their exact surface (#308) (#357)
Since 4e595de a partial-depth cut that ends inside a cylinder runs through the
exact stacked-section path, which leaves 0.3 mm wide strips of the cylinder wall
beside the cut. Their tessellation is one triangle wide, too few distinct
samples for the cylinder fit, so the merged topology reported them as a sphere
and a freeform face. Where the mesh fit finds no plane or cylinder, use the
face's exact surface (plane, cylinder or sphere) instead.

Fixes #308


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

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 14:35:13 -04:00
Pablo López 511d105dcb Project faces split by hidden seams as a whole (#352)
Projecting a cylinder's wall or the face of a round along a circular edge
failed with "face projection has an open or invalid boundary": these
surfaces are stored in halves joined by hidden seams, and the projection
dropped the seams but kept only the picked half. It now projects the whole
patch, the face as drawn, through the same surface_patch the viewport uses
to highlight it.
2026-10-12 14:29:08 -04:00
bherbruckandClaude Opus 5.5 2ebceb6918 Sheet metal: sew the folded-material guard per piece so adjacent flanges cut (#296) (#355)
The guard that refuses machining through bends sewed every bend zone and
nonparallel panel into one solid. Two bends meeting at an open corner relief
share only an edge, so that sew failed with "nonmanifold edge contact" and any
cut on the base was refused. Sew and test each piece on its own instead.

Reference case 75-sm-bracket-holes now passes; its known_failure mark is removed.

Fixes #296


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

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 14:27:40 -04:00
7db66bb824 Pick a horizontal or vertical dimension from where its text is placed (#345)
Between two points, or on a line, that don't line up with an axis, the
Sketch Dimension tool only offered the distance between them: an auto
dimension ignored where its text went. Now the placement picks what it
measures: across from the two (between the perpendiculars through them)
the distance between them, as before; past that, above or below them the
horizontal distance, beside them the vertical one. With no text, an
explicit type, or two ends already on an axis, nothing changes.

The tool previews the dimension from the cursor, so the choice follows
the pointer while placing it.

Fixes #257

Co-authored-by: Vitali M. <[email protected]>
Co-authored-by: bherbruck <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 14:18:11 -04:00
a7e47fdae6 fix: unfold sheets shrunk by a parameter change (#349)
A sheet-metal tray built at 2000 x 2500 mm with an edge flange on all four
edges (72° or 90°) and unfolded failed to rebuild when W and H went to
400 x 300, 600 x 450, or W alone to 400: "planar region arrangement failed
or exceeded its bounded processing budget". The same tray drawn at 400 x 300
unfolded, and growing it worked.

The sketch solved again after the change leaves its corners ~1e-13 mm off,
so the flanges inherit a hair of tilt and their panels and bend zones meet
the trimmed base within 1e-13 mm instead of exactly. The flat pattern is a
balanced union of those pieces, and the arrangement chains its output loops
within 10 tol. In a later round, ray parity classified a bend zone's edge
midpoint that lay level with a corner whose two lines did not quite meet:
the ray slipped through the gap without a crossing, the zone's edges counted
as inside the other operand and were dropped, and the outline no longer
closed. A rectangle turned by 1e-14 rad failed the same way.

inside_loop now closes the gap where one line ends and the next begins with
a straight connector while it counts crossings; exactly closed joints are
unaffected. Joints with an arc keep the arc's own endpoint rule, which
treats ends within its angular tolerance as on the ray (a connector there
double counted at a circle split into two arcs).

Co-authored-by: NOWN-DEV <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 14:15:55 -04:00
bherbruckandClaude Opus 5.5 ce226a88eb Regression tests for the XZ plane orientation (#78): a positive extrude from XZ and from an XZ offset plane goes +Y; a format 5 design with XZ sketches, an offset plane and extrudes opens, and resaves, with identical volumes and bounding boxes (#353)
Claude-Session: https://claude.ai/code/session_01RjGw4gA5ZvKrU8MQP6sBgT

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 14:10:48 -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
Pablo López fc8a5b21cf Loop blends: accept straight wall seams that are not lines (#335)
* Loop blends accept straight wall seams that are not lines

Filleting or chamfering the top outline of two overlapping cylinders whose
concave edges were rounded failed with "the wall seams must be straight":
the round's contact lines with the cylinders are straight NURBS curves, and
the loop blend only accepted seams stored as lines. A seam now counts as
straight when every sample lies on its chord; it is trimmed as a line as
before.

* Fillet and chamfer arrows point into the body

The radius arrow pointed out of the body along the edge's bisector, so
dragging it away from the edge grew the round. It now points into the
material, like the shell's thickness arrow: dragging toward the body grows
the radius or distance, dragging out of it shrinks it.
2026-10-12 13:15:30 -04:00
354a267830 test: failing regression test for #260: Targeted cut deletes another body that the tool fully covers (#275)
* test: reproduce #260 (Targeted cut deletes another body that the tool fully covers)

* Primitives honour `targets` for join/cut/intersect (#260)

solid.box/cylinder/sphere/torus had no targets field, so a `targets` list was
ignored and every body of the component took part: a targeted cut also cut a
non-target body, and deleted one it covered fully. Their `targets` now become
the feature's participants.

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

---------

Co-authored-by: bherbruck <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 12:18:59 -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
667e05a0c9 test: failing regression test for #259: Join with explicit targets also merges a non-target body that the tool touches (#274)
* test: reproduce #259 (Join with explicit `targets` also merges a non-target body that the tool touches)

* Primitives honour `targets` for join/cut/intersect (#259)

solid.box/cylinder/sphere/torus had no targets field, so a `targets` list was
ignored and every body of the component took part: a join also merged a touched
non-target body, a cut also cut (or deleted) one. Their `targets` now become the
feature's participants.

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

---------

Co-authored-by: bherbruck <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 12:17:02 -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 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 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
4d4b3a715f Create construction planes along drawn 3D sketch paths (#240)
Co-authored-by: bherbruck <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 11:44:19 -04:00
6bf86f1f1e test: failing regression test for #267: timeline.delete of a sketch leaves later features that use its body by name (#282)
* test: reproduce #267 (timeline.delete of a sketch leaves later features that use its body by name)

* fix: timeline.delete reports features that newly fail (#267)

A later feature that names a deleted body (a combine targeting a deleted extrude's body) is
listed under failing: [{feature, error}] in the reply instead of failing silently.

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

---------

Co-authored-by: bherbruck <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 11:44:15 -04:00
cb8aa38cc9 test: failing regression test for #266: Multi-profile solid.extrude with body_name names only the first body (#281)
* test: reproduce #266 (Multi-profile solid.extrude with body_name names only the first body)

* fix: name every body of a multi-body feature after body_name (#266)

A new-body feature that makes more bodies than it was given names now stores Ribs (2),
Ribs (3)... for the rest instead of falling back to generic BodyN names. Command-time only:
saved designs keep their names.

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

* fix: keep body_name list for the multi-body naming step (#266)

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

---------

Co-authored-by: bherbruck <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 11:24:33 -04:00
14163d0965 test: failing regression test for #264: Sheet metal: collinear tabs cannot be folded or flanged together (#279)
* test: reproduce #264 (Sheet metal: collinear tabs cannot be folded or flanged together)

* fix(sheet): fold and flange tabs that sit side by side on one line (#264)

A fold's bend zone and a flange's setback were cut with an infinite line, so
a second tab on the same line blocked a fold ('slanted or curved edge') and
two tab-end flanges in one feature shared one trimmed edge (nonmanifold).
When the zone falls apart into pieces, fold only the pieces the fold points
span, each as its own bend; trim a flange's setback only from the material
along its own edge, and attach each flange to the edge it was picked on.

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

---------

Co-authored-by: bherbruck <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 11:19:21 -04:00
bherbruckandClaude Opus 5.5 3b8028a040 Retire legacy reference data and the external command catalog (#302)
- Remove xtask/data/fusion-catalog.tsv and xtask/data/fusion-ids.tsv.
- Remove the legacy command-id table (crates/engine/src/legacy_ids.rs) and its use in
  find_command and in the shortcut, recents and pinned-command loaders. Only SolidCraft's
  dotted ids name commands; any other id is an unknown command.
- Boundary Fill's id becomes solid.boundary_fill.
- Replace the parity report with SolidCraft's own feature checklist: `cargo xtask features`
  writes docs/features.md from the command registry.
- docs/oracle.md, docs/step-import.md (corpus section) and docs/iges-import.md: reference
  results are being rebuilt on analytic and open-source (FreeCAD) values; see #292.
  `cargo xtask oracle` and `cargo xtask step-corpus [--iges]` write these placeholders.
- AGENTS.md, CLAUDE.md, CONTRIBUTING.md, README.md, ROADMAP.md and the book describe
  SolidCraft as independent, with reference tests on analytic values and open-source tools,
  and agents do not drive or extract data from commercial CAD products.


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

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 11:19:15 -04:00
c7f0139ad4 test: failing regression test for #261: Rectangular pattern of a hole cut scales super-linearly (#276)
* test: reproduce #261 (Rectangular pattern of a hole cut scales super-linearly)

* Patterns of many hole cuts scale near-linearly (#261)

Two quadratic costs:
- Meshing a face tested every point against every segment of every boundary
  loop, so a plate face with hundreds of holes meshed in quadratic time. The
  vendored truck-meshalgo inside test now skips loops whose parameter box is
  clear of the point (they wind zero times around it).
- A pattern cut each copy in turn, rebuilding and re-checking an ever more
  complex target per copy. Copies whose boxes are clear of each other are now
  one tool of many pieces (kernel::union_apart, no intersection work), cut once.

512 holes now take ~7 s instead of not finishing. The test's volume tolerance is
widened from 1e-6 relative to 0.1 % of each hole: the measure meshes the bores
and reads each hole ~0.06 % small, on main too.

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

* Hole-pattern test: a budget CI can meet; coarser apart-box meshes (#261)

CI runs every test binary at once and is about ten times slower than a
machine running this test alone (15 tests passed 60 s in the last run), so the
512-hole pattern (about 6 s alone) hit the 60 s timeout there. The budget now
only has to tell linear from the quadratic hang: 300 s timeout, 120 s budget.
union_apart meshes each copy coarsely (2 % of its size, box grown by twice that).

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

* Hole-pattern test checks the scaling, not a wall-clock time (#261)

CI runs every test binary at once: the 512-hole pattern (about 6 s alone, 10 s
CPU single-threaded) took 177 s there, so no fixed time is reliable. The test
now patterns 128 and then 512 holes and requires the second to take under 8x
the first (linear is 4x, measured 3.5-4.2x here; the old quadratic cost was 16x
or more), each with the volume check and a 900 s hang guard.

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

---------

Co-authored-by: bherbruck <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 11:18:28 -04:00
f525704192 test: failing regression test for #268: Panic from truck-topology lib.rs:186 when joining struts that end on a bore wall (#283)
* test: reproduce #268 (Panic from truck-topology lib.rs:186 when joining struts that end on a bore wall)

* Sew refuses edges whose ends weld to one vertex (#268)

The sewer welds vertices on a 1e-6 grid but only rejected edges shorter than
1e-9, so a sliver edge between the two welded its ends into one vertex and
truck-topology panicked ("Two same vertices cannot construct an edge") inside
the guard. Such an edge is now a degenerate-edge error before any truck call,
and the boolean takes its usual fallback.

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

---------

Co-authored-by: bherbruck <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 11:18:24 -04:00
59fabc16ac Reject ambiguous origin and construction plane names (#247)
* Reject ambiguous origin and construction plane names

* Reserve origin plane names for construction planes; suggest the other alias

A construction plane can no longer be created with or renamed to XY, XZ, YZ, Top, Front or
Right, so a plane argument can't become ambiguous in new designs. For older designs that
already hold such a plane, the ambiguity error now also names the origin plane's other
alias (XY for Top...). The existing tests build those legacy names directly.

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

---------

Co-authored-by: bherbruck <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 11:05:57 -04:00
aea16f2b5a test: failing regression test for #265: Renaming a pattern copy renumbers the remaining copies (#280)
* test: reproduce #265 (Renaming a pattern copy renumbers the remaining copies)

* fix: renaming a body keeps its sibling bodies' names (#265)

body.rename pinned only the names of the feature's bodies before the renamed one, so unpinned
pattern copies after it were renumbered into the freed name. Pin every sibling's current name.

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

---------

Co-authored-by: bherbruck <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 10:59:08 -04:00