Skip to content

Add mesh_curvature: Rusinkiewicz per-vertex curvature (Phase 3 mesh-analysis expansion)#110

Merged
gsdali merged 1 commit into
mainfrom
feat/phase3-curvature
Jul 22, 2026
Merged

Add mesh_curvature: Rusinkiewicz per-vertex curvature (Phase 3 mesh-analysis expansion)#110
gsdali merged 1 commit into
mainfrom
feat/phase3-curvature

Conversation

@gsdali

@gsdali gsdali commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Summary

Phase 3 of the mesh-analysis expansion (.claude/plans/2026-07-21-mesh-analysis-expansion.md): one new tool, mesh_curvature, plus the Phase 3 primitive backlog filed and cross-linked.

Tool count: 71 → 72. serverVersion1.23.0. Test count: 113 → 118.

Scope decision (mirrors how #104/align_bodies was handled in #105/#106)

Phase 3's remaining primitives are mesh-domain algorithms that belong upstream in OCCTSwiftMesh per the ecosystem's factoring rule (OCCTMCP wraps, never implements mesh algorithms). mesh_curvature ships because its one primitive is already released; everything else needs a new upstream primitive first and is tracked as a filed issue rather than implemented ad hoc:

SecondMouseAU/OCCTSwiftMesh (primitive backlog):

  • #26 — Slippage analysis (Gelfand-Guibas) per region: classify plane/cylinder/sphere/extrude/revolve/helix with axes
  • #27 — RANSAC segmentation strategy + auto-selection bake-off
  • #28 — Crease-edge detection: dihedral-fold rings outlining recessed/raised features
  • #29 — Curvature-ordered seeding for segmented(_:) (now that vertexCurvatures() exists)
  • #30 — Generalized winding number orientation / inside-out check

SecondMouseAU/OCCTMCP (tracking issues, blocked-on links like #104 had):

  • #107fit_primitives: RANSAC primitive report (blocked on OCCTSwiftMesh#27)
  • #108detect_mesh_features: crease-ring feature outlines (blocked on OCCTSwiftMesh#28)
  • #109 — Slippage integration: zone kind/axis in segment_mesh_zones + sweep axis defaults (blocked on OCCTSwiftMesh#26)

All 8 were pre-filed and are cross-linked from the tool description, CLAUDE.md, and docs/reference/mesh-analysis.md's new "Phase 3 backlog" section.

Design decisions

  • Welding is mandatory, not optional: vertexCurvatures()'s own precondition is a welded mesh (unwelded input degrades to zero curvature everywhere). The tool welds internally before computing anything, and both the stats and the render are built entirely from that SAME welded mesh — no triangle-index correspondence problem to guard, unlike MeshZoneTools' adjacentZones.
  • Units: k1/k2/mean are 1/mm; gaussian = k1*k2 is 1/mm² — a genuinely different unit. highCurvatureFraction's clamp is always computed from the SAME channel colorBy selects (never cross-compared against a different channel), so this never creates a silent unit mismatch.
  • highCurvatureFraction is the fraction of vertices whose |colorBy value| exceeds the SAME clamp used for the render — by construction close to 1 - clampPercentile (that's exactly what "clamped for color" means). clampPercentile: 1.0 drives it to 0; this relationship is what MeshCurvatureToolsTests.clampPercentileSemantics pins.
  • flatFraction is deliberately colorBy-independent: max(|k1|,|k2|) < 0.1/bboxDiag (1/mm), an absolute model-scale threshold, not a percentile of the sample — so it means the same thing regardless of how curved the rest of the body is.
  • Unweldable-soup warning fires only on a demonstrated weld failure (vertexCount == triangleCount*3 post-weld) — a mesh-topology fact, never a curvature-value heuristic, so a genuinely flat body (which also reads near-zero almost everywhere) can't false-positive it.
  • colorBy dispatch validation follows the Add align_bodies: GOM-style point-to-plane ICP registration (#104) #106 convention: an unrecognized value errors naming the valid ones, rather than silently falling back to the default.

Test plan

OCCTMCP_FORCE_REMOTE_DEPS=1 swift build, swift build -c release, and swift test — all green: 118 tests in 21 suites, 0 failures (was 113; +5 new).

New MeshCurvatureToolsTests (5), following the #105/#106 patterns (scene() helper + Decodable mirrors + hand-written STL fixtures, "reimplemented locally to keep this file self-contained"):

  • Grid box (hand-subdivided 12x12-per-face grid STL, not Shape.box's native tessellation — a flat BREP face doesn't necessarily get any interior vertices from BRepMesh at all): k1/k2/mean/gaussian medians are exactly zero (the ~56% face-interior majority is exactly co-planar; curvature contamination from the fold bleeds one hop past the literal edge/corner vertices, so the naive "~84% flat" estimate from edge-counting alone doesn't hold — measured and documented in the fixture's own comment), flatFraction > 0.45, render off.
  • Cylinder tube (hand-written open multi-ring STL, not Shape.cylinder — its native tessellation doesn't reliably subdivide along the height axis): median k1 ~ 1/radius, median k2 ~ 0, median gaussian ~ 0, generous 20-30% tolerance since the report has no per-ring breakdown to exclude the open boundary's documented bias.
  • colorBy validation: dispatch-level unknown colorBy errors naming valid values.
  • Unweldable-soup warning: two far-apart triangles sharing nothing (vertexCount == triangleCount*3 post-weld) trips the warning; curvature degrades cleanly to zero, not garbage.
  • clampPercentile semantics: 1.0 vs 0.95 on the grid box's genuine edge/corner spike (the cylinder tube's curvature turned out too smooth/near-uniform for a clean percentile tail — swapped fixtures based on the actual measured distribution) — highCurvatureFraction responds as designed, the underlying k1 stat block doesn't move.

Docs

docs/reference/mesh-analysis.md (mesh_curvature section: args, units, clamp semantics, welded-internally note, colorBy table, Phase 3 backlog with all 8 issue links); README tool table + counts 71 → 72 (README, CLAUDE.md all spots, docs/reference/README.md, docs/guides/getting-started.md); CLAUDE.md bullet for MeshCurvatureTools.swift; CLAUDE.md's OCCTSwiftMesh dependency bullet updated to note vertexCurvatures is now consumed.

🤖 Generated with Claude Code

… mesh-analysis expansion)

Ships the one Phase 3 tool unblocked today: OCCTSwiftMesh 1.5.0 (already
pinned) shipped Mesh.vertexCurvatures() in v1.4.0 but OCCTMCP hadn't consumed
it yet. mesh_curvature welds a body's mesh internally (vertexCurvatures'
own precondition), computes k1/k2/mean/gaussian stats plus flatFraction and
a clamp-relative highCurvatureFraction, and renders a colored PNG reusing
the band-group + colorbar trick HeatmapTools/MeshZoneTools established.

Phase 3's remaining design-intent primitives (slippage classification,
RANSAC segmentation, crease-edge detection, curvature-ordered seeding,
generalized winding number) need new upstream primitives and are tracked
as already-filed issues (SecondMouseAU/OCCTSwiftMesh#26-#30,
#107-#109) rather than implemented ad hoc.

Tools: 71 -> 72. serverVersion -> 1.23.0. Tests: 113 -> 118.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@gsdali
gsdali merged commit 0f33884 into main Jul 22, 2026
gsdali added a commit that referenced this pull request Jul 22, 2026
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>
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.

1 participant