Commit Graph
3220 Commits
Author SHA1 Message Date
Tommaso Bianchi 6b3642fa53 Mate viability: which of the five apply, and why the others do not
snaporca-lukg wants a palette offering all five mate types with the non-viable
ones DIMMED AND EXPLAINED rather than hidden — its reasoning being that a menu
changing shape between invocations destroys the motor memory experts rely on.
That needs an answer this document could not give. This is that answer, and
nothing else: mate_options(cs_a, cs_b) returns five MateOption{kind, viable,
reason}, always five, always in kind order, never filtered.

The geometry test rides on the fingerprint added for snaporca-kqih, which is why
it costs no new serialized field: coordsys_face_kind already records the surface
type. Revolute and Cylindrical need a cylindrical face at both ends because they
need an axis to turn about; Planar needs flat faces; Fastened and Slider
constrain frames rather than surfaces, so no geometry test applies to them.

UNKNOWN IS PERMISSIVE. A fingerprint of -1 means PointWorld or a connector that
has not resolved yet, and it does NOT make a type non-viable. Refusing on missing
information is the false-alarm behaviour that gets a whole feature ignored — the
same reasoning already recorded on kqih for the drift warning, applied again
because it is the same trade.

Reasons name WHICH connector is the problem when only one is. "needs a
cylindrical face at both ends" tells the user what the rule is; "connector A is
on a flat face" tells them where to look, and the second half is the one that
saves the time.

The stability contract has its own test, asserting five entries in kind order
even for a completely invalid pair. That matters more than any individual
verdict: the palette addresses rows by position, so a shorter list would move
every row below it.

Golden fixture unchanged — this is a pure query. Suite 167 -> 171 cases,
2284 -> 2348 assertions, green. snaporca-lukg part A; the palette is part B.
2026-08-12 23:45:37 +02:00
Tommaso Bianchi a3398c6609 MCP: let a caller find out its face/edge ids went stale
snaporca-rgbj measured the damage: four chamfers on a box remove
0.400/0.397/0.397/0.395 mm3 when each id is re-read, and
0.400/0.008/0.397/0.280 when the four ids are captured up front. The kernel is
right in both runs — the second one asks for the wrong edges. Neither errors,
because a stale id still resolves to a real edge, just not the one that was
measured.

That makes it an API problem rather than a script bug. Reading the scene once and
then issuing several operations is the natural way to drive a socket, it is what
every agent will write, and it produced silently wrong geometry with nothing
anywhere reporting it.

CadDocument::topo_generation is bumped where the bodies are replaced — the single
line in recompute() where the face and edge maps actually change, so a feature
type added later cannot forget to bump it, which a per-mutator counter would
invite. describe_scene and query_topology return it. A caller may pass it back as
"generation" on any call, and a mismatch is refused with a message that says what
to do about it.

Two deliberate choices:

OPTIONAL, not mandatory. Every existing script keeps working unchanged; passing
the generation is what buys the guarantee. Making it required would break every
caller to fix a mistake only some of them make.

CHECKED AT THE DISPATCHER, not in each handler. One site covers fillet, chamfer,
shell, draft, coordsys, thicken, cut, project, delete_face and everything added
after them. A per-handler check is a list that goes stale the first time someone
adds a method in a hurry.

Not serialized: an id means something only within the run that produced it, so
persisting the counter would promise a stability the ids themselves do not have.
No recipe version change.

describe_tools now carries an id_lifetime note, because the guard only helps a
caller who knows to ask for it.

Kernel suite 167 cases / 2277 assertions green; libslic3r_gui builds. The guard
itself is NOT exercised — it needs the socket, so it is on snaporca-bdco.
snaporca-o1l2.
2026-08-12 23:27:11 +02:00
Tommaso Bianchi 9125e0b4f8 Connector face drift: warn without crying wolf — and bump the recipe version
kqih option (c). A FaceAndDirection connector stores a global face index, and an
upstream edit can renumber faces so the index silently names a different one. The
DANGLING case already threw; this is the in-range-but-wrong case, which nothing
detected.

Fingerprint the face on first resolve, compare afterwards, and report a mismatch
into mate_conflicts — the channel that already marks the tree row — never as an
error. A drift warning must not abort the recompute, because the alternative
makes a legitimate Draft on a mated face fatal.

WHAT THE FINGERPRINT IS, AND WHAT IT IS NOT. Surface type plus edge count. Not
centroid or area: legitimate parametric edits move and resize faces, which is the
entire point of the model, so either would fire on every dimension change. Not
the normal, which is the tempting one — Draft deliberately tilts a face and
Transform reorients a body, both legitimate. Type and edge count survive rigid
motion, tilting and resizing, and catch the case that actually happens: a planar
index sliding onto a fillet's cylindrical face after a dress-up inserts faces.
The accepted cost is that a slide between two planar 4-edge faces is invisible. A
partial detector that never cries wolf beats a total one that does, because a
false alarm on a valid connector teaches people to ignore the warning.

Connectors with no fingerprint record one on first recompute, so old recipes
self-heal and both writers (DesignPanel, McpControl) get it without changing.

THE VERSION BUMP IS THE IMPORTANT HALF. The task was specified with "do not
change the recipe version" — that was wrong, and the rule is written in the
header three lines above the constant: bump whenever save/load gains a field.
deserialize_recipe() gates on v == VERSION and then reads a FLAT symmetric field
list. A v3 blob under a v3 build that has grown two fields passes the gate and
reads two ints past the end of every connector, into the next feature's bytes.
That is silent corruption of a saved project, which is worse than any load error.
Now v4, and v3 gets the existing clean refusal.

cad_recipe_v3.bin is KEPT, unregenerated, with a test asserting it is refused and
that nothing half-read is left behind. It is the only artefact that can prove the
gate works, because it was written by an older build — regenerating it with
today's code would destroy the evidence, which the test says in as many words.

Suite 163 -> 167 cases, 2248 -> 2277 assertions, green. Fixture v4 34928 bytes.
snaporca-kqih.
2026-08-12 23:04:49 +02:00
Tommaso Bianchi b9d6b59f90 A feature that destroys a body must say so, not ship a phantom
Driving the control socket: hexagon prism, six vertical fillets, four chamfers
on the already-filleted rim, an M8 hole. Afterwards describe_scene reported
bodies=3 and error='' — entirely healthy — while body 2's TopoDS_Shape was null.
Only mass_properties on that one body revealed anything was wrong.

So a feature destroyed a body, recompute() returned true, and the document went
on advertising it. Any downstream consumer — slicing, STEP export, a mass
properties report — met a null shape with no warning. That is the silent
corruption class, which is the one class this project does not tolerate.

recompute() now scans the freshly built bodies for a null shape, names the body
and the feature that destroyed it, and returns false. Returning false rather than
just setting error is the point: it hands the caller its normal rollback path, so
the operation that destroyed the body is undone instead of committed.

The message says "an unidentified feature" when source_feature is -1. "feature 0"
would be a lie, and a message that exists to tell you where to look has to be
trusted.

TEST IS A POSITIVE CONTRACT, AND THE REASON MATTERS. The reported order was
driven headlessly first, as the better test: it does NOT reproduce. The dress-up
step throws "fillet radius too large", which is an already-loud already-caught
path, so recompute fails honestly and never nulls a body. No public-API sequence
found so far reaches the guard's branch without a GUI, and faking a null into
`bodies` after the fact would not exercise it — the guard runs on `built`, before
the swap. So the test asserts what can be asserted: a box + fillet recomputes
true, error is empty, and no body is null. The guard's own branch is defensive
and currently unexercised; that is stated here rather than implied by a green
suite.

Kernel suite: 2217 assertions in 161 test cases, all passing. No existing test
relied on a null body surviving a recompute, so hardening this broke nothing.

snaporca-5425 (part a). Part b — why the chamfer chain degenerates on an
already-filleted rim — is untouched and stays open.
2026-08-12 20:57:35 +02:00
Tommaso BianchiandClaude Opus 5 d584d66003 Mate conflicts: name what silently wins, don't call it over-constraint
Two enabled mates driving the same body is not an error today — the later one
just wins, and the earlier mate looks ignored with nothing said. A cycle in the
mate graph is worse: composition still produces a result, but an arbitrary,
order-dependent one.

recompute() now fills a mate_conflicts vector of (feature index, reason) before
the geometry pass, so it survives a throw further down. It catches a second mate
on the same target body, a mate positioning a body against itself, and a cycle,
via an iterative three-colour DFS over the body graph. Broken mates are skipped
silently — apply_mate() already errors on those.

Deliberately non-fatal: recompute() still returns true and error stays empty.
Deliberately not "over-constraint" — that word promises DOF analysis from a
solver this kernel does not have.

Port of snaporca ec4ffeb979. Kernel half of snaporca-bioq.
Suite: 2213 assertions / 160 cases green on this fork too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 15:06:17 +02:00
Tommaso BianchiandClaude Opus 5 8ff7ba440d CadDocument: reindex mate connectors on feature delete and reorder
mate_cs_a/mate_cs_b are feature indices. remove_feature() remapped sketch_ref
through the deletion but not the mate connectors, and move_feature() swapped
sketch_ref but not the mate connectors. Deleting or reordering any feature
ahead of a connector slid both references onto whatever features landed on
those slots.

Nothing reported it. recompute() only rejects out-of-range and non-CoordSys
targets, and a shifted index normally lands on the assembly's other CoordSys —
an assembly carries at least two by construction. So the mate resolved against
the wrong frames and moved the wrong body, silently.

Extracted a remap lambda in remove_feature() and a swap_ref lambda in
move_feature(), applied to sketch_ref and both mate connectors.

Two tests, both confirmed red before the fix. [mate] tags green here:
395 assertions / 29 cases — the first end-to-end kernel compile of this fork.

Ported from snaporca; CadDocument.cpp is byte-identical across forks again.

Refs: snaporca-kqih

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 13:14:10 +02:00
Tommaso Bianchi b2654ebd8a Design: a body knows what made it, so "Delete Body" can exist
Reported by Tommaso: select a body, and there is no Delete in the offer. Two independent
faults stacked behind that.

FIRST, clicking a body never selects the body. Whole-body picking is deliberately unbound
(DesignSketchTool.cpp) pending the rubber band, so a viewport click only ever yields
Face/Edge/Vertex. The offer therefore saw face_planar, and "delete" accepted body_solid but
no face kind, so the row was filtered out entirely — while the status line read "Body 1
face 0 selected", which actively teaches the wrong model.

SECOND, even selecting the body from the Bodies list, Delete refused in red: "Select the
FEATURE that created this body". CadBody had no link back to its maker, so the offer was
advertising a verb it could not perform — worse than the action:null rows fixed earlier this
session, because this one is ENABLED and its refusal reads like user error.

CadBody::source_feature fixes the second. It is stamped in ONE place, the recompute loop,
and the rule is just "still unset?". That is sufficient because of an invariant worth
stating: no feature ever replaces a whole CadBody. Every in-place op writes only .shape
(boolean, cut, mirror-fuse, transform, dress-up — all 8 sites checked), so a body keeps the
stamp it was born with; a consumed body is erased outright, taking its stamp with it; and
the only bodies still at -1 are the ones the current feature just pushed. A feature type
added later needs no change here as long as it keeps to that invariant.

"Delete Body" fixes the first, sitting beside "Delete Face" in Modify and reachable by
pointing at any face/edge/vertex. The two names cannot be confused, and "delete" gave up the
body kinds so both can never appear for one selection. Deleting a body removes the feature
that made it, which is a real edit to the recipe, so it asks first and NAMES the feature — a
body vanishing from the viewport is not evidence of which feature went, and this is the one
action here that cannot be eyeballed.

Multi-body delete is NOT offered. bodies_2 was in the first draft of the verb; the handler
deletes exactly one body, so a two-body selection would have silently deleted whichever was
m_sel_solid_body. Caught before it reached a binary, at the cost of one rebuild.

Verified on BOTH rigs, full round trip: click a face -> Modify > Delete Body -> "Delete
Extrude2?" -> body gone, Sketch1 correctly left behind, panel falls back to the idle hint ->
Undo -> Extrude2 and Body 1 restored.
2026-08-02 09:41:58 +02:00
Tommaso Bianchi 3f8f46f93f Delete the sketch plane dropdown; the viewport decides
The plane combo is gone. A sketch takes its plane from what is picked in
the viewport: a face on a solid, or one of the reference-plane ghosts,
clicked in 3D. The card is now a single line of instruction instead of a
control.

The combo had become worse than redundant. Once a picked face could be
the plane it displayed a row that CONTRADICTED the actual target — it
still said XY while the sketch went onto the face — so the one place a
user could look to confirm where they were drawing was the one place
guaranteed to be wrong.

What replaces it is state, not UI: m_ref_plane records which reference
plane was last clicked in 3D (0/1/2 = XY/XZ/YZ, >=3 indexes the datums)
and ref_plane_name() turns it into text for the on-geometry hint. Clicking
a ghost plane while a session is live re-planes it immediately, which the
combo's own handler used to do; that behaviour is kept, just driven from
the geometry instead of the widget. A plane click also drops a stale face
pick, so last pick wins in both directions.

populate_plane_choices() stays — seven other pickers use it (Plane base,
Axis A/B, Helix, Project, Mirror, Cut). Those are the next candidates,
tracked on snaporca-e1p; this commit only removes the one that had become
actively misleading.

Also: tessellation now matches Orca's OWN STEP importer, linear deflection
0.003 instead of 0.01 (Format/STEP.hpp default; angular was already 0.5
rad and unchanged). The Design viewport was never using a different
rendering technique — it hosts a real GLCanvas3D, builds a real
Model/ModelVolume and goes through the same reload/GLVolume path and the
same shaders as Prepare and Preview. What differed was the mesh handed to
it: 3.3x coarser than anything else in the application, which is why a
curved face read as faceted beside an imported part. Suite unaffected at
154 cases / 2125 assertions, so nothing depended on the old density.

Verified on :10: the card shows no dropdown, one click on a face then a
sketch tool still reports "on the picked face", and the circle is drawn in
that face's plane (artifacts/shots/h3a2-02-sketch.png, h3a2-03-drawn.png).

snaporca-e1p, snaporca-3a2.
2026-07-30 14:24:29 +02:00
Tommaso Bianchi 6ce20d78c3 Sketch where the user pointed: a picked face is the sketch plane
Selecting a face and sketching on it is the most common gesture in solid
modelling, and it was impossible. The plane came from a combo holding
XY/XZ/YZ plus datums, and plane_from_choice had no face branch at all, so
the only route onto a face was to build a Coincident datum plane on it
first, confirm that, reopen the sketch and find the datum in the
dropdown. Three extra steps and a junk feature in the tree.

The fix is not another combo row. A new sketch now takes its plane from
what is SELECTED IN THE VIEWPORT: a picked planar face wins outright, and
only when nothing is picked does it fall back to the reference plane —
which is itself normally set by clicking one of the ghost planes in 3D,
not by opening the combo. The tool hint names the target ("Circle — click
center, then radius · on the picked face") so the choice is visible on the
geometry side rather than needing a control to read back.

CadDocument::plane_of_face is the shared derivation, so the sketch path
and the Coincident datum method cannot drift apart. It refuses
non-planar faces: face_normal_world evaluates at the mid parameter, which
on a cylinder or a fillet is a tangent plane at one arbitrary point —
fine for offsetting a datum, wrong as a sketch plane, and silently
sketching on a tangent is worse than declining.

Picking the face also CONSUMES it. Leaving the pick live meant the next
Extrude saw a selected face and push/pulled it instead of extruding the
sketch just drawn — the same trap the imported-art path already guards
against.

Verified on :10 end to end with no combo interaction: build a box, click
its top face twice to cycle whole -> face, press S then C, and the circle
is drawn in the plane of that face with its Radius tab on the geometry
(artifacts/shots/f3a2-03-face.png, f3a2-04-sketch-on-face.png,
f3a2-05-circle-drawn.png). Kernel side: 154 cases / 2125 assertions green
on both forks, including that a cylinder resolves exactly its two flat
caps and refuses the barrel.

Still side-panel-shaped and to be dealt with separately: the Plane combo
remains on the card and now merely displays a stale row when a face is
the real target. It should show the actual target or go away.

snaporca-3a2.
2026-07-30 13:42:11 +02:00
Tommaso Bianchi 7f0a8c85ee An entity sketch that forms no wire fails, instead of extruding a default box
entities_to_wire handles exactly two shapes: one lone closed entity
(Circle/Ellipse), or a chain of open ones (Line/Arc/EllipseArc/BSpline).
Anything else -- a circle coexisting with a line, two circles -- returns a
null wire. build_sketch_wire answered that by falling through to its
legacy tail, which ends in a rectangle built from width/height. For an
entity sketch those fields are whatever they were initialised to, so the
extrude produced a box the user never drew, silently and with ok:true.
That is how the ellipse+stray-arc case in the P2 Tier-B.1 verification
turned into a default-rectangle solid.

Throw there instead. The legacy profile/shape paths below are still
reached by sketches that legitimately carry no entities at all, so the
enum and profile constructors are untouched -- only the case where
entities exist and cannot be turned into a wire now fails, which is
exactly the case that was fabricating geometry.

This does NOT implement the multi-loop support the issue asks for. Doing
that properly means deciding containment -- a circle inside a rectangle is
a hole, a circle beside it is a second region -- and make_extrude_regions
cannot be reused because it takes flattened Vec2d contours for imported
Text/SVG art and would discard the analytic circle. Guessing containment
would trade a visible failure for a wrong solid, which is the opposite of
the point. Left scoped on snaporca-88v.

Also converts the three float comparisons in the two test cases added this
session from Approx to WithinAbs/WithinRel, per tests/CLAUDE.md, which
rules Approx out for being asymmetric and double-only. The rest of the
file's pre-existing Approx uses are left alone.

153 cases / 2090 assertions green on both forks; no existing test depended
on the default-rectangle fallback.

snaporca-88v (partial: the silent-fallback half).
2026-07-30 09:56:18 +02:00
Tommaso Bianchi 5f1811c2c5 A subtraction that removes nothing is an error, not a silent success
A boolean cut whose tool misses the target is a perfectly legal
operation: OCCT reports IsDone(), the shape comes back unchanged, and the
feature lands in the recipe reporting ok:true. Driving the MCP socket,
that produced two consecutive {ok: true, bodies: 1, error: ''} responses
for a hole that was never drilled -- same viewport, same 46939.11 mm3 --
and the tree grew two Hole features that will never cut anything. A
caller, an agent especially, has no signal at all that the thing it asked
for did not happen.

Measure the volume across the op in route_feature's in-place branch and
refuse the no-op. Only for removals: Hole, Thread, and Extrude / Revolve
/ Sweep / Loft in Cut mode. Everything else may legitimately leave the
volume alone -- a Transform certainly does. The tolerance is relative,
because an absolute epsilon is wrong across the mm-to-metre range of real
parts, and a cut that shaves a numerically invisible sliver is a miss
too. The existing rollback in the MCP actions already preserves the
reason, so a missed hole now answers ok:false with the error and undoes
the feature.

The confusion underneath was not itself a bug: hole's x/y are in the
sketch plane's frame, whose origin is describe_scene's modeling_origin,
and describe_tools documented them only as "number, unit mm". Passing the
world centre put the hole 135 mm clear of the solid. All six x/y params
on hole / hole_styled / hole_standard now say which frame they are in,
since the wrong guess was silent.

Regression test drives the reported failure directly: a hole at x=135 on
a 20x20x20 box is rejected and leaves the body untouched, the same hole
at the origin still removes exactly pi*4^2*20, and a cut-mode extrude
whose profile sits at x=200 is rejected too. 152 cases / 2083 assertions
green on both forks.

snaporca-daf.
2026-07-27 07:21:06 +02:00
Tommaso Bianchi 347b83d887 Sketch fillet: never write a failed solve's geometry back, and commit the op
Two P0s in the same gesture. Filleting a corner of a parametric rectangle
produced either nothing at all or a sharp corner with a stray arc floating
above it.

Trigger (snaporca-cq2): the only routes that ever reached confirm_op were
finishing the whole sketch and an unsignposted click on empty space.
set_tool() dropped a ready op, so typing a radius or dragging the arrow and
then touching any other tool threw the value away. Commit a ready op on tool
change (before m_mode is reassigned — op_ready() and confirm_op() both switch
on it), commit on Enter in the radius editor, and drop the pending op before
Esc's tool downgrade so Esc still cancels rather than applies.

Substitution (snaporca-pl5): libslvs writes its last Newton iterate into the
params whether or not it converged, and SketchSolver read them back
unconditionally, so every REJECTED solve deformed the sketch. The fillet
ladder tries a deliberately over-constrained rung first (a tangent on each
leg, against the legs' own H/V); it is correctly rejected, but its wreckage
then failed rungs 2 and 3, which solve cleanly on their own. The arc ended up
with no constraints at all, the rigid loop won, and the corner snapped shut.
Measured: from pristine geometry rung 1 gives result=INCONSISTENT with 3 bad
constraints, rung 2 gives dof=6 with the arc's radius intact.

Read the geometry back only on success. try_add_constraints then needs no
"restore" re-solve — the entities still hold the prior solved state.

Kernel suite 151 cases / 2072 assertions green on both forks; the GUI check
ran on the Snapmaker fork (9d72c4377a).

Ported from the Snapmaker fork. snaporca-pl5 snaporca-cq2
2026-07-26 23:33:50 +02:00
Tommaso Bianchi 695932ad87 Drop the degenerate triangle OCCT emits at every filleted corner
A filleted solid arrived on the plate as a broken model: the slicer reported
"8 non-manifold edges" on an 80x50x12 box with r=3 on all edges, and advised
repairing it in another CAD application -- the exact round trip the Design tab
exists to remove. The same box without the fillet committed cleanly.

Measured rather than guessed. Each of the 8 bad edges is degenerate, both
endpoints the same vertex:

    open tri=145  edge=1 face=5 v59(3.000000 3.000000 0.000000) v59(3.000000 3.000000 0.000000)
    open tri=538  edge=1 face=6 v87(3.000000 3.000000 12.000000) v87(...)
    ... one per corner, 8 corners

OCCT triangulates a degenerate surface parameterization with a triangle at the
pole; a corner sphere patch has exactly one. Its two pole nodes are distinct in
the per-face triangulation and collapse to a single vertex when the faces are
welded, leaving a zero-area triangle whose v->v edge can never pair with a
neighbour. its_face_neighbors counts it as open, and the field the object panel
prints as "non-manifold edges" is in fact stats.open_edges.

So the geometry was never wrong -- the B-rep volume matches the Steiner formula
for a box dilated by a ball to 0.016%. Only the bookkeeping was.

Dropping those triangles after the weld removes 8 of 3492 and takes open_edges
to 0. Zero area, so nothing about the shape changes. tri_face is compacted in
the same pass, since it must stay index-aligned with the triangle list that the
face picking and per-body colouring both index into.

Guarded by a new [CadDocument] case that asserts open_edges == 0, no degenerate
triangle survives, and both per-triangle maps still match the triangle count.
The existing suite only ever checked B-rep volumes and areas, which is why a
mesh defect this visible went unnoticed: 150 cases / 2049 assertions green on
both forks.

snaporca-agw
2026-07-26 17:27:40 +02:00
Tommaso BianchiandClaude Opus 5 2c5ddd4102 OCCT link order: put TKFillet/TKOffset before their dependencies, not after
OCCT_LIBS is an explicit single-pass static link order — dependents first, TKernel
deliberately last. The CAD block appended its two extra toolkits to the END of that list,
which puts them after everything they depend on:

    list(APPEND OCCT_LIBS TKFillet TKOffset)

TKOffset references BRepAlgo_Loop, and nm against the built deps prefix shows TKBool is the
only toolkit that defines it (TKTopAlgo, TKBO, TKPrim, TKFillet and TKOffset all define it
zero times). TKBool sits first in the list, so a single-pass linker has passed it long before
it reaches the appended TKOffset and will not go back:

    libTKOffset.a(BRepOffset_MakeLoops.cxx.o): undefined reference to
    BRepAlgo_Loop::BRepAlgo_Loop()

Only one configuration ever objected — the Snapmaker fork Flatpak (aarch64). Ordinary Linux,
macOS and Windows links resolve it regardless, and the mainline fork Flatpaks pass, so six
green platform legs said nothing about whether this list was correct.

Prepended via set() rather than list(PREPEND), which needs CMake 3.15 while this project
supports 3.13.

Worth knowing for later: TKFillet and TKOffset are mutually dependent, 20 symbols needed in
each direction, so a stricter single-pass link could still trip on that pair. It does not on
any current platform, so no --start-group or duplicate entry is added here; if something ever
complains about ChFi or BRepFill symbols, that cycle is the reason.

snaporca-2kj

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM
2026-07-26 15:28:46 +02:00
Tommaso BianchiandClaude Opus 5 5026dd11a6 Fix the solver abort on circle-line tangency; the CAD suite now runs complete
snaporca-tkz, the last quarantined test. Root cause read out of the vendored
source rather than guessed: slvs/constrainteq.cpp, Type::ARC_LINE_TANGENT does

    ExprVector ap = SK.GetEntity(arc->point[other ? 2 : 1])->PointGetExprs();

so it dereferences the ARC'S ENDPOINTS. A full circle entity carries only
point[0], its centre. point[1] and point[2] are zero handles, FindById throws
"Cannot find handle", and the process ABORTS rather than failing the solve —
taking every later test in the binary with it. That is also the wrong equation
for a circle regardless: it only makes the line perpendicular to the radius at
an endpoint that does not exist.

CT::Tangent no longer hands a full circle to that constraint. For a circle it
emits PT_LINE_DISTANCE(centre, line) = radius, which is precisely what tangency
to a circle means. Arcs keep the ARC_LINE_TANGENT path they are built for.

One limitation, stated rather than buried: the slvs C API takes a constant
distance and offers no way to reference the circle's radius parameter, so the
radius is captured when the constraint is emitted. That is exact whenever the
radius is fixed or is simply not driven by another constraint in the same
solve, and re-solving restores tangency if something else moves it. Tying them
would need an auxiliary point constrained onto both the circle and the line.

With this and eeca6794e7, both quarantined tests are gone and the exclusion in
kernel-test.sh goes with them. A green run now means the whole CAD suite
passed, not "everything except the two we gave up on":

    149 cases / 2043 assertions, no filters.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 09:33:50 +02:00
Tommaso BianchiandClaude Opus 5 df6ef85614 Un-quarantine the internal-thread test: the geometry was right, the test was not
snaporca-kzy was filed as "internal thread cuts too little material". It does
not. Measured on the test's own fixture, a 40x40x20 box:

  plain Ø12 bore   removes 2261 mm3
  internal thread  removes 2157 = 1571 (minor bore) + 586 (groove)

apply_thread bores at the MINOR radius (radius - depth = 5) and then carves the
groove out to radius + depth = 7. A tapped hole therefore keeps the crests
between turns and holds MORE material than a plain clearance hole at the
nominal radius — which is what every real tapped hole does. The test asserted
the opposite, so it was asking for something physically wrong and had been
quarantined for it since it was written.

One hypothesis discarded on the way: that the shortfall was a tessellation
artefact, since chords on a helical surface undercut a concave bore. Exact
BRepGProp::VolumeProperties agreed with the tessellated volume to within
2.5 mm3, so that was not it and is not offered as a hedge.

The reference is now the tap-drill bore the thread actually starts from (Ø10),
against which the groove's 586 mm3 is the meaningful quantity — that is what
"the thread cuts" means. Test re-tagged [CadDocument][thread], so CI covers the
thread path again instead of skipping it.

Also documented the (void)internal in make_thread_profile. It reads like a bug
and is not: the V is the same shape either way and the caller decides, fusing
it onto a shaft or cutting it out of a wall. Someone "fixing" it to point
inward for the internal case would make the groove sweep already-empty bore
space and cut nothing — the exact failure the old comment described.

Suite 148 cases / 2035 assertions, with this test now among them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 09:32:47 +02:00
Tommaso BianchiandClaude Opus 5 1726e93760 Mate connectors: derive a face-only frame's X from the face, not from world
datum_frame took a FaceAndDirection frame's Z from the face normal, which
follows the body, but its X from coordsys_x_hint, a world constant, whenever
no explicit edge reference was set. Spinning a body about its own face normal
therefore left the frame bit-identical: the connector could not encode that
rotation at all, so Fastened and Slider mates claimed to fix an orientation
the frame could not see.

X now comes from the face's own first usable edge, which rotates with the
body. The hint survives only as a last resort, for faces that offer no
in-plane direction — a full circular edge has coincident endpoints, and a
seam projects to nothing in-plane.

The new test spins a box 90 degrees about its top-face normal and asserts the
frame's X turned with it. Reverting just the X_tent derivation and rerunning
makes it fail with "1.0 is within 0.000001 of 0.0" — cos(angle) between the
before and after X is exactly 1, i.e. the frame did not move — and that is the
only failure in 2019 assertions, so the test discriminates this defect and
nothing else.

Note for anyone replaying an older document: a face-only connector's frame
can now differ from what that recipe produced before, so a mate built on one
may place its body differently. Nothing in the suite or the golden v3 fixture
changed, but the semantics did.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 08:18:42 +02:00
Tommaso BianchiandClaude Opus 5 b25335e1b3 Rib: accept a Project feature as its sketch ref
Rib guarded with `sk.type != CadFeatureType::Sketch`, while every other
sketch consumer — Extrude, SurfaceExtrude, SurfaceRevolve, the loft paths —
tests `!= Sketch && != Project`. A Project feature carries a plane and Line
entities, which is all a rib reads, so the guard blocked "project a body
edge, then rib along it" for no stated reason.

The picker in the Design tab offered Sketch features only, so it is widened
to match: a kernel that accepts Project refs and a GUI that never lists them
would have left the path unreachable anyway.

Worth recording for whoever hits this next: Rib also needs a sketch carrying
EXPLICIT entities. A parametric Rectangle sketch (add_sketch with
width/height) has an empty entities vector — build_sketch_wire synthesises
its profile on demand — so rib_entity 0 is out of range there and it fails
with "rib: bad entity". That is why Rib could not be driven headlessly at
all before this change; a Project feature is now the one programmatic way to
produce a ribbable line.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 07:48:16 +02:00
Tommaso BianchiandClaude Opus 5 1cb80f7f9f Project: implement "(all edges)"; stop discarding the failure reason
Two defects found while driving the tools that Phase B wired but nobody had
exercised yet.

apply_project had no all-edges branch: with no face picked and no explicit
edge list it threw "no edges or face selected". That is precisely the state
the Project card opens in, and its label reads "(all edges)" — so the card's
default could never be confirmed. It now projects every edge of the source
body. Edges perpendicular to the target plane collapse to a point when
projected, so segments whose endpoints coincide are dropped instead of being
emitted as zero-length lines that would poison the sketch downstream.

The second defect is why the first one was invisible. 29 of the 31 rollback
sites in McpControl ran `if (!ok) doc.undo();`, and undo() recomputes the
restored feature list — which succeeds and clears doc.error. Every failing
command therefore reported `error: ""`. Yesterday's fix covered 2 sites and I
treated the file as done; it was not. All 31 now capture the reason before
the rollback and restore it after. Failures that read as `""` now read as
"rib: bad entity" / "surface-revolve: revolve failed".

Verified on the running GUI through the control socket: the Project call that
previously returned ok:false now returns ok:true, and failures carry a reason.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 07:04:15 +02:00
Tommaso BianchiandClaude Opus 5 4d331099de CAD: Axis PlaneIntersection read its plane refs in the wrong index space
axis_plane_a/b are filled by the GUI from populate_plane_choices(), whose rows
are XY / XZ / YZ followed by the datum planes, and the row is stored verbatim.
The kernel's base_plane() indexed datum_planes[ref] directly, so the two spaces
were off by three: picking XY resolved to datum plane 0, and picking the first
datum ran past the end and failed with "plane ref not found". The
PlaneIntersection axis type could not work from the GUI at all.

base_plane() now uses the encoding CadFeature::plane_base already uses — 0/1/2
are the base planes through the modeling origin, >=3 indexes datum_planes[ref-3]
— so there is one convention for plane references instead of two. That also
makes two base planes usable, which the previous code rejected as out of scope
even though XY x XZ is an ordinary way to define the X axis.

Removed the dead find_plane lambda directly above it. It was never called and
half-anticipated this exact offset ("if (ref >= 3) // base plane offset"),
which is presumably where the confusion started.

Tests: the existing parallel-planes case encoded the OLD convention, passing
axis_plane_a = 0 to mean "datum 0" — values the GUI cannot produce — so it is
re-based onto rows 3 and 4. Two new cases cover what the GUI actually emits:
base x base (XY x XZ -> X) and base x datum, the latter pinning the +3 offset.
Both verified to FAIL against the previous indexing, at test_caddocument.cpp
:2325 and :2346.

Found by auditing the remaining tools for the index-space defect class that had
already produced three bugs in the GUI; this is the first instance of it
crossing the GUI/kernel boundary.

Suite 143 cases / 1980 assertions (was 141/1972). GUI compiles clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 05:17:53 +02:00
Tommaso BianchiandClaude Opus 5 c565d6ba86 CAD: undo() never rolled back variables, so a bad one bricked the recipe
checkpoint() snapshotted `features` and undo() restored `features`, but
`variables` is a separate member of CadDocument. Every caller of the documented
checkpoint -> mutate -> recompute -> undo-on-failure pattern therefore failed to
roll a variable back: the bad value stayed in the document and every later
recompute failed, which is exactly the corruption the pattern exists to prevent.
Feature `expr` bindings were unaffected only because expr lives inside
CadFeature and rode along in the features snapshot — which is why the feature
side appeared to work.

This was a kernel gap, not a GUI one: McpControl::action_set_variable has the
same sequence and was equally broken.

The undo/redo stacks now hold a {features, variables} Snapshot. Nothing here is
serialized, so no recipe version change and no golden-fixture regeneration.

Two tests, both verified to FAIL against a faithful reproduction of the bug
(undo() leaving `variables` untouched) at test_caddocument.cpp:4420 and :4441:
one covers restoring a variable's previous value, the other covers removing a
variable that did not exist before the checkpoint. Worth recording that the
first mutation attempt was NOT faithful — it dropped the restore but kept
std::move(variables) into the redo stack, which empties the map as a side effect
and made the second test pass for the wrong reason. A mutation has to reproduce
the original defect, not merely break the code.

Second defect, same area: undo() calls recompute(), which succeeds and clears
doc.error, so the reason an edit was rejected was destroyed before anything
could display it. Six sites — four in DesignPanel, two in McpControl — now carry
the message across the rollback. on_remove_variable additionally asserted
"referenced by a feature expression" as fact; it now offers that as the likely
cause and appends the real error, since that diagnosis is wrong for any other
failure.

Suite 141 cases / 1972 assertions (was 139/1960). GUI compiles clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 23:56:34 +02:00
Tommaso BianchiandClaude Opus 5 ed9d02093e Default both build scripts to orcacad-deps, not the other fork's image
Both scripts defaulted IMAGE to snaporca-deps. That image is Snapmaker-based
and lacks Eigen 5.0.1, CGAL 5.6.3, wx 3.3.2 and Python 3.12 Development.Embed,
so running either script here without an explicit IMAGE= dies at CMake
configure — which is a large part of why this fork reached M8 having never once
compiled (1633005bba). The volume defaults were fixed in that commit; the image
default was missed in both files.

docker-iter-build.sh also never mounted deps_src, so it could not have
configured even with the right image: root CMakeLists.txt:947 FATAL_ERRORs
when deps_src/pybind11/include/pybind11/pybind11.h is absent, and
src/CMakeLists.txt pulls semver/hints/imgui/imguizmo/hidapi from the same tree.
kernel-test.sh got that mount in 1633005bba; this is the same fix for the GUI
build path, needed before the Phase B GUI work can be ported here.

CadDocument.hpp: mate_kind comment, mirrored verbatim from snaporca dbaa104f62
so the header stays byte-identical across forks.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 14:35:25 +02:00
Tommaso BianchiandClaude Opus 5 8c6df84acc Merge upstream/main into cad-mainline (530 commits)
Catches the fork up from 449a4cf9fc (2026-06-28) to d6cb667b89 (2026-07-24).
Upstream touched 2326 files; 17 of them overlap the 164 this branch touches.

16 of the 17 auto-merged, including all three CMakeLists.txt, the build_all.yml
CI workflow, and every GUI file. The CAD core never conflicts: CadDocument,
SketchEngine, SketchSolver, McpControl and test_caddocument are files this fork
adds, so upstream does not touch them.

The one conflict, tests/libslic3r/test_3mf.cpp, was purely additive in all three
hunks and is resolved as a union: our test pinning that store_bbs_3mf embeds the
CAD recipe as Metadata/SnapOrca_cad.bin, upstream's multi-nozzle plate-metadata
round-trip tests, and both sets of includes. All three were verified present
after resolution rather than assumed.

NOT BUILD-VERIFIED, for a reason that predates this merge and is not caused by
it: this fork cannot be configured on nativedev at all. Its CMakeLists has
required Eigen3 5.0.1 since before the merge (line 592 pre-merge), while the
only deps image on the machine is snaporca-deps, built for snaporca's
find_package(Eigen3 3.3). CMake fails at configure, so nothing compiles.

That means this fork's Catch2 suite has never run. Every "suite green" figure
recorded for M1-M8 was snaporca's suite; the ports were verified by patch-apply
plus the CAD sources being byte-identical to snaporca's. Building an orca_cad
deps image with Eigen 5.0.1 is what would finally close that gap.

Pre-merge state is preserved at branch cad-mainline-pre-upstream-2026-07-25
(30d54f0074).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 12:28:33 +02:00
Tommaso BianchiandClaude Opus 5 30d54f0074 M8c: interference detection
check_interference(min_volume) reports every pair of solid bodies whose
intersection encloses more than min_volume, as {body_a, body_b, volume}. It
reports only — no geometry is mutated, so calling it cannot disturb mates or
placements. Read-only at the MCP surface too: no checkpoint, no recompute.

Sheet bodies are skipped up front: an intersection involving one encloses no
volume, so the boolean would be wasted work. Bodies that merely touch share a
face and enclose nothing, so face-to-face contact is not an interference.

A boolean that fails on one pair must not lose the report for every other pair,
so each pair is guarded — and OCCT raises Standard_Failure, which is not a
std::exception and would otherwise escape.

No new serialized fields, no recipe bump: this reads `bodies`, which is
recompute output and was never serialized.

No separate MCP listing for instances and mates: describe_scene already emits
the feature tree, and Mate has rendered there correctly since M8a fixed
feature_type_name.

Tests assert the exact overlap volume (20*20*4 = 1600 mm^3), both negative cases
(clearly apart, and exact face contact), that sheets are skipped, that the
min_volume gate silences a real overlap, and that a clash created by a Fastened
mate is detected — which ties the M8b placement work to this report.

Suite 139 cases / 1960 assertions green. McpControl.cpp is reviewed but not
compiled by kernel-test.sh, which builds only libslic3r_tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 12:15:30 +02:00
Tommaso BianchiandClaude Opus 5 6a0031c9e5 M8b: Revolute / Slider / Cylindrical mates
Each kind constrains the DOFs it owns and PRESERVES the rest from the body's
current pose, following the pattern Planar established in M8a. Resolved instead
as "Fastened with a parameter", all three would have been geometrically
identical to Fastened — relabelling rather than behaviour.

  Revolute     fixes position on the axis line; rotation about it survives
  Slider       fixes orientation and perpendicular position; axial position survives
  Cylindrical  fixes the axis line only; rotation and axial position both survive

No new serialized fields, no recipe bump, no fixture regeneration: mate_kind is
already an int and mate_offset / mate_angle already exist.

The minimum-rotation z-alignment (including the antiparallel 180 deg case fixed
in M8a) is now a shared make_z_align lambda rather than a second copy.

Fixes a rotation-about-pivot bug found by the no-op tests: R_full was built as a
rotation about the origin with a translation to oB appended, instead of a proper
rotation about oB (translation = oB - R*oB). It moved bodies that were already
correctly placed, and accounted for three of the seven initially failing cases.

Testing notes, both of which cost real debugging time here:

- Mates are defined on connector FRAMES, but the convenient thing to measure is
  CentreOfMass(), and the two coincide only when the body is symmetric about its
  connector. Five expectations in this milestone asserted the centroid while
  meaning the connector. These tests assert on the mated face's centroid.

- A CoordSys built from a face ALONE takes its z from the face normal (which
  follows the body) but its x from coordsys_x_hint, a world constant. Such a
  frame cannot see rotation about its own normal, so no mate can correct or
  preserve a spin it does not encode. The Slider and Cylindrical rotation tests
  pin coordsys_edge to an edge of their own body; without that both passed
  vacuously, one of them for a wrong implementation.

The Cylindrical rotation test was verified to fail when its mate kind is mutated
to Slider, and the Slider test failed at axis_aligned == 2 before the connectors
were edge-pinned. Neither is green by accident.

Known wart: mate_angle is silently ignored for Slider, whose rotation is fully
constrained. Defensible but undiagnosed at the API surface.

Suite 134 cases / 1927 assertions green. McpControl.cpp is reviewed but not
compiled by kernel-test.sh, which builds only libslic3r_tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 11:30:37 +02:00
Tommaso BianchiandClaude Opus 5 b13ca01ccc M8a: assembly mates — Fastened + Planar (recipe v3)
An assembly is a multi-body document. An instance is already expressible as
Transform with xf_copy=true, and a mate connector is already a CoordSys
feature, so this adds exactly one feature type: Mate.

No constraint solver. A mate rigidly transforms the body carrying connector B
so that B's frame lands on connector A's, applied in feature order like every
other feature. Chains resolve by composition; closed kinematic loops do not
converge (last mate wins) and are out of scope.

The vendored SolveSpace in src/libslic3r/slvs/ was evaluated for 3D extension
and rejected: it is built and linked but has zero callers, and SketchEngine's
solver is hand-rolled. Extending it would mean adopting a dependency to write
more code than the alternative.

- CadFeatureType::Mate appended; six fields (mate_kind, mate_cs_a, mate_cs_b,
  mate_offset, mate_angle, mate_flip) appended at the END of both cereal lists
- SNAPORCA_CAD_RECIPE_VERSION 2 -> 3; v2 blobs are rejected, as by design there
  is no migration path. Golden fixture renamed to cad_recipe_v3.bin and
  regenerated once, extended with two CoordSys + one Mate so the new fields are
  tripwired by the field-order assertions
- datum_frame() extracted from resolve_datum_coordsys() so a mate can resolve
  its connectors against the in-progress bodies vector during replay
- apply_mate dispatched early-return, so Mate is deliberately absent from
  starts_new (unreachable for that dispatch style)
- Planar: the degenerate branch splits on the sign of zB.z_target — antiparallel
  needs a 180 deg rotation about a perpendicular axis, which an earlier revision
  silently skipped, leaving the body's normal inverted
- MCP: mate command, named bare to match the other 38 methods

Drive-by: feature_type_name() was missing Mirror, ThickenSurface, SurfaceOffset,
SurfaceLoft and SurfaceFill, which reported as "Unknown" to MCP clients.

Suite 122 cases / 1741 assertions green. Note that kernel-test.sh builds only
libslic3r_tests, so McpControl.cpp is reviewed but not compiled here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 10:51:22 +02:00
Tommaso BianchiandClaude Opus 4.8 9c28be5860 M7c: SurfaceLoft + SurfaceFill (open skins from profiles / a boundary)
The two remaining ways to create a sheet body. SurfaceLoft skins 2+ profile
sketches without end caps via a new SketchEngine::make_loft_surface — a
sibling of make_loft with the ThruSections solid flag false, so no existing
call site changes. SurfaceFill patches a single closed boundary wire into a
smooth face with BRepOffsetAPI_MakeFilling, adding each boundary edge as a
C0 constraint.

Purely additive: two enum values appended to CadFeatureType, reusing the
existing loft_profile_refs/loft_ruled and sketch_ref fields. No new cereal
fields, recipe stays v2, golden fixture unchanged (30773). MCP
surface_loft/surface_fill added as pure additions. Suite 107 cases / 1553
assertions green, including a test contrasting the open skin against the
solid loft of the same profiles.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
2026-07-25 09:56:46 +02:00
Tommaso BianchiandClaude Opus 4.8 2da75d28ca M7b: thicken-from-surface + surface offset
Two features bridging sheet bodies back to solids and to other sheets:
ThickenSurface feeds a whole sheet shell to MakeThickSolidBySimple (the same
OCCT recipe the face-level Thicken already uses) and appends the result as a
solid; SurfaceOffset offsets a sheet's shell along its normals via
MakeOffsetShape::PerformBySimple, keeping it open. Both refuse a non-sheet
target with a clear error.

Purely additive: two enum values appended to CadFeatureType, reusing the
existing target_body / thicken_thickness / thicken_flip / plane_offset
fields. No new cereal fields, recipe stays v2, golden fixture unchanged
(30773). MCP thicken_surface/surface_offset added as pure additions.
Suite 103 cases / 1520 assertions green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
2026-07-25 09:41:51 +02:00
Tommaso BianchiandClaude Opus 4.8 01e474e17c M7a: surface bodies — SurfaceExtrude + SurfaceRevolve (open shells)
Two body-producing features that emit an open shell instead of a capped
solid: SurfaceExtrude (prism of a sketch wire, no end caps) and
SurfaceRevolve (revolve of a wire about an in-plane axis, no caps). Each
appends a new sheet body whose TopoDS_Shape has no TopAbs_SOLID.

Purely additive: two enum values appended at the end of CadFeatureType,
reusing existing serialized fields (sketch_ref/distance,
revolve_angle/revolve_axis). No new cereal fields, recipe stays v2, golden
fixture unchanged. is_sheet_shape() derives sheet-ness from the OCCT shape
type (bodies are not serialized). MCP surface_extrude/surface_revolve added
as pure additions. Suite 99 cases / 1474 assertions green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
2026-07-25 09:19:02 +02:00
Tommaso BianchiandClaude Opus 4.8 15ea0a813f M6: Variables & equations — parametric expressions driving feature dimensions
Add named document variables (CadDocument::variables) and per-feature
expression bindings (CadFeature::expr, field-name -> expression). On
recompute(), variables are evaluated topologically (cycle detection), then
each feature's expr entries are evaluated and written into its numeric fields
before geometry runs. Self-contained shunting-yard evaluator (+ - * /, parens,
unary minus, sqrt/abs/sin/cos/tan(deg)/min/max, pi). assign_field allow-lists
the 33 dimension fields + pattern_count; unknown names error loudly.

Additive: recipe stays v2 (fields appended to both cereal lists, golden
fixture regenerated). MCP set_variable / set_feature_expr are pure additions.
Suite 95 cases / 1440 assertions green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
2026-07-25 08:11:06 +02:00
Tommaso BianchiandClaude Opus 4.8 aa30575369 M5c: pattern-on-curve — replicate a body along a sketch curve
Extend CadFeatureType::Pattern (no new enum) with a curve mode: when
pattern_curve_sketch >= 0 it takes precedence over linear/circular. The guide
entity is sampled at equal-parameter points via a file-local sample_entity_2d()
(Line lerp, Arc angle-lerp, cubic-BSpline Bernstein, p0->p1 fallback), and each
seed copy is translated by (P_i - P_0) and fused. Two serialized fields
(pattern_curve_sketch/pattern_curve_entity) appended to both symmetric cereal
lists (version stays 2, golden fixture regenerated 30269->30517). MCP:
pattern_on_curve. 3 new [CadDocument][pattern] tests; suite 89/1395.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
2026-07-25 04:15:31 +02:00
Tommaso BianchiandClaude Opus 4.8 a606dfe00a M5b: rib — thin stiffening wall grown from an open sketch line
New CadFeatureType::Rib (appended). A straight open Line entity in a sketch
is offset ±thickness/2 along its in-plane perpendicular into a thin rectangle,
extruded rib_depth along the sketch-plane normal, and fused to the target body.
Line-only for now (ponytail; polyline/arc ribs are a later extension) — a
non-line entity fails cleanly at recompute. Four serialized fields
(rib_sketch_ref/rib_entity/rib_thickness/rib_depth) appended to both symmetric
cereal lists (version stays 2, golden fixture regenerated 29525->30269). MCP:
rib. 3 new [CadDocument][rib] tests; suite 86/1376.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
2026-07-25 04:03:52 +02:00
Tommaso BianchiandClaude Opus 4.8 0bbf22ceae M5a: hole standards library — counterbore/countersink + ISO/ANSI table
Extend CadFeatureType::Hole (no new enum value) with a style flag
(simple/counterbore/countersink) and the matching geometry: a coaxial
shallow cylinder cut for counterbores, a cone-frustum cut for countersinks.
A file-local hole_std_lookup() resolves screw designations (ISO 273/4762/
10642 metric M3–M10 + common ANSI unified) into clearance/cbore/csink dims;
add_hole_standard() fills the feature from it, add_hole_styled() takes them
explicitly. Six serialized fields appended to both symmetric cereal lists
(recipe version stays 2, golden fixture regenerated 28161->29525). MCP:
hole_styled, hole_standard. 4 new [CadDocument][hole] tests; suite 83/1351.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
2026-07-25 03:52:01 +02:00
Tommaso BianchiandClaude Opus 4.8 e19e51b150 M4: delete-face direct edit — remove faces and heal via OCCT defeaturing
New CadFeatureType::DeleteFace: removes a set of global face ids from target_body
and heals the gap via BRepAlgoAPI_Defeaturing (TKBO, already linked), mirroring the
Shell/Draft body-modifying pattern. delete_faces appended to both symmetric cereal
lists (recipe version stays 2, golden fixture regenerated 27913->28161). MCP
delete_face method (pure additions). 3 new [CadDocument][deleteface] tests: remove a
fillet face restores the sharp-box volume, bad index fails safely, round-trip.
Full kernel suite green (79 cases, 1309 asserts).

Move-face / replace-face deferred to snaporca-3c4 / snaporca-tc6 (no clean shipping
OCCT direct-modeling primitive; need research, and replace-face depends on M7 surfaces).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
2026-07-25 03:28:25 +02:00
Tommaso BianchiandClaude Opus 4.8 65caa2ea6e M3c: 2D bridging curve — cubic-Bezier G1 connector between sketch endpoints
Adds SketchEngine::make_bridge (4-pole cubic Bezier, G1-tangent to Line/Arc
endpoints, straight-line fallback for other types) emitted as the existing
BSpline SketchEntity — no new geometry type, no serialized-field change, golden
recipe fixture untouched. CadDocument::add_bridge appends it (non-parametric,
index-validated, throws on bad refs). MCP `bridge` method mirrors action_project.
4 new [CadDocument][bridge] tests; full kernel suite green (76 cases, 1280 asserts).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
2026-07-25 01:59:47 +02:00
Tommaso BianchiandClaude Opus 4.8 161d006e92 CAD: Project feature — convert solid edges into a parametric sketch
Onshape-style "Use / Convert entities": pick edges (or a whole face) of an
existing solid and get sketch geometry projected onto a target plane, then
extrude/revolve/edit it like any sketch. Parametric: apply_project re-derives
the feature's entities from the source body on every recompute, so editing the
source updates the projection.

Line edges -> Line entities (exact); circles/arcs whose plane is parallel to
the sketch plane -> Circle/Arc (exact); everything else (incl. non-parallel
circles that project to ellipses) -> sampled Line chain.

Append-only: new enum value Project + project_source_body/project_edges/
project_face fields at the end of save/load; recipe version stays 2. Recompute
loop made non-const solely so apply_project can write back f.entities.

Suite 67->71 cases, 1178->1229 assertions, RC=0. Fixture 25493->26835 B.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
2026-07-24 23:42:40 +02:00
Tommaso BianchiandClaude Opus 4.8 07b39d45cb CAD: split a body by a picked face; add the missing both-halves cut test
Split-by-face reuses the existing Cut feature rather than adding a new type:
two appended fields (cut_face_body, cut_face) let apply_cut derive the cut
plane from a picked face via SketchPlane::from_face when cut_face >= 0,
otherwise it keeps using the base `plane`. cut_offset / cut_flip still apply
along the derived normal, so the same square-wire split machinery handles
both cases. add_split_by_face() is the convenience entry point; MCP gains a
`split` method (body / face_body / face / keep_upper / keep_lower).

Serialization stays append-only — cut_face_body, cut_face appended to
save/load, recipe version unchanged at 2.

Tests: the previously-missing both-halves plane cut (keep_upper && keep_lower
=> two bodies whose volumes sum to the original), split-by-face via a
top-face plane offset into the interior, keep-upper-only, and a round-trip.
Golden fixture regenerated with a GoldenSplit cut-by-face feature and exact
field-value assertions. Suite 63 -> 67 cases, 1119 -> 1178 assertions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
2026-07-24 21:46:24 +02:00
Tommaso BianchiandClaude Opus 4.8 9da5851534 CAD: Thicken feature — offset a face into a thin solid plate
Pick a face of an existing body, offset it by a wall thickness along its
normal, and append the resulting thin solid as a new body. Onshape-parity
Tier-2 item; the kernel had Shell (hollow a whole solid) but no way to turn
a single face into a plate.

Kernel: CadFeatureType::Thicken, add_thicken()/apply_thicken() as a
body-level op next to Transform/Mirror. The picked face is wrapped in a
TopoDS_Shell and offset via BRepOffsetAPI_MakeThickSolid::MakeThickSolidBySimple;
the result is orientation-normalised to positive volume (same convention as
apply_mirror). Serialization stays append-only — thicken_face,
thicken_thickness, thicken_flip appended to save/load, recipe version
unchanged at 2.

MCP: `thicken` method (body/face/thickness/flip) plus the missing
feature_type_name() case.

Tests: 6 new [CadDocument] cases (plate volume within 1%, flip direction,
bad face id, zero thickness, fuse-with-source, round-trip). Golden fixture
regenerated with a GoldenThicken feature and exact field-value assertions.
Suite 57 -> 63 cases, 1054 -> 1119 assertions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
2026-07-24 18:34:15 +02:00
Tommaso BianchiandClaude Opus 4.8 0100c95ed1 CAD: Transform feature — move/rotate a body as a real B-rep operation
Until now move and rotate lived only in the GUI as m_body_xform, a display
transform. That made them a correctness hole, not a missing tool: a moved body
recomputed and booleaned at its ORIGINAL position, and the move was not in the
recipe at all, so it vanished on save/reload. Only export_step consulted the
transform, which is why the discrepancy stayed hidden.

CadFeatureType::Transform makes it a real feature: rotate angle_deg about
xf_axis through xf_pivot, then translate by xf_translate, applied to the target
body with BRepBuilderAPI_Transform. xf_copy=true keeps the source and appends
the transformed body instead of mutating in place, which covers Onshape's
Transform/copy in the same feature.

Rotation is composed before translation (trsf = tr * rot) so the pivot means
what a user expects — the point the body turns about, not a point that then
drifts with the translation. A rotation with a degenerate axis is refused
rather than silently skipped; a zero angle skips the rotation entirely so a
pure move needs no axis at all.

The decisive test is not the bbox arithmetic but "moved body participates in a
later boolean at its new position": two coincident boxes, one moved to partial
overlap, fused. The fused volume must be strictly greater than one box (the
move took effect in the kernel) and strictly less than both (they still
intersect). With a display-only transform the first assertion fails.

Serialization stays append-only; recipe version unchanged at 2. Golden fixture
regenerated with a GoldenTransform feature carrying distinctive literals so a
field reorder shows up as obviously wrong values. Also fills in the Helix arm
of feature_type_name(), missing since the helix commit.

Kernel suite 51 -> 57 cases, 985 -> 1054 assertions, green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
2026-07-24 17:50:08 +02:00
Tommaso BianchiandClaude Opus 4.8 017bb08a1d CAD: helix / spiral curve, consumable as a Sweep path
Adds CadFeatureType::Helix — the missing input for Sweep. Sweep already
existed but could only follow a sketch, so springs, coils, augers and
non-standard-pitch threads were unreachable. Helix + the existing Sweep now
gives all of them with no further work.

Built the OCCT way: a 2D line on a Geom_CylindricalSurface (Geom_ConicalSurface
when helix_taper_deg != 0) turned into an edge and lifted to 3D with
BRepLib::BuildCurves3d — a true analytic helix, not a sampled polyline, so a
swept spring is smooth rather than faceted. The axis is the plane normal
through the plane origin, matching how Revolve and Plane already work.

Sweep's path resolution is widened to accept either a Sketch (unchanged
behaviour) or a Helix, and rejects anything else with a clear error. Helix
itself is skipped in route_feature and recompute — like the datum features, it
produces no body and exists to be consumed.

Invalid input is refused rather than approximated: non-positive radius or
pitch, negative height, a turn count above 10000 (which would hang OCCT), and
a taper that would drive the radius negative before reaching the top all fail
with a specific error.

Serialization: helix_radius/pitch/height/left_handed/taper_deg appended at the
very end of both save and load, identical order, after the coordsys block.
SNAPORCA_CAD_RECIPE_VERSION stays 2; Helix is appended to the end of
CadFeatureType. Golden fixture regenerated with distinctive literals and
field-value assertions; all pre-existing assertions pass unchanged.

Tests assert analytic values. The one that actually proves it is a helix and
not a circle or a spiral: arc length of r=5 pitch=2 height=10 measured with
BRepGProp::LinearProperties against 5*sqrt((2*pi*5)^2 + 2^2), WithinRel 1e-3.
Plus bounding box (2r in X and Y, height in Z), the conical top radius, the
left-handed winding compared at equal parameter, and the integration test:
a circle r=1.5 swept along a 5-turn helix gives one valid solid of ~1115 mm^3
(WithinRel 0.1 — pipe sweeping is not exact).

MCP: `helix` method registered in describe_tools().

Ported from snaporca 6cdc6b5459. Two fork-specific adjustments: the two new
error-message assertions use Catch2 v3's ContainsSubstring (v2's Contains does
not exist here), and the golden fixture is copied rather than regenerated
because this fork cannot be compiled locally — make_golden_doc_v1() is
byte-identical across both forks, so the blob is provably the same.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
2026-07-24 16:39:36 +02:00
Tommaso BianchiandClaude Opus 4.8 93e80bc407 CAD: datum axis and datum coordinate system
Adds CadFeatureType::Axis and ::CoordSys — reference geometry that produces
no solid, modelled on the existing Plane datum feature.

Axis construction methods (AxisType): TwoPoints, FaceNormal,
CylinderCenterline, PlaneIntersection, AlongEdge. The centreline case is the
useful one: it gives a real axis through an existing hole or boss.

Coordinate systems (CoordSysType): PointWorld and FaceAndDirection. The
latter Gram-Schmidts the picked references, so the stored frame is
orthonormal even when the user's X hint is not perpendicular to the face
normal; the third axis is derived by cross product rather than stored, so it
cannot drift out of sync.

Revolve and pattern are deliberately NOT rewired to consume these — this
commit adds the reference geometry only and changes no existing behaviour.

Serialization: all axis_*/coordsys_* fields appended at the very end of both
CadFeature::save and load, identical order, after mirror_keep_original.
SNAPORCA_CAD_RECIPE_VERSION stays 2; Axis and CoordSys are appended to the
end of CadFeatureType so existing type ordinals are unchanged. Golden fixture
regenerated with distinctive non-default literals and field-value assertions
for every new field; all pre-existing assertions pass unchanged.

Tests assert analytic values: two-point axis direction exactly +Z with unit
length, cylinder centreline collinear with Z and on the true axis, parallel
planes fail cleanly, and the Gram-Schmidt frame is orthonormal to 1e-9 with
X x Y == Z. Degenerate input (identical points) fails with a non-empty error
rather than producing NaNs.

MCP: `axis` and `coordsys` methods registered in describe_tools().

Ported from snaporca 242d4efecb. The golden fixture is copied rather than
regenerated because this fork cannot be compiled locally (Eigen 5.0.1 vs the
build image's 3.3); make_golden_doc_v1() is byte-identical across both forks,
so the two fixtures are provably the same blob.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
2026-07-24 15:58:13 +02:00
Tommaso BianchiandClaude Opus 4.8 c980725e9d CAD: mirror body feature (reflect a solid about a plane)
Adds CadFeatureType::Mirror: reflect a target body about a plane using
gp_Trsf::SetMirror + BRepBuilderAPI_Transform. BooleanMode::New keeps the
mirrored copy as its own body (mirror_keep_original decides whether the
source survives); BooleanMode::Add fuses it back into the source, so an
overlapping mirror does not double-count volume.

Serialization: mirror_keep_original is appended at the very end of both
CadFeature::save and load (append-only contract). The mirror plane reuses
the existing `plane` member and the body selector reuses `target_body`,
as Cut already does. Golden fixture regenerated at the current
SNAPORCA_CAD_RECIPE_VERSION = 2; the existing field-value assertions all
still pass unchanged, and the reorder tripwire was re-verified after
regeneration (swapping draft_face/draft_angle in `load` alone still
fails the golden test).

Tests assert analytic values: mirrored volumes equal (8000 each) with the
reflected centroid, Add on a non-overlapping asymmetric body gives exactly
2x volume, Add across an intersecting plane gives strictly less than 2x,
and an invalid body index fails cleanly with a non-empty error.

MCP: `mirror` method registered in describe_tools().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
2026-07-24 11:52:29 +02:00
Ian Chua 43725128d3 chore: make console logging optional (#14932)
# Description

Adding optional flag for enabling console logging introduced in #14439.
Console logging not working with LLDB-DAP on VSCode as per #14897
Code changes won't fix this because it is a LLDB-DAP issue documented in
#14909

Use flag `-DUSE_SLIC3R_CONSOLE_LOG=ON` or `-DUSE_SLIC3R_CONSOLE_LOG=OFF`
to enable/disable it.
Alternatively in you VSCode's settings.json, 
```
"cmake.configureSettings": {
   "USE_SLIC3R_CONSOLE_LOG": "OFF"
}
```

<!--
> A guide for users on how to download the artifacts from this PR.
-->

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-07-24 14:23:21 +08:00
Ian Chua 1ce21c98ac chore: make console logging optional 2026-07-24 12:22:57 +08:00
Tommaso BianchiandClaude Opus 4.8 b807be3c4a CAD mass properties: volume / area / centre of mass / inertia
Port of snaporca c2821af783. New GeometryEngine::mass_properties over BRepGProp
(separate VolumeProperties/SurfaceProperties), bounds-checked
CadDocument::body_mass_properties, and a mass_properties MCP method. Query-only:
no CadFeature, no serialization, no version change. Analytic tests
(WithinRel/WithinAbs): cube 8000/2400/COM(0,0,10)/inertia 533333, cylinder
500pi/300pi, hollow = solid-500pi, invalid index -> valid=false.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
2026-07-23 23:18:42 +02:00
Tommaso BianchiandClaude Opus 4.8 8edc12c8f1 CAD recipe v2: legible version-mismatch errors, refuse old files cleanly
Port of snaporca 04c3d579a7. Bump SNAPORCA_CAD_RECIPE_VERSION 1 -> 2;
deserialize_recipe sets a user-facing error distinguishing too-new / too-old
/ corrupt instead of a silent bare false. No per-version migration by design.

Golden fixture regenerated at v2 (v1 retired), field-value reorder tripwire
unchanged. Fork adjustment: Catch2 v3 string matcher ContainsSubstring
(not v2's Contains) in the two new version-mismatch tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
2026-07-23 22:32:56 +02:00
SoftFever 5b32213d44 Silence CMake configure warnings
Remove DEPENDS/empty-COMMAND args that are invalid in the
add_custom_command(TARGET) form (CMP0175), fix the FindDraco.cmake case
mismatch, and opt Boost lookup into upstream BoostConfig via CMP0167 for
the OpenVDB module and the CGAL find.
2026-07-24 00:56:56 +08:00
Kris Austin f23ad64303 fix: infill rotation template crashes and misbehaves on raft prints (#14894) 2026-07-22 17:51:55 -03:00
Kris Austin b8cfa32a41 fix: multi-material slicing crash with line width 0 (#14455) 2026-07-22 16:49:37 -03:00
Kiss Lorand 5edd4963eb Fix acceleration/jerk state after custom G-code (#14613) 2026-07-22 16:30:43 -03:00