Skip to content

fit_primitives: RANSAC primitive report over a mesh (#107) - #112

Merged
gsdali merged 1 commit into
mainfrom
feat/107-fit-primitives
Jul 23, 2026
Merged

fit_primitives: RANSAC primitive report over a mesh (#107)#112
gsdali merged 1 commit into
mainfrom
feat/107-fit-primitives

Conversation

@gsdali

@gsdali gsdali commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Summary

segment_mesh_zones vs. fit_primitives. segment_mesh_zones' dihedral region-growing only ever absorbs edge-ADJACENT neighbours, so a cylindrical barrel interrupted by a boss reads as two-or-more zones there even though it's one cylindrical surface. RANSAC instead claims GLOBAL inliers — every triangle within tolerance of a fitted candidate counts wherever it sits in the mesh — so it can unify a primitive the zone table keeps split. That's the reverse-engineering question ("does this same primitive recur elsewhere") a per-region zone fit can't answer.

Zone scoping. Optional zoneId (from segment_mesh_zones) scopes the fit to just that zone's triangles, resolved via the identical path zone_continuity_sweep uses (re-mesh at the zone's own stored deflection, MeshSignature staleness check, subMesh).

Strategy. "ransac" (default) or "auto" (segmentedAutoSelect's dihedral-vs-RANSAC substantial-clean-coverage bake-off; strategyScores reports both scores plus chosen).

uncoveredFraction vs. a maxPrimitives cap — kept strictly separate. The tool always calls the upstream primitive with an unbounded region count, so uncoveredFraction reflects only "no primitive, at any cap, ever claimed this triangle." maxPrimitives is then applied against the already largest-first-sorted result, with its own separate warning naming the triangle count it trimmed — never folded into uncoveredFraction. Passing maxPrimitives straight into the library's own maxRegions would conflate the two (per the library's own docs), which is exactly the ambiguity this design avoids.

Other changes in this PR:

Test plan

  • OCCTMCP_FORCE_REMOTE_DEPS=1 swift build — clean, confirms Package.resolved pins OCCTSwiftMesh at 1.7.0.
  • OCCTMCP_FORCE_REMOTE_DEPS=1 swift build -c release — clean.
  • OCCTMCP_FORCE_REMOTE_DEPS=1 swift test138/138 passed (133 baseline + 5 new), including:
    • Disjoint panel + sphere STL soup: both plane and sphere primitives found, largest-support-first, supportFraction sane, byte-identical across two calls (determinism).
    • zoneId-scoped fit on the open-tube fixture: the barrel zone fits ~one cylinder covering most of its own triangles.
    • strategy: "auto" reports strategyScores (dihedral, ransac, chosen).
    • Dispatch-level invalid strategy string errors, naming the valid values (the Add align_bodies: GOM-style point-to-plane ICP registration (#104) #106 guard convention).
    • minSupportTriangles set between a small feature's and a large feature's triangle counts: the large one is still found, the small one reports via uncoveredFraction, and the two never mix into one warning.

Deviations from the brief

None of substance. One documentation decision made independently: while touching docs/reference/mesh-analysis.md's "Phase 3 backlog" list (which I had to edit anyway to remove the now-shipped fit_primitives entry), I also removed the #109/slippage entry since that's already integrated into segment_mesh_zones/zone_continuity_sweep per CLAUDE.md — the list was already stale on that point before this PR. Left #108/detect_mesh_features (the sibling PR's tool) and #29/#30 untouched.

🤖 Generated with Claude Code

Wraps OCCTSwiftMesh v1.7.0's Mesh.segmentedRANSAC(_:) / segmentedAutoSelect(
dihedral:ransac:) (OCCTSwiftMesh#27/#32): Schnabel-style global-inlier
primitive extraction, distinct from segment_mesh_zones' edge-adjacent-only
region growing, so one primitive can span regions the dihedral grower keeps
separate (e.g. a cylinder interrupted by a boss). Optional zoneId scopes the
fit to one zone via the same resolution path zone_continuity_sweep uses;
strategy "auto" runs the dihedral-vs-RANSAC bake-off and reports which won.
uncoveredFraction and a maxPrimitives cap are computed and reported
separately, never conflated.

Repins OCCTSwiftMesh to >=1.7.0. Tool count 72 -> 73, serverVersion ->
1.25.0, test count 133 -> 138.

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

gsdali commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator Author

Review: approved

Verified against the brief and the v1.7.0 upstream surface; no blocking findings.

  • The uncovered-vs-capped separation is the standout design decision: never passing maxPrimitives into the library call (maxRegions: nil always), computing uncoveredFraction from the unbounded truncatedTriangleCount, and applying the cap tool-side with its own distinctly-worded warning. This is exactly the right resolution of the upstream type's documented conflation (the SegmentedMesh doc nit from OCCTSwiftMesh#32's review) — uncoveredFraction provably never moves when the cap shrinks, and the test suite pins both halves including the negative assertion (no maxPrimitives warning when only minSupport exclusion occurred).
  • Zone resolution mirrors ZoneSweepTool's path exactly (stored-deflection re-mesh, MeshSignature staleness, deflection-override warning); the auto strategy applies minSupportTriangles to BOTH bake-off candidates for a fair comparison and surfaces the dihedral candidate's fitMergeSkipped only where it is meaningful.
  • Tests: byte-identical determinism asserted on the full JSON, largest-support ordering, zone-scoped cylinder, auto scoring, the Add align_bodies: GOM-style point-to-plane ICP registration (#104) #106-convention dispatch guard, and the uncovered/cap separation. 138/138 green.
  • Docs consistent (73 tools everywhere, the global-vs-zone-fits distinction explained); the stale-backlog cleanup call (dropping already-shipped Slippage integration: zone kind/axis in segment_mesh_zones + sweep axis defaults (blocked on OCCTSwiftMesh#26) #109 from the Phase 3 list while leaving the sibling's detect_mesh_features: crease-ring feature outlines (blocked on OCCTSwiftMesh#28) #108 entry) was the right independent judgment.

One wording nit, fine to fold into the landing train: when maxPrimitives: 0 empties the report, the empty-result message says "No primitive met minSupportTriangles" — misleading in that one degenerate combination (the cap warning alongside it does clarify). Not worth a round-trip on its own.

Lands first in the train as v1.25.0, per the coordination plan; the detect_mesh_features PR rebases over it.

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.

fit_primitives: RANSAC primitive report (blocked on OCCTSwiftMesh#27)

1 participant