Commit Graph
29509 Commits
Author SHA1 Message Date
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 62d39fba27 test(cad): lock construction-geometry flag with real regression tests
The construction flag was already honored (excluded from the extrude wire in
SketchEngine.cpp, participates in the solver, and serialized) but the existing
"construction line excluded" test was a false tripwire: its construction line
ran corner-to-corner inside the square, so the bbox was unchanged whether or not
the line was excluded.

- Strengthen that test: the construction line now runs (-30,0)->(30,0) outside
  the profile, so an exclusion regression breaks the closed wire / bbox.
- Add a serialize/deserialize round-trip test asserting construction survives.
- Lock the flag on-disk: add Sketch_Ctor to the golden fixture with a real edge
  + a construction edge, and assert both flags survive the binary recipe.

Test-only; no kernel change. Recipe version stays 2 (construction was already a
serialized field). Suite: 72 cases / 1246 assertions green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
2026-07-25 00:27:16 +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
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
Tommaso Bianchi d04b02cd0e test: golden on-disk fixture that actually detects a serialization reorder
CadFeature::save/load is append-only by contract, and ~20 planned features
each append fields. The existing roundtrip test cannot police that: it writes
and reads with the same code, so any self-consistent ordering passes. Only a
blob written by older code and stored on disk can detect that the format moved.

The first attempt at this test passed while the defect was present. I proved
it by swapping draft_face (int) with draft_angle (double) in both save() and
load() -- a genuine byte-layout change -- and it still reported 613 assertions,
exit 0. It asserted only derived geometry: body count, per-body volume, feature
types. The golden document had no Draft feature, so those fields sat at their
defaults, the reorder scrambled values nothing read, and the recomputed solids
came out byte-identical.

So the fixture now asserts the DATA, not what the data produces:

- make_golden_doc_v1() builds 22 features across 14 types (Draft, Shell,
  Revolve, Pattern, Cut, Hole, Chamfer, Fillet, Extrude taper/symmetric,
  Thread, Sweep, Loft, Boolean, Plane) with distinctive non-default literals
  (draft_angle 7.25, shell_thickness 1.375, revolve_angle 217, pattern_count 5)
  so a reorder produces visibly wrong values rather than swapped defaults.
- Layer 1 reads the committed blob with raw cereal and asserts field by field,
  independent of recompute, so a geometry regression cannot mask a format break.
- Layer 2 keeps the geometry checks as a separate concern.

Verified to trip, twice, by deliberate breakage rather than by assertion:
  draft_face   <-> draft_angle   -> draft_face reads 1075642368 (0x401d0000),
                                    the high half of double 7.25
  revolve_angle <-> revolve_axis -> revolve_angle reads 0.0, not 217.0
Both revert clean to 758 assertions / 30 cases.

Also scoped the "regenerate the fixture" hint to the feature-count check only.
It was in scope for every assertion in the block, so a detected reorder told
you to run [.regen] -- which would bake the corrupted layout in as the new
golden and permanently disarm the test. A guard must not advise disabling
itself.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q

Ported from snaporca f9e0f99bcb. The patch needed fuzz: this fork's copy of
test_caddocument.cpp carries a Catch2 v3 include, a `using Catch::Approx`, and
an extra [Deviation] case appended after line 1611 -- exactly where these hunks
land. Verified after applying: new symbols present, the [Deviation] case
intact, braces balanced, and only WithinAbs/WithinRel used (both exist in v3).
Compile and test verification here is CI, not local: this fork needs Eigen
5.0.1 while the local deps image ships 3.3.
2026-07-23 14:41:31 +02:00
Tommaso BianchiandClaude Opus 4.8 3621785984 build: headless CAD-kernel test loop, and quarantine two broken tests from it
Adds scripts/kernel-test.sh: build the libslic3r_tests target and run the CAD
kernel tags, exit code as the whole contract. Clean build 4m36s, incremental
18s, no display needed -- every [CadDocument] case builds a CadDocument,
recompute()s it and asserts on geometry.

Four things this had to get right, each found by it going wrong first:

- SLIC3R_GTK=3 and BUILD_TESTS=ON are mandatory. src/CMakeLists.txt turns
  SLIC3R_GTK into 'wx-config --toolkit=gtk<N>', so omitting it asks for
  toolkit "gtk", nothing matches, and configure dies with the thoroughly
  misleading "Could NOT find wxWidgets" -- while wx-config sits right there
  in the deps prefix, working. BUILD_TESTS=ON is what creates the target.
- Configure runs unconditionally. Guarding on "CMakeCache.txt exists" is
  wrong because a FAILED configure writes that file too, after which the
  guard skips reconfiguring forever and every later run silently reuses the
  poisoned cache, ignoring corrected flags.
- A new build volume is always built clean. Cloning a warm cache from
  another tree is a correctness trap: rsync preserves source mtimes, ninja
  compares them against foreign object timestamps, concludes everything is
  current and relinks stale objects. That produced a binary containing NO
  [CadDocument] tests at all -- while exiting 0. A green run that tests
  nothing is worse than a red one.
- --host builds where the deps image already lives, staged per volume so
  parallel workers never share a tree.

The two [known-broken] tags: "entity constraints: tangent/midpoint/symmetric/
angle" aborts inside the vendored solver (slvs/dsc.h FindById, "Cannot find
handle"), and SIGABRT is fatal to the Catch2 process -- that single case took
the suite down at 12 of 31, so a green baseline was unreachable. The thread
groove case is a plain pre-existing assertion failure. Both are excluded from
the dev loop's default filter ONLY; ctest in CI still runs and reports them,
so neither bug is hidden. Baseline is now 603 assertions / 29 cases green,
which is what makes "I broke nothing" a meaningful statement.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
2026-07-23 13:47:25 +02:00
Tommaso BianchiandClaude Opus 4.8 52a8ca965c docs: capability gap analysis of the Design tab against Onshape
Enumerated from the source rather than from recollection: CadFeatureType and
add_* in CadDocument.hpp, Tool in DesignPanel.hpp, Mode in
DesignSketchTool.hpp, SketchConstraintType + SketchEntity::Type in
SketchEngine.hpp, and the JSON-RPC dispatch in McpControl.cpp.

Findings worth stating up front:

- The 2D sketcher is at or near Onshape parity -- 19 constraints, every entity
  type including B-splines and elliptical arcs, trim/extend/offset/mirror and
  both array kinds. Very little is missing there.
- The gaps are all breadth beyond sketching: assemblies/mates, surface
  modelling, sheet metal, drawings, and variables/configurations.
- The most defensible criticism is the absence of variables and expressions.
  Every dimension is a literal double, so the feature tree is parametric in
  structure but not in value -- "change one number and the model updates" is
  only half delivered. It is also the cheapest Tier 1 item to close.

The doc separates platform capabilities (version control, FeatureScript, FEA,
rendering, cloud PDM) into their own tier rather than counting them as missing
tools: that is Onshape-the-platform, not Onshape-the-modeller, and holding a
slicer tab to it would not be a fair comparison.

One entry is a correctness gap rather than a missing feature: move/rotate body
(m_body_xform) is display-only and never enters the B-rep, so a moved body
exports and booleans at its original position while the viewport shows it
moved.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
2026-07-23 13:10:24 +02:00
Tommaso BianchiandClaude Opus 4.8 8aea63a919 docs: rewrite the upstream brief from measurement, correcting two errors
The 2026-06-21 assessment was written before the persistence work landed and
got two load-bearing facts wrong. Both are corrected here against the branch
itself rather than from recollection:

1. It called OCCT "a dependency mainline OrcaSlicer has never carried" and
   built its whole conclusion on that. False: deps/OCCT/ exists at the
   merge-base, and upstream already links it from Format/STEP.cpp,
   Format/svg.cpp and Shape/TextShape.cpp. The real dependency diff is one
   line -- BUILD_MODULE_ModelingAlgorithms OFF -> ON -- costing a measured
   3.77 MiB of Windows DLLs (TKFillet + TKOffset; TKBool already arrives
   transitively via DataExchange).

2. It described the vendored SolveSpace solver as LGPL. False:
   src/libslic3r/slvs/LICENSE is GPL-3.0. Harmless for us, but a licence
   must not be misstated in a document aimed at upstream.

It also claimed no changes to Model, which stopped being true when 3MF
recipe persistence added a std::string there.

The rewrite replaces prose estimates with counted figures: 138 new files,
23 modified upstream files at +457/-75, nothing deleted, 99.3 % of the diff
in new files. That reframes the ask from "adopt a CAD kernel" to "widen a
build flag you already carry", which is the argument that actually has a
chance upstream.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
2026-07-22 20:16:31 +02:00
Tommaso BianchiandClaude Opus 4.8 2db59bb85a Design: trace the solid-pick path behind SNAPORCA_PICK_TRACE
Selection failures on a real desktop kept looking identical from the UI
whether the ray missed the solid, the click was rejected as a drag, or the
press never reached the tool at all. The status line added in edb1adbfa3
reports WHICH body/face was picked, so it distinguishes "picked" from
"silence" and nothing finer -- not enough to tell those three apart.

This narrates the whole press -> release -> ray path on stderr, one distinct
line per failure mode:

  down x= y=                      the press reached the tool
  up with no pending press        the press was eaten upstream
  up ... drift=N / rejected       the click-vs-drag threshold decided
  ray tris=N -> body= face= t=    the ray reached the solid (or missed)
  no solid data (bodies= mesh=)   the pick pointers were never wired

Gated on getenv("SNAPORCA_PICK_TRACE"), cached in a function-local static,
so a normal build pays one load and prints nothing. It ships enabled-on-
demand because the failing environment is a real X session with a real
mouse, which the headless rig cannot reproduce.

First use retired a wrong theory of my own: the trace showed drift=0 on
three consecutive picks, so the widened threshold in edb1adbfa3 was not
what fixed anything. The reported symptom is best explained by a stale
pre-orcawidgets binary left running alongside the current one.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
2026-07-22 19:06:05 +02:00
Tommaso BianchiandClaude Opus 4.8 edb1adbfa3 Design: widen the solid-pick click threshold, and report what got picked
Ports snaporca-cad f5c7e74e9b.

The LeftUp pick discarded anything moving more than 4 px total since the
press. A hand-held mouse drifts that much during an ordinary click, so
real clicks were thrown away as drags and it read as "selection does not
work". Use 8 px per axis, GTK's own drag threshold.

Also report the pick on the status line (body / face / edge). A solid
pick previously set no text at all, so its only feedback was the viewport
highlight, and a pick that registers but draws faintly looked identical
to one that never fired.

DesignSketchTool.cpp copied verbatim (identical between the forks apart
from this change). DesignPanel.cpp took the status hunk only, since this
fork keeps mainline's Item-based DropDown in feat_dropdown/ToolFlyout.

Not confirmed on hardware yet, on either fork; this fork remains
uncompiled (needs Eigen 5.0.1).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
2026-07-20 20:57:46 +02:00
Tommaso BianchiandClaude Opus 4.8 7858cd6b6a Sketch: open the dimension field on the right monitor, and stop it freezing the view
Ports snaporca-cad c741fb9677.

SketchInlineEditor::open() clamped its position with
wxGetClientDisplayRect(), which describes only the PRIMARY monitor. On a
multi-head desktop (the reporting machine runs 5760x1080 across screens
at +0, +1920 and +3840) a field anchored on the left or right screen was
clamped onto the middle one and left invisible, while m_awaiting_length
made the sketch tool consume every mouse event until it was answered:
orbit and pan died after any sketch, with Enter the only way out. Clamp
to the display the anchor is actually on instead.

Also let drags and the wheel through that freeze, so a field that lands
somewhere unexpected degrades to odd placement rather than a dead
viewport.

Both files were byte-identical between the forks apart from this change,
so they are copied verbatim. Verified on the snaporca side by the user;
this fork is still uncompiled (needs Eigen 5.0.1, which the available
deps image does not provide).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
2026-07-20 10:14:02 +02:00
Tommaso BianchiandClaude Opus 4.8 b3e7d65cca Design: port the Orca-widget rebuild and the mouse-press fix from snaporca
Mirrors snaporca-cad a016dffb48 + 4b828c4798 so the two forks do not drift:

- Sidebar on Orca's widget set: ComboBox for all 25 pickers, StaticBox
  frames for the tool cards / feature tree / bodies list, framed double
  spins, and no stack of full-width action buttons (Prepare's left panel
  is parameters only).
- Toolbar: document group + undo/redo + mode-gated tools, commit far
  right, ordered via keyed slots, at Prepare's 40 px / 4 px geometry.
- Polygon options and a new Move/Rotate card (distance, axis, angle) in
  the sidebar instead of inline in the toolbar row.
- DesignSketchTool: stop consuming the LeftDown over a solid. Orca starts
  a rotate drag on the press, so swallowing it killed orbit/pan whenever a
  body was on screen. The pick now resolves on LeftUp within 4 px.

DesignSketchTool.{cpp,hpp} were byte-identical across the forks and are
copied verbatim. DesignPanel.cpp needed one hand-merge: mainline's
DropDown is Item-based (DropDown::Item{text,tip,icon}, DropDown(items&))
where snaporca passes parallel vectors, so feat_dropdown/ToolFlyout keep
the mainline form. Checked field-by-field against this fork's
Widgets/DropDown.hpp.

scripts/docker-iter-build.sh: mount the root CMakeLists.txt and cmake/
rather than inheriting the baked copies, which silently drops the
SLIC3R_CAD gate, and stop defaulting to snaporca's build volume - sharing
it made the two forks overwrite each other's cache and binary.

NOT COMPILED. This fork needs Eigen 5.0.1 (find_package(Eigen3 5.0.1
REQUIRED)) while the available snaporca-deps image supplies 3.3, so it
needs its own deps build to verify. The behaviour above was verified only
on the snaporca side.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
2026-07-20 07:48:36 +02:00
Tommaso BianchiandClaude Opus 4.8 f4469b8450 Design: use Orca's teal CheckBox instead of raw wxCheckBox
Design drew raw OS wxCheckBoxes (grey square + inline text) where Prepare shows
Orca's teal check, one of the most visible reasons the two panels looked
unrelated. The six sidebar checkboxes (extrude flip, hole through, thread
internal, revolve flip, boolean keep-tool, loft ruled) now use Widgets/CheckBox:
the widget carries no text, so each label moves into the row's left column,
which is also Prepare's row idiom and matches the label/control grids.

CheckBox is a wxBitmapToggleButton, so its per-control Binds and the panel-wide
preview refresh listen for wxEVT_TOGGLEBUTTON as well; boolean/loft get a
label+control row instead of a bare full-width control. Checkboxes align to the
left edge of the control column, as Prepare aligns its own.

The two sketch-toolbar checkboxes are left alone — they live in the top toolbar,
not the left panel.

Verified on :10: Flip direction renders as a teal check with a white tick and
toggles correctly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
2026-07-19 13:15:35 +02:00
Tommaso BianchiandClaude Opus 4.8 d25ff88d76 Design tools: label-left/control-right rows, and drop the empty-state gaps
Second half of the Prepare alignment. The 14 tool forms were plain 2-column
wxFlexGridSizers whose control column never grew, so every control sat at its
natural width right next to its label — nothing lined up, and it looked nothing
like Prepare's "label ......... [value]" rows.

- two_col_form() builds the grid with a growable control column; all 14 forms use
  it and their 56 controls are added with wxEXPAND, so controls fill one aligned
  column at the panel edge.
- The sketch-session Plane row is a box sizer, not a grid, so it gets a stretch
  spacer for the same effect.
- The DoF readout collapses when empty, and the Bodies block starts hidden — both
  reserved a blank line on a fresh document, leaving dead space above the action
  buttons that Prepare does not have.

Verified on :10: Extrude edit shows Extrude dist / End / 2nd dist / Taper /
Result as aligned label+control rows; Plane row right-aligns; no Bodies box on an
empty document.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
2026-07-19 12:23:15 +02:00
Tommaso BianchiandClaude Opus 4.8 7d2a63ffcb Design sidebar: align with Prepare's visual language
The two tabs used different idioms for the same concepts, which reads as two
different apps: Design was 264 px against Prepare's ~467 (the canvas edge jumped
on every tab switch), used bare micro-labels where Prepare uses icon + Head_14
card headers with a rule, hung its row actions on a loose strip under the tree
instead of in the section header, and drew raw OS-default wxButtons next to
Prepare's Orca-styled ones.

- Width now tracks Prepare's sidebar at runtime (sync_sidebar_width() reads the
  live width on tab activation) rather than being hardcoded, so the two cannot
  drift apart if Orca changes its sidebar.
- "Feature tree" and "Bodies" use the card_header() helper the panel already had
  (icon + Label::Head_14) plus a wxStaticLine, exactly as the tool cards do.
- The six row actions moved into the Feature tree header, Prepare-style, at
  header weight (24 px) instead of 36 px control weight.
- Buttons are Orca Buttons (ButtonType::Expanded, full width); Commit to Plate
  gets ButtonStyle::Confirm as the tab's primary action.
- Margins/spacing come from SidebarProps (ContentMargin/TitlebarMargin/
  ElementSpacing) instead of hardcoded 12/6/4.

Verified on :10: sidebars are the same width and share the header idiom.
Still to do: label-left/control-right rows inside the tool dialogs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
2026-07-19 12:09:44 +02:00
Tommaso BianchiandClaude Opus 4.8 b9e927876d Design: move bodies into their own Parts list (Onshape-style)
Bodies were appended INSIDE the feature tree, after the features, and only when
there was more than one body. Two consequences:

- With a single body — sketch + extrude, the common case — no body row existed
  at all, so the solid could not be selected from the tree. That also blocked
  Move (it requires a selected body), the show/hide eye and every body-targeted
  op; the viewport was the only way to select.
- With several features the Bodies group was pushed past the tree's auto-sized
  height (capped at 9 rows) and clipped out of view, so bodies became
  unreachable as history grew.

Bodies now live in their own list under the feature tree, mirroring Onshape's
Features + Parts split that the rest of the tab already follows. The list is
hidden while empty, sizes to its content (scrolls past 6), keeps the selected
row across a recompute, and greys hidden bodies as before. Selecting in either
list clears the other, so only one thing is ever "the target".

Verified on :10: single body -> Body 1 listed, selectable, Move opens the gizmo
on it (previously impossible); two imported solids -> both listed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
2026-07-19 11:35:11 +02:00
Tommaso BianchiandClaude Opus 4.8 8d529ad413 Size the Design move/rotate gizmo from the body, like Orca's Prepare gizmos
The Design gizmo already had both move arrows and rotation rings, but drew them
at a fixed 70 px arm regardless of the body: on a 40 mm cube everything
collapsed into a ~100 px tangle buried inside the solid, so the rings were
effectively invisible and the tool read as "move only, no rotate".

Orca's Prepare gizmos size themselves from the selection's bounding sphere
(GLGizmoRotate3D: m_radius = Offset + sphere radius) so the handles always clear
the object. Same rule here: DesignPanel passes the body's bounding-sphere radius
(scale-aware) into the gizmo, and move_gizmo_arm() returns
max(70 px, 1.25 * radius) — the screen-space floor keeps it grabbable on a tiny
body or when zoomed far out. Ring radius and BOTH hit-tests derive from that one
helper, so picking cannot drift from what is drawn.

Verified on a 40 mm cube: rings now encircle the body, arrow drag moves with a
live mm readout, ring drag rotates live.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
2026-07-19 08:36:26 +02:00
Tommaso BianchiandClaude Opus 4.8 f4160595b0 Kill the remaining Design-tab UI freezes
query_topology: indexing a body face-by-face was quadratic — face_by_index
re-walks the explorer and edge_by_index rebuilds the whole indexed map on every
single call. On a 15.7k-face / 25.6k-edge imported solid this blew past the
MCP 15 s main-thread timeout with the UI frozen throughout. GeometryEngine
gains faces_of()/edges_of(), which enumerate once in the very same order (ids
stay interchangeable with the _by_index accessors, so fillet/up_to_face targets
are unaffected). Measured on that body: 15 s timeout -> 0.46 s.

Feature ops: every commit-time m_doc.recompute() (fillet, cut, shell, boolean,
extrude, ...) now goes through recompute_guarded(), which runs the rebuild on a
worker thread. Live-preview/drag paths stay inline on purpose — yielding inside
a drag would be worse than the stall.

run_off_ui_thread(): the progress dialog is now created only after 300 ms, so a
fast op does not flash a dialog, while input stays blocked (wxWindowDisabler)
for the whole operation since the worker owns the document.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
2026-07-15 00:13:10 +02:00
Tommaso BianchiandClaude Opus 4.8 01aa6903f0 Harden the GL canvases against dropped frames and UI-thread freezes
Viewport: GLCanvas3D::on_idle() cleared m_dirty even when
_refresh_if_shown_on_screen() rendered nothing because the canvas was not on
screen yet — a frame requested while the notebook was still showing a page got
silently swallowed and the viewport stayed blank until some later event dirtied
it again. _refresh_if_shown_on_screen() now reports whether it rendered, and
on_idle keeps the canvas dirty when it did not. Covers Design/Prepare/Preview.

Freeze: a big STEP (17.8 MB) spent ~40-50 s inside OCCT on the UI thread
(read_step_solids + recompute), so the window stopped repainting and the
compositor marked the app unresponsive. Both now run on a worker thread behind
an app-modal pulsing progress dialog: the UI keeps painting and the document
cannot be touched while the worker owns it. OCCT's Standard_Failure is not a
std::exception, so the worker catches it explicitly — an escaping exception
would terminate the process.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
2026-07-15 00:04:25 +02:00
Tommaso BianchiandClaude Opus 4.8 6d9368276c Fix Design tab not painting on hardware GL after a page switch
request_repaint() only invalidates the canvas (Refresh()) on the hardware-GL
path and relies on a wxEVT_PAINT to follow. When the notebook re-shows the
Design page, that invalidation is issued mid-show and dropped: no paint event
arrives, the canvas never renders, and the pane stays blank until another tab
switch forces an expose. Software GL renders directly, so it never showed there.

force_repaint() defers past the show, then Refresh() + Update() for a
synchronous paint. Called from on_tab_shown().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
2026-07-14 23:26:58 +02:00
Tommaso BianchiandClaude Opus 4.8 343a0439f1 Import a triangle mesh as an editable B-rep body (mesh2step port)
Opening an STL/OBJ in the Design pane now rebuilds it into a real OCCT B-rep
solid that the face/edge feature tools can operate on, instead of a print mesh.

GeometryEngine::mesh_to_brep is a native C++ port of mesh2step
(github.com/tommasobbianchi/mesh2step): vertices and edges are shared across
triangles at construction time (vertex cache by deduped index, edge cache by
unordered index pair), so no BRepBuilderAPI_Sewing pass is needed to rebuild the
topology afterwards, and watertightness falls out of the edge-usage counts for
free. An open mesh is returned as a shell and reported as such — never dressed up
as a fake solid.

It runs in-process on the OCCT kernel libslic3r already links, so no STEP file is
written or re-read. That is not an optimisation but the whole point: a faceted
STEP of a 62k-triangle mesh is ~149 MB and OCCT's STEPControl_Reader takes >300 s
to parse it back, so routing this through a file would hang the GUI.

Coplanar neighbours are merged (ShapeUpgrade_UnifySameDomain, 5° default) so the
body arrives with pickable CAD faces rather than one face per triangle — on the
20,656-triangle test part that is 20,614 faces down to 4,784. Without it the
import is technically a solid but nothing you can meaningfully fillet or extrude.

- Design pane: "Import mesh" button + Shift+M; warns above 50k triangles.
- MCP: import_mesh {path, tolerance, merge_angle_deg}, returning the full
  conversion stats so a caller can tell an honest solid from an open shell.
- Catch2: cube round-trip (exact volume, 12 faceted faces, 6 after merge), open
  mesh stays a shell, and the scale-independent sliver rule that a naive
  area < tolerance^2 test would get wrong.

Verified end-to-end on the real 20,656-triangle ir3v2 hotend STL: reproduces
mesh2step's Python run exactly (20,614 kept, 42 degenerate, 0 boundary edges,
2 non-manifold edges, not watertight) and the resulting body's bbox matches the
one FreeCAD reports for the same part.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
2026-07-11 07:02:33 +02:00
Tommaso BianchiandClaude Opus 4.8 c618965a4c Add -DSLIC3R_CAD compile-time gate for the Design/CAD tab
Gate the entire parametric Design/CAD subsystem behind a single CMake
option so the fork can be built with or without it. With SLIC3R_CAD OFF
the build is behaviour-neutral against upstream OrcaSlicer; this is the
"parallel build" for upstream integration discussion.

Gated surface:
- option(SLIC3R_CAD) + add_definitions(-DSLIC3R_CAD)
- deps/OCCT/OCCT.cmake: BUILD_MODULE_ModelingAlgorithms=${SLIC3R_CAD}
  (OFF matches upstream OCCT exactly; ON adds only TKFillet + TKOffset)
- TKFillet/TKOffset link, add_subdirectory(slvs) + libslvs
- CAD kernel + GUI sources, GLGizmoPrimitive (needs GeometryEngine),
  CAD Catch2 tests
- 11 upstream C++ hook sites (GLCanvas3D, MainFrame, GLGizmosManager)
- TabPosition and gizmo EType enums switched to implicit numbering so
  OFF reproduces upstream indices exactly

Verified on behemoth both ways: OFF and ON link snapmaker-orca +
libslic3r_tests (exit 0); ON shows the Design tab and passes 8 [design]
+ 1 [Deviation] tests, OFF omits them and shows upstream's tab layout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
2026-07-10 18:12:53 +02:00
Tommaso BianchiandClaude Opus 4.8 546cef5f42 Windows packaging: assert every linked OCCT toolkit has a DLL to ship
The previous guard only caught an empty glob. That is the wrong invariant.
The glob ships whatever the deps prefix holds, which is not the same as what
libslic3r links.

This fork sets BUILD_MODULE_ModelingAlgorithms=ON; upstream OrcaSlicer sets it
OFF. Upstream's DataExchange module pulls in most ModelingAlgorithms toolkits
transitively, but not TKFillet and TKOffset -- those two exist only because we
turned the module on. So a source build against a deps tree carried over from
upstream has 40 of the 42 OCCT DLLs. The glob copies all 40, the build
succeeds, and the slicer dies at launch with "error 126 (dependency not
found)". Reported by SoftFever, who named exactly those two DLLs.

CI is unaffected: the deps cache key is hashFiles('deps/**'), so flipping the
OCCT flag invalidated it and every shipped artifact has all 42.

Publish OCCT_LIBS from libslic3r as the single source of truth and check each
toolkit has a DLL, so the list can never drift from what we link. Verified
against a simulated stale prefix: the configure fails naming exactly
TKFillet.dll;TKOffset.dll, and passes when both are present.

Windows-only: every call site of the copy function is inside if (WIN32).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
2026-07-10 08:48:11 +02:00
Tommaso BianchiandClaude Opus 4.8 d568e89c34 Windows packaging: fail the configure when the OCCT glob matches nothing
The glob that replaced the hand-maintained OCCT DLL list traded a loud
failure for a silent one. The old code named each DLL explicitly, so an
unpopulated deps prefix made file(COPY) error out at configure time. A glob
that matches nothing instead yields an empty list, copies nothing, and leaves
the install manifest without a single TK*.dll -- producing a package that dies
at launch with "error 126 (dependency not found)".

This only bites source builds whose CMAKE_PREFIX_PATH lacks bin/occt (OCCT's
install layout varies by version); CI populates it, so every shipped artifact
is intact. Restore the loudness rather than ship a slicer without OpenCASCADE.

Windows-only: every call site of the copy function is inside if (WIN32).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
2026-07-10 08:39:56 +02:00
Tommaso BianchiandClaude Opus 4.8 3d9b36f108 Add Romanian (ro_RO) localization
Romanian was absent from this fork and from upstream, so there was no
catalog to reuse. Machine-translated with a local qwen3.6 model against the
union of both forks' .pot files; 99.0% coverage (5472 singular + 9 plural).
Header credits it as machine translation pending native review.

Registering wxLANGUAGE_ROMANIAN in supported_languages[] is what actually
exposes the language: the Preferences dropdown filters the installed
dictionaries against that allowlist, so a valid .mo alone would ship a
language nobody could select.

Translations that failed to preserve their printf/boost placeholders were
left empty rather than shipped, since msgfmt --check-format is fatal in
run_gettext.sh and would break the build on all three platforms. Those 57
strings fall back to English. Verified: run_gettext.sh exits 0 and the
compiled catalog has 0 placeholder mismatches.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
2026-07-10 07:14:13 +02:00
Tommaso BianchiandClaude Opus 4.8 5e272d4f5c CI: build on push to cad-mainline
The push trigger listed main/release/belt-printer but not our working branch
cad-mainline, so pushes to it never auto-built. Add cad-mainline.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
2026-07-03 07:06:18 +02:00
Tommaso BianchiandClaude Opus 4.8 493acb6059 Windows packaging: bundle ALL OCCT DLLs via glob (fix launch error 126)
The portable/installer hardcoded an OCCT DLL list that drifted from what
libslic3r actually links (OCCT_LIBS) + their transitive OCCT deps. The
CAD build links TKFillet/TKOffset/TKBool (and pulls TKFeat/TKBin*/TKIGES/
TKRWMesh/TKSTL/TKVRML/TKXDEIGES/TKXml* transitively) which were absent
from the shipped zip -> OrcaSlicer.dll failed at launch with error 126
(dependency not found). Replace both hardcoded lists (the file(COPY) and
the install manifest) with a glob over the built occt/ dir so the package
can never drift from the build again. Windows-only: OCCT is Shared solely
on Windows (deps/OCCT/OCCT.cmake); macOS/Linux static-link it. Reported by Marc.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
2026-07-03 00:52:05 +02:00
Tommaso BianchiandClaude Opus 4.8 f8175fc9a4 Design: fix hole placement (top-face default) + invisible internal thread
Hole: with no face explicitly picked, the tool fell back to the XY datum at
z=0 (the model's underside), so placing a hole from a top view read parallax-
shifted. Default to the solid's top face (top_face_index_of) so the footprint
sits on the surface being viewed; the XY/XZ/YZ dropdown still overrides.

Internal thread: the bore was re-cut at the nominal radius, which coincides
with an existing hole's wall — the coincident faces fouled the groove boolean
so it removed ~nothing (invisible thread). Cut the bore at the minor diameter
(radius - depth) instead: strictly inside any existing wall, leaving it clean
for the groove; on solid stock it forms the tap-drill.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
2026-07-02 20:18:41 +02:00
Tommaso BianchiandClaude Opus 4.8 2a9a433127 Design: rename 'near' lambda to 'is_near' (MSVC windows.h macro clash)
Windows minwindef.h defines legacy 'near'/'far' as empty macros, so MSVC
mangled `auto near = ...` (C2513) and every near(...) call (C2679/C2678 on
Vec2d). gcc/clang were unaffected, so only the Windows build failed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
2026-07-02 14:04:55 +02:00
Tommaso BianchiandClaude Opus 4.8 3ba6feb7a7 tests: fix hex-escape-out-of-range in CAD recipe 3mf test
gcc reads "\x10cad..." as one escape (c/a/d are hex digits → 0x10CAD > 255).
Split the string literal so the \x10 escape terminates. clang let it slide;
gcc (Linux/Windows CI) errored and blocked the OrcaSlicer build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
2026-07-02 13:06:28 +02:00
Tommaso BianchiandClaude Opus 4.8 1673c2c760 Design tab: keyboard shortcuts, plane/axis toggles, section view
- Keyboard shortcuts (Onshape-style, three scoped layers): feature tools on
  Shift+letter, 2D sketch tools on single letters (in-sketch), view/nav on
  single letters (out-of-sketch). Dispatched via the DesignPanel CHAR_HOOK.
- View toggles: P = origin planes, A = world axis triad (render_view_helpers).
- Section view (non-destructive): a single horizontal clip that hides half the
  model to inspect inside, showing only the solid remaining half (no ghost).
  Left-panel "Section View" button or X toggles it; PageUp/PageDown move the
  plane; "Flip Section" button or F shows the opposite half. Never a body/Cut.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
2026-07-01 23:19:18 +02:00
Tommaso BianchiandClaude Opus 4.8 de16d566b2 Design tab: value-label titles + 5 sketch/UX fixes
Six fixes to the Design tab, all live-verified on :10:
- Datum-plane re-pick: wxEVT_CHOICE on m_draw_plane re-planes the live sketch
- Delete-feature dismisses its lingering settings card (on_delete_feature)
- Revert rotation-orbit regression (no feed_bodies/reload on close_tool)
- Sketch undo/delete via focus-independent CHAR_HOOK (Ctrl+Z/Y, Delete)
- Fix undo hang: reset_autoedit() clears dangling auto-edit sequence
- Titled value fields: each inline dim field shows its role (Width, Height,
  Radius, Angle, Length, Side, Distance, Major, Minor)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
2026-07-01 19:19:23 +02:00
Tommaso BianchiandClaude Opus 4.8 5a005d4728 Design: in-canvas gizmos for Draft/Cut + operand highlight for Boolean/Sweep/Loft
Closes the gizmo-parity gap (bd snaporca-4h5): the five solid features that were
card-only now give in-canvas feedback like their siblings.

Draft — angle-arc drag gizmo (clone of the Revolve gizmo): once a side face is
picked, a cyan arc anchored at the face centroid shows the taper angle; drag the
tip or type the angle, the live ghost tapers with it. Axis = world +Z (the neutral
pull direction), clamped [-89, 89].

Cut — plane offset-arrow + cutting-plane rectangle (clone of the Shell arrow, adds
a wire rectangle in the cut plane sized to the target body bbox). The arrow drags
the signed offset along the plane normal; the rectangle rides at the cut position;
the ghost splits live. (Also covers bd snaporca-1gh / snaporca-mmr.)

Boolean / Sweep / Loft — operand highlighting (new by-index highlight infra):
- Boolean tints the target body teal-green and the tool body orange (per-index
  body tint added to the DesignCanvas GLVolume colour loop + set_operand_bodies).
- Sweep tints the profile sketch cyan and the path sketch magenta.
- Loft tints every selected profile sketch green.
  Sketch tints reuse the DisplaySketch overlay via a feature-index -> colour map
  (sketch_hl_color); DisplaySketch struct unchanged. All self-gate by active tool
  and clear on close_tool.

Wiring mirrors the existing gizmo pattern 1:1 (m_*_active / render_* / set_* /
clear_* / update_* in refresh_preview / DesignCanvas passthroughs / render dispatch
+ on_mouse drag branch). Both forks; DesignSketchTool.{cpp,hpp} + DesignCanvas.hpp
+ DesignPanel.hpp byte-identical across forks. Built clean on both. Draft, Cut and
Boolean highlight live-verified on :10; Sweep/Loft sketch tint is code-complete and
build-clean (visual check pending — needs hand-drawn profile/path sketches).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
2026-07-01 10:19:33 +02:00
Tommaso BianchiandClaude Opus 4.8 8cbf5b393b Design: Measure-style dimension labels (+ projection fix), New Design, tree auto-fit, i18n pin, UX
Sketch dimension labels — now identical to the Prepare/Preview Measure gizmo:
- draw_text repurposed to draw_dim_label: white ImGui text in a translucent-white box,
  mirroring GLGizmoMeasure::render_dimensioning exactly (push_common_window_style sets the
  text colour, BringWindowToDisplayFront, imgui_internal.h).
- ROOT-CAUSE FIX: world_to_screen_px multiplied two Eigen Transform3d objects
  ((proj * view).matrix()); a projection is not affine so Eigen mangled it -> garbage screen
  coords, so labels never appeared. Now proj.matrix() * view.matrix() like Measure. (That
  helper was previously [[maybe_unused]] dead code, never exercised.)
- Leaders: offset clear of the sketch line (no longer coincident with the geometry), single
  point-to-point dimension line + arrows, neutral colour, width 0.6 -> 0.2.
- dim_text appends mm/in on linear dims (angles keep the degree sign).

New Design + delete:
- New "New Design" button wipes the whole document (confirm dialog) — the clear-all the
  per-row Delete can't give. CadDocument::clear() now also clears bodies + display_body_meshes
  (it left them stale, so solids lingered after a clear).
- on_delete_feature: a Body-row selection now shows a helpful hint (bodies are recomputed
  results with no directly-removable feature) instead of silently doing nothing.

Feature tree: auto-fits its content (refresh_tree clamps height 1..9 rows, scrolls past),
instead of a fixed 140px block.

i18n (Design tab pinned English, per the UX contract):
- Restore the lost #undef _L / #define _L(s) wxString::FromUTF8(s) override atop DesignPanel.cpp;
  wrap all ~54 dropdown options in _L so the single lever governs them. feature_type_name left
  untranslated (machine-facing MCP JSON).

UX: per-card Value Confirm/Cancel buttons removed — the single ribbon action bar owns value
confirm/cancel via an m_value_cont guard in tool_confirm/tool_cancel. "needs a body" status
messages unified.

Both forks; DesignSketchTool.{cpp,hpp} + CadDocument.cpp byte-identical across forks. Built
clean; New Design / feature-delete / rotation / tree auto-fit live-verified on :10.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
2026-06-30 23:21:04 +02:00
Tommaso BianchiandClaude Opus 4.8 6e11cfc49b Design: Measure-style dimension labels + restore English-only pin + UX consistency
Dimensions (uniform with Prepare/Preview):
- Repurpose DesignSketchTool::draw_text -> new draw_dim_label that renders each
  sketch dimension as the exact Prepare "Measure" gizmo label: white ImGui text
  in a translucent-white box, positioned via the existing world_to_screen_px
  projection inside the active ImGui frame. All 29 label call sites convert with
  no churn; the bespoke Hershey vector font is retired.
- dim_text appends mm/in on linear dims (angles keep the degree sign).
- Placed-dimension leaders simplified to a single point-to-point line + arrows in
  a neutral colour (no extension lines), matching the Measure look.

i18n (Design tab pinned English, per the UX contract):
- Restore the lost "#undef _L / #define _L(s) wxString::FromUTF8(s)" override atop
  DesignPanel.cpp so one lever de-translates the whole tab, ending the half-EN/IT
  state. Wrap all ~54 dropdown options in _L so the single lever governs them.
- feature_type_name left untranslated (it feeds the MCP JSON, machine-facing).

UX consistency:
- Remove the per-card Confirm/Cancel buttons from the Value card; the single ribbon
  action bar now owns value confirm/cancel via an m_value_cont guard in
  tool_confirm/tool_cancel (one confirm surface, per contract).
- Unify the eight divergent "needs a body" status messages to one template.

Both forks; DesignSketchTool.{cpp,hpp} byte-identical across forks. Built clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
2026-06-30 22:01:54 +02:00