Skip to content

Boolean fidelity matrix, and the three torus bugs it found - #807

Open
ecto wants to merge 3 commits into
mainfrom
claude/inspiring-boyd-ac9bad
Open

Boolean fidelity matrix, and the three torus bugs it found#807
ecto wants to merge 3 commits into
mainfrom
claude/inspiring-boyd-ac9bad

Conversation

@ecto

@ecto ecto commented Aug 16, 2026

Copy link
Copy Markdown
Owner

Why

Booleans always return a BRep now, so "is it a BRep?" no longer separates a real result from a degraded one. Every mesh-CSG fallback is re-wrapped as a triangle-soup BRep that still passes can_export_step() and still exports STEP — just as thousands of facets instead of the cylinder the part was authored from. Nothing reported that until export time.

What changed

A fidelity seam. vcad-kernel gains a provenance ledger (SolidFidelity, LossKind, DegradeEvent) and Solid::try_boolean_reported, which separates failed from succeeded but degraded and names which fallback fired.

A characterisation matrix over it. vcad-torture gains a fidelity mode running 34 curated arrangements × 3 booleans = 102 cells, across operation × operand surface pair × configuration (generic overlap, tangent, coincident face, through-cut break-out, bore, non-manifold contact):

cargo run -p vcad-torture -- fidelity --md docs/boolean-fidelity-matrix.md --json crates/vcad-torture/fidelity-baseline.json

Result: 15 of 102 cells degrade, 0 produce wrong geometry. The boundary is curved-vs-curved intersection curves — break-out into a cylinder, cone or sphere wall, plus the Steinmetz cross. Plane-vs-anything is analytic everywhere. Full table in docs/boolean-fidelity-matrix.md.

Keeping it honest

  • Baseline drift testVCAD_FIDELITY_BLESS=1 to re-bless. Only the fidelity class is compared, not volumes or face counts, since those already differ across architectures.
  • Overlap guard — asserts each fixture's difference actually removes volume. A cell reading analytic proves nothing if the operands never met; this caught the dead torus on its first run, and then four of my own misplaced fixtures (curved primitives are origin-centred while cube is 0..size).
  • NURBS is left out rather than faked. loft emits ruled planar faces (LoftMode::Smooth is unimplemented), so STEP import is the only source of SurfaceKind::BSpline. Stated as a coverage gap in the report.

The three bugs the guard found

All silent — nothing failed, the answers were just wrong.

  1. tessellate_brep had no Torus arm. Torus faces fell through to the planar tessellator. A whole donut (four half-edges on one seam vertex) meshed to zero triangles, so Solid::torus(10, 2.5, 32) had volume 0 and every boolean against a torus returned the other operand while reporting Analytic. A fillet blend patch (four distinct corners) meshed as a flat quad, so blends drew as chamfers but stayed watertight — which is why nothing was red. There is a tessellate_toroidal_face; it was only reachable from a second dispatch site. Volume now matches 2π²Rr² to 1.28%, the expected inscribed-polygon error.

  2. Cavity toruses wound inside-out. tessellate_toroidal_face applied reversed twice in its winding correction (flip already encodes the desired orientation). A torus subtracted from a 30³ cube added its volume: 27389.7 instead of 26605.

  3. Blend patches didn't weld once they actually meshed. An arc-profile fillet had 6173mm of open boundary against a 276mm tolerance (22×), so fillet() rejected its own result and returned the input unfilleted. Fixed with tessellate_torus_two_chain, the torus analogue of the cylinder's ruled-two-chain path. Three things mattered, in descending order of effect:

    • Pair equal-length rails index-wise, don't union-grid them. The two rails' u values routinely differ by ~0.01 rad; a union grid invents a column there, and every invented column is a vertex the neighbour lacks. Alone: 1181 → 342.
    • Try all loop rotations. A rail spanning the loop closure is split in half by any linear scan. 96 patches: 0 accepted → 75 with one hand-picked rotation → 93 trying all. Loops are 4 vertices, so brute force is correct and free.
    • Let chains share a corner where rails meet at a vertex instead of via a connector edge — otherwise one chain gets a single vertex and is rejected. That was the last 3 of 96.

    Net: 22.35× → 0.83×, boundary edges 1764 → 265.

Reviewer notes

  • n_v is span-adaptive (ceil(n_full · v_span / 2π)), not a constant, so patches sharing a v range derive the same count and their shared end edges stay welded. A fixed n_v=2 measured 1.65× while 1 and 4 passed — that parity is luck, not a fix, and pinning a constant would be tuning to it.
  • test_torus_boolean_subtract was passing vacuously. It only asserted volume <= 27000, which a cut that removes nothing satisfies. Now pinned to 30³ − 2π²Rr² within 2%. Any "should not exceed" assertion on a boolean is a candidate for the same failure mode.
  • The residual 265 boundary edges are the spherical vertex-blend corner gaps that blend_result_is_valid already documents as tolerated by design.
  • No packages/kernel-wasm/vcad_kernel_wasm* artifacts are touched.

Verification

  • cargo test --workspace --exclude vcad-desktop314 suites pass, 0 fail
  • cargo clippy --workspace --exclude vcad-desktop -- -D warnings — clean
  • cargo fmt --all --check — clean
  • New regression tests: torus tessellation against its closed form, the tightened torus subtraction, plus the matrix's own drift/overlap/uniqueness/completeness guards.

🤖 Generated with Claude Code

Booleans always return a BRep now, so "is it a BRep?" no longer separates a
real result from a degraded one — every mesh-CSG fallback is re-wrapped as a
triangle-soup BRep that still passes can_export_step() and still exports
STEP, just as thousands of facets instead of the cylinder the part was
authored from. Nothing reported that until export time.

Adds a representation-fidelity seam and a characterisation matrix over it:

- vcad-kernel gains a provenance ledger (SolidFidelity, LossKind,
  DegradeEvent) and Solid::try_boolean_reported, which separates "failed"
  from "succeeded but degraded" and names which fallback fired.
- vcad-torture gains a `fidelity` mode running 34 curated arrangements x 3
  booleans = 102 cells, over operation x operand surface pair x
  configuration (generic overlap, tangent, coincident face, through-cut
  break-out, bore, non-manifold contact). Writes docs/boolean-fidelity-matrix.md
  and a checked-in baseline.

Result: 15 of 102 cells degrade, 0 produce wrong geometry. The boundary is
curved-vs-curved intersection curves — break-out into a cylinder, cone or
sphere wall, and the Steinmetz cross. Plane-vs-anything is analytic
everywhere.

Kept honest by a baseline drift test (VCAD_FIDELITY_BLESS=1 to re-bless;
only the fidelity class is compared, since volumes differ across
architectures) and an overlap guard asserting each fixture's difference
actually removes volume — a cell reading "analytic" proves nothing if the
operands never met. NURBS is left out rather than faked: loft emits ruled
planar faces (LoftMode::Smooth is unimplemented), so STEP import is the only
source of B-spline faces. That gap is stated in the report.

The overlap guard failed on its first run, which unwound into three silent
bugs:

1. tessellate_brep had no Torus arm — torus faces fell through to the planar
   tessellator. A whole donut (four half-edges on one seam vertex) meshed to
   zero triangles, so Solid::torus(10, 2.5, 32) had volume 0 and every
   boolean against a torus returned the other operand while reporting
   Analytic. A fillet blend patch (four distinct corners) meshed as a flat
   quad, so blends drew as chamfers but stayed watertight — which is why
   nothing was red. Volume now matches 2*pi^2*R*r^2 to 1.28%, the expected
   inscribed-polygon error.

2. tessellate_toroidal_face applied `reversed` twice in its winding
   correction, so every cavity torus face came out outward-wound. A torus
   subtracted from a 30^3 cube added its volume: 27389.7 instead of 26605.

3. Blend patches did not weld once they actually meshed — an arc-profile
   fillet had 6173mm of open boundary against a 276mm tolerance (22x), so
   fillet() rejected its own result and returned the input unfilleted. Fixed
   by giving toruses the cylinder's ruled-two-chain treatment
   (tessellate_torus_two_chain): pair equal-length rails index-wise instead
   of union-gridding them (the rails' u differ by ~0.01 rad and every
   invented column is a crack), try all loop rotations since a rail spanning
   the loop closure is split by any linear scan, and let chains share a
   corner where rails meet at a vertex rather than via a connector edge.
   22.35x -> 0.83x, boundary edges 1764 -> 265. n_v is span-adaptive so
   patches sharing a v range derive the same count and their shared end
   edges stay welded; a fixed n_v=2 measured 1.65x while 1 and 4 passed, and
   that parity is luck rather than a fix.

test_torus_boolean_subtract was passing vacuously — it only asserted
volume <= 27000, which a cut that removes nothing satisfies. Pinned to the
closed form. Also adds a torus tessellation regression test against
2*pi^2*R*r^2.

314 test suites pass; clippy and fmt clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 16, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

4 Skipped Deployments
Project Deployment Actions Updated (UTC)
mecheval Ignored Ignored Aug 16, 2026 2:25am
vcad Ignored Ignored Aug 16, 2026 2:25am
vcad-docs Ignored Ignored Aug 16, 2026 2:25am
vcad-mcp Ignored Ignored Aug 16, 2026 2:25am

Request Review

Conflicts were all in crates/vcad-kernel/src/lib.rs, where main's
fail-closed blend rework (#804 and friends) landed on the same methods this
branch touched to add the provenance ledger.

Resolution, hunk by hunk:

- `try_boolean_reported`/`op_name` (ours) and `non_brep_error` (theirs) are
  unrelated additions at the same spot — kept both.
- `chamfer`, `fillet`, `shell_reported`: main rewrote these to return
  `Result<_, BlendError>` with a `BlendReport` instead of silently returning
  the input unchanged. That supersedes our side of those bodies, so theirs
  was taken wholesale; the only edit was threading `provenance:
  self.provenance.clone()` through each `Solid` literal, since the field is
  new on this branch and main's new literals don't set it.

Note the two changes agree in spirit: main made blend failure *reportable*
rather than silent, which is the same argument this branch makes for boolean
representation loss.

318 test suites pass (up from 314 — main's new fillet_torture and
face_queries suites); clippy and fmt clean. The fidelity baseline did not
drift, so main's kernel changes do not move any cell in the matrix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Net +6 passing (678 → 684 of 752). Eighteen cases move, and the direction
is not uniform, so both halves are worth stating plainly.

Twelve improvements. All ten `tess-torus*` cases went bad-geometry → pass:
they were tessellation-watertightness cases on a primitive that meshed to
zero triangles, so they had been failing outright. `rand-159` and `rand-367`
are boolean cases that now come out clean.

Six regressions, all boolean cases with a torus operand — rand-078, -166,
-298, -306, -358, -372. These were **passing vacuously**. With torus faces
meshing to nothing, the boolean was a no-op that returned a watertight
operand, so the grader saw a closed mesh and called it a pass. The geometry
was always wrong; only now is it visible.

The underlying gap is in the boolean, not the tessellator, and it is
pre-existing: inverting the loop vertices of the torus faces these cases
produce gives (0,0) four times over — the analytic whole-torus seam loop —
so the splitter never trimmed the torus at all. `test_torus_boolean_subtract`
has carried a comment saying as much ("full torus subtraction requires
torus-plane SSI to produce proper split curves") since long before this
branch. The fidelity matrix reports the same thing from the other side:
`generic-overlap/plane-torus/difference` degrades to triangle-soup. Fixing
torus SSI splitting is its own piece of work; this commit does not attempt
it, and these six cases are the honest record of where it stands.

Baseline taken from the CI `torture-scorecard` artifact (run 31921298869),
not a local run, per the platform-specific-baseline note in
torture-track.yml — as of #758 five cases land differently on x86_64 Linux
than on aarch64 macOS. CI and local agreed on all eighteen here, including
the open-edge counts, and no other case moved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ecto

ecto commented Aug 16, 2026

Copy link
Copy Markdown
Owner Author

Merged main in and refreshed the torture baseline. Two things a reviewer should know about, since neither is visible from the diff alone.

Merge conflicts were all in crates/vcad-kernel/src/lib.rs, where main's fail-closed blend rework landed on the same methods this branch touched. try_boolean_reported/op_name (ours) and non_brep_error (theirs) were unrelated additions at the same spot, so both were kept. For chamfer, fillet and shell_reported, main's Result<_, BlendError> + BlendReport versions supersede our side outright — theirs taken wholesale, with provenance: self.provenance.clone() threaded through each Solid literal since that field is new here. The two changes agree in spirit: main made blend failure reportable rather than silent, which is the same argument this branch makes for boolean representation loss.

Torture track: net +6 passing (678 → 684 of 752). Eighteen cases move and the direction isn't uniform:

  • 12 improvements. All ten tess-torus* cases went bad-geometry → pass — they were watertightness cases on a primitive that meshed to zero triangles, so they'd been failing outright. rand-159 and rand-367 now come out clean.
  • 6 regressions, all boolean cases with a torus operand (rand-078, -166, -298, -306, -358, -372). These were passing vacuously. With torus faces meshing to nothing, the boolean was a no-op returning a watertight operand, so the grader saw a closed mesh and called it a pass. The geometry was always wrong; only now is it visible.

The gap behind those six is in the boolean, not the tessellator, and it predates this branch: inverting the loop vertices of the torus faces these cases produce gives (0,0) four times over — the analytic whole-torus seam loop — so the splitter never trimmed the torus at all. test_torus_boolean_subtract has carried a comment saying exactly that ("full torus subtraction requires torus-plane SSI to produce proper split curves") since well before this work, and the fidelity matrix reports the same thing from the other side: generic-overlap/plane-torus/difference degrades to triangle-soup. Fixing torus SSI splitting is its own piece of work and this PR doesn't attempt it — those six cases are the honest record of where it stands.

Baseline was taken from the CI torture-scorecard artifact (run 31921298869), not a local run, per the platform-specific note in torture-track.yml. CI and local agreed on all eighteen cases including open-edge counts, and nothing else moved.

Verified on the merged tree: 318 test suites pass (up from 314 — main's new suites), torture gate reports no regressions, clippy and fmt --check clean.

🤖 Addressed by Claude Code

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