Skip to content

Integrate slippage classification into segment_mesh_zones + zone_continuity_sweep (#109) - #111

Merged
gsdali merged 3 commits into
mainfrom
feat/109-slippage-integration
Jul 22, 2026
Merged

Integrate slippage classification into segment_mesh_zones + zone_continuity_sweep (#109)#111
gsdali merged 3 commits into
mainfrom
feat/109-slippage-integration

Conversation

@gsdali

@gsdali gsdali commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Summary

Closes #109. Integrates OCCTSwiftMesh v1.6.0's Mesh.slippage(forTriangles:maxSamples:) (Gelfand-Guibas local slippage analysis, OCCTSwiftMesh#26/#31) into the two existing zone tools. This is an integration into existing tools, not a new tool — the server still exposes 71 tools (unchanged).

  • Repins OCCTSwiftMesh to >= 1.6.0 (Package.swift/Package.resolved, already staged before this branch was cut).
  • segment_mesh_zones: each zone table entry gains an optional slippage: {kind, axisPoint?, axisDirection?, pitchPerRadianMm?, confidence} field (kind ∈ {plane, sphere, cylinder, extrusion, revolution, helix, freeform}). ZoneRecord gains the matching optional slippage: ZoneSlippage? field (default nil, so a zones.json sidecar written before this PR still decodes — no sidecar version bump). list_zones summaries gain slippageKind: String?.
  • zone_continuity_sweep: a zoneId-scoped sweep with no explicit axis argument now defaults to the zone's own slippage axis when eligible; axisSource gains a third value, "slippage", alongside "explicit"/"pca".
  • serverVersion1.24.0.

Correspondence guard (critical detail carried over from adjacentZones)

segmented()'s MeshRegion.triangleIndices are indexed against the unwelded input mesh, but slippage needs the welded mesh (its classifier reads vertexNormals(), meaningless on unwelded soup). MeshZoneTools already solved exactly this problem for adjacentZones: weld independently, then check welded.triangleCount == mesh.triangleCount — if no triangle was dropped, the same index still means the same triangle in both meshes. This PR reuses that exact same welded mesh and guard for slippage, computing both inside the one guarded branch. When the guard fails, slippage is omitted per zone (alongside the pre-existing empty adjacentZones), with its own warning in the same wording family.

Axis-selection rules (ZoneSweepTool.selectSweepAxis(record:explicit:))

Factored into a pure, geometry-free function (unit-tested directly, no mesh/body needed) resolving axisSource in priority order:

Rung Condition axisSource
1 Explicit axis argument "explicit" (always wins, unconditionally)
2 zoneId-scoped sweep; ZoneRecord.slippage.kind ∈ {cylinder, extrusion, revolution, helix}, non-nil axisDirection, confidence >= 0.25 "slippage"
3 Everything else (no record, no slippage, ineligible kind, or confidence < 0.25) "pca" (unchanged pre-#109 behavior)

Plane is never eligible — its slippage axisDirection is the surface normal, and sweeping a panel along its own normal is exactly backwards, not merely unhelpful. Sphere and freeform have no preferred axis at all. Below the confidence floor, the tool falls back to PCA and appends a warning naming the rejected kind and confidence value. axisDirection's sign is arbitrary (inherent to the underlying eigenvector recovery) and its meaning is kind-dependent — documented in ZoneSlippage's doc comment and in docs/reference/mesh-analysis.md, pulling wording from upstream's docs/algorithms/slippage.md rather than inventing new claims. confidence is a spectral-gap diagnostic, not a probability — a near-symmetric body reads as low-confidence rather than confidently wrong.

Out of scope, documented as the #109 follow-up remainder: revolve-aware angular stationing (stations about the axis instead of along it) for revolution-classified zones.

A fixture-design finding worth flagging for review

While building the "every zone classifies plane" test, a genuinely closed box (all 6 faces welded together, sharing edges/corners — e.g. the existing Shape.box() fixture, or a naively hand-authored closed-box STL) turned out to be an unreliable fixture for plane classification: vertexNormals() at a shared edge/corner vertex blends across the adjacent, differently-oriented face, and at some grid resolutions this pushed a genuinely flat face's classification to sphere/revolution/freeform with high confidence — a consumer-side characteristic of vertexNormals()-driven classification on a connected mesh, not a bug in OCCTSwiftMesh's algorithm itself (independently verified: Shape.box()'s own faces tessellate to just 2 triangles regardless of deflection, below the algorithm's 6-sample floor, for an unrelated reason). The new plane test uses 6 disjoint (small-gap-inset, non-touching) square panels instead, sidestepping both issues; this is documented at length in SlippageIntegrationTests.swift's file header for future readers. The existing (pre-#109) Shape.box()-based tests are untouched.

The mini-carbody fixture (existing, shared by MeshZoneIntegrationTests) exhibits the same shared-edge phenomenon: its front wall is a genuine translational-symmetry-along-Z extrusion but reads as helix here due to boundary contamination from the adjacent top/bottom/cap faces. The "plane never uses its slippage axis" sweep test picks the roof (flat, no ramp) instead of the front wall, and documents why in-line.

Test plan

  • OCCTMCP_FORCE_REMOTE_DEPS=1 swift build — clean.
  • swift build -c release — clean.
  • swift test126/126 passing (up from 113 on main; 13 new top-level @Tests in SwiftTests/OCCTMCPCoreTests/SlippageIntegrationTests.swift, some parameterized across kinds):
    • segment_mesh_zones: slippage classification (#109): a disjoint-panel cube's 6 zones all classify plane with axis parallel to the face normal and sensible confidence; an imported multi-ring open-tube STL's barrel zone classifies cylinder with axis aligned to the tube's own axis.
    • zone_continuity_sweep: slippage axis default (#109): the tube barrel's sweep with no axis argument resolves axisSource == "slippage" with the axis matching the stored ZoneRecord.slippage.axisDirection; an explicit axis still overrides (axisSource == "explicit"); the mini-carbody roof's sweep resolves axisSource == "pca"; a hand-stripped zones.json record (no slippage key at all, simulating a pre-Slippage integration: zone kind/axis in segment_mesh_zones + sweep axis defaults (blocked on OCCTSwiftMesh#26) #109 sidecar) decodes cleanly and its sweep falls back to PCA without error.
    • ZoneSweepTool.selectSweepAxis: pure axis-selection logic (#109): 9 unit tests directly on the pure function covering explicit-override, no-record, no-slippage, all 4 eligible kinds, plane/sphere/freeform exclusion, the low-confidence fallback + warning wording, and the exact-floor boundary.
  • CLAUDE.md's test count updated 113 → 126.

Deviations from the task brief (with justification)

  • The brief assumed "72 stays 72" tools; the actual current tool count on main is 71 (per PingTests.swift), which stays 71 — no new tool added either way, just a stale baseline number in the brief.
  • The scripted Shape.box() fixture (used by the other, pre-existing zone-tools tests) was not reused for the new plane-classification test, for the reasons above; a purpose-built disjoint-panel-cube STL fixture is used instead.

🤖 Generated with Claude Code

gsdali and others added 3 commits July 23, 2026 07:18
…inuity_sweep (#109)

Repins OCCTSwiftMesh to >=1.6.0 (Mesh.slippage(forTriangles:maxSamples:),
Gelfand-Guibas local slippage analysis, OCCTSwiftMesh#26/#31) and wires it
into the existing zone tools:

- segment_mesh_zones: each zone table entry (and ZoneRecord, optional for
  zones.json backward compatibility) gains a `slippage` classification
  (kind/axisPoint/axisDirection/pitch/confidence), reusing the SAME welded
  mesh + triangle-count guard adjacentZones already established for index
  correspondence.
- zone_continuity_sweep: a zoneId-scoped sweep with no explicit axis
  defaults to the zone's own slippage axis when kind is
  cylinder/extrusion/revolution/helix (never plane, whose axis is the
  surface normal) and confidence >= 0.25; axisSource gains a "slippage"
  value alongside "explicit"/"pca". Axis selection is factored into a pure,
  unit-testable ZoneSweepTool.selectSweepAxis(record:explicit:).

Revolve-aware angular stationing remains the #109 follow-up remainder.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ssification

Review finding on #111: on a connected mesh, vertexNormals at a
zone-boundary vertex blend the neighbouring zone's surface in, and the
contaminated constraint rows corrupt per-zone slippage (a small extrusion
panel read as helix; connected box faces read sphere/revolution/freeform,
per this PR's own fixture notes). Zones are now classified from their
interior triangles only (boundary = any vertex touched by another zone or
an unassigned triangle), with a floor (max(24, 25% of the zone)) below
which the full region is kept and the zone is NAMED in a warning rather
than reported clean. Fine/coarse connected L-panel regression tests; the
disjoint fixtures and the tube are unaffected by construction. 128 tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
serverVersion stays 1.24.0; test count reconciles to 133 (128 + #110's 5);
CLAUDE.md's OCCTSwiftMesh bullet keeps the 1.6.0 slippage entry with
mesh_curvature marked consumed; mesh-analysis.md intro unions both
phases' additions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@gsdali
gsdali merged commit 80873f2 into main Jul 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Slippage integration: zone kind/axis in segment_mesh_zones + sweep axis defaults (blocked on OCCTSwiftMesh#26)

1 participant