Summary
Difference(plate, Union(rect, cyl)) where cyl is tangent to rect (their lateral surfaces share a planar boundary along a line) under-subtracts the cylinder's external lobe by ~66%. The standalone Union geometry is correct — the bug surfaces only when that Union is fed as the right-hand operand of a Difference.
Discovered while triaging the residual error on a4-slotted-bracket-01 after the planar-tessellator fix (#161, resolved). With #161's fix in place, the slotted bracket dropped from 11.7% over to 1.93% over — this issue accounts for the remaining 1.93%.
Repro — bisect from rect-only to rect+1cyl to rect+2cyls
Plate 80×40×8. Cutter is a slot built from a 40×10×8 rectangle plus 0, 1, or 2 tangent cylinders (R=5, H=8) at (±20, 0). All three cutters have their top/bottom flush with the plate (z=0..8).
rect only → vol = 22400.0000 expected 22400.00 dev = 0.000% ✓
rect + 1 cyl → vol = 22295.4484 expected 22085.84 dev = 0.949% ✗
rect + 2 cyls → vol = 22190.8968 expected 21771.68 dev = 1.926% ✗
Each tangent cylinder costs ~209 mm³ of missed subtraction = ~66% of the external semicircle's volume (π·5²/2 × 8 = 314.16 mm³). Residual scales linearly with cylinder count.
Minimal .vcad for the rect+1cyl case:
{
"version": "0.1",
"nodes": {
"1": {"id": 1, "name": "plate", "op": {"type": "Cube", "size": {"x": 80, "y": 40, "z": 8}}},
"2": {"id": 2, "name": "plate_t", "op": {"type": "Translate", "child": 1, "offset": {"x": -40, "y": -20, "z": 0}}},
"3": {"id": 3, "name": "rect", "op": {"type": "Cube", "size": {"x": 40, "y": 10, "z": 8}}},
"4": {"id": 4, "name": "rect_t", "op": {"type": "Translate", "child": 3, "offset": {"x": -20, "y": -5, "z": 0}}},
"5": {"id": 5, "name": "cyl_l", "op": {"type": "Cylinder", "radius": 5, "height": 8, "segments": 64}},
"6": {"id": 6, "name": "cyl_l_t", "op": {"type": "Translate", "child": 5, "offset": {"x": -20, "y": 0, "z": 0}}},
"7": {"id": 7, "name": "u", "op": {"type": "Union", "left": 4, "right": 6}},
"8": {"id": 8, "name": "out", "op": {"type": "Difference", "left": 2, "right": 7}}
},
"materials": {}, "part_materials": {},
"roots": [{"root": 8, "material": "default"}]
}
Expected: 80·40·8 − (40·10 + π·5²/2)·8 = 25600 − 3514.16 = 22085.84. Actual: 22295.45.
Standalone Union is fine
The same Union(rect, cyl_l, cyl_r) evaluated without a subsequent Difference reports volume 3827.31 vs expected stadium volume 3828.32 (0.026% off, well within polygon-vs-circle approximation). So the Union output by itself is correct geometry — the bug is in how Difference consumes a Union'd cutter whose components share a tangent boundary.
Extending the cutter past the plate in Z (so it's no longer flush) makes the residual worse, not better (extended slot reports 6.92% over). So this is not the same family as #161 — it doesn't disappear by avoiding coincident top/bottom faces.
Probable root cause
The tangent line where rect's x=-20 face meets cyl_l's lateral surface produces a coplanar/coincident boundary inside the Union solid. The Union's mesh likely contains an interior face along that line that is correctly suppressed for display (the standalone Union volume is right) but confuses the SSI/face-classification step in vcad-kernel-booleans when the Union becomes the right-hand operand of a Difference: classification incorrectly marks part of the cylinder's lateral surface as "outside the operand" and skips trimming the corresponding plate region.
This is in the same family as #160 (Steinmetz cylinder Union dedup) — both cases involve tangent / coplanar boundaries between Union'd primitives — but the failure mode differs: #160 is about Union double-counting overlap, this is about a downstream Difference under-subtracting the Union'd cutter.
Acceptance
Difference(plate, Union(rect, cyl_tangent_to_rect)) produces volume within 0.1% of the analytic value
- Add regression tests covering 0/1/2 tangent cylinders unioned with a rect, then differenced from a plate
- The
a4-slotted-bracket-01 mecheval task (currently 0.83 on Opus/Sonnet for the mass_props check) returns to 1.0
Refs
Summary
Difference(plate, Union(rect, cyl))wherecylis tangent torect(their lateral surfaces share a planar boundary along a line) under-subtracts the cylinder's external lobe by ~66%. The standalone Union geometry is correct — the bug surfaces only when that Union is fed as the right-hand operand of a Difference.Discovered while triaging the residual error on
a4-slotted-bracket-01after the planar-tessellator fix (#161, resolved). With #161's fix in place, the slotted bracket dropped from 11.7% over to 1.93% over — this issue accounts for the remaining 1.93%.Repro — bisect from rect-only to rect+1cyl to rect+2cyls
Plate 80×40×8. Cutter is a slot built from a 40×10×8 rectangle plus 0, 1, or 2 tangent cylinders (R=5, H=8) at (±20, 0). All three cutters have their top/bottom flush with the plate (z=0..8).
Each tangent cylinder costs ~209 mm³ of missed subtraction = ~66% of the external semicircle's volume (
π·5²/2 × 8 = 314.16 mm³). Residual scales linearly with cylinder count.Minimal
.vcadfor the rect+1cyl case:{ "version": "0.1", "nodes": { "1": {"id": 1, "name": "plate", "op": {"type": "Cube", "size": {"x": 80, "y": 40, "z": 8}}}, "2": {"id": 2, "name": "plate_t", "op": {"type": "Translate", "child": 1, "offset": {"x": -40, "y": -20, "z": 0}}}, "3": {"id": 3, "name": "rect", "op": {"type": "Cube", "size": {"x": 40, "y": 10, "z": 8}}}, "4": {"id": 4, "name": "rect_t", "op": {"type": "Translate", "child": 3, "offset": {"x": -20, "y": -5, "z": 0}}}, "5": {"id": 5, "name": "cyl_l", "op": {"type": "Cylinder", "radius": 5, "height": 8, "segments": 64}}, "6": {"id": 6, "name": "cyl_l_t", "op": {"type": "Translate", "child": 5, "offset": {"x": -20, "y": 0, "z": 0}}}, "7": {"id": 7, "name": "u", "op": {"type": "Union", "left": 4, "right": 6}}, "8": {"id": 8, "name": "out", "op": {"type": "Difference", "left": 2, "right": 7}} }, "materials": {}, "part_materials": {}, "roots": [{"root": 8, "material": "default"}] }Expected:
80·40·8 − (40·10 + π·5²/2)·8 = 25600 − 3514.16 = 22085.84. Actual:22295.45.Standalone Union is fine
The same
Union(rect, cyl_l, cyl_r)evaluated without a subsequent Difference reports volume3827.31vs expected stadium volume3828.32(0.026% off, well within polygon-vs-circle approximation). So the Union output by itself is correct geometry — the bug is in how Difference consumes a Union'd cutter whose components share a tangent boundary.Extending the cutter past the plate in Z (so it's no longer flush) makes the residual worse, not better (extended slot reports 6.92% over). So this is not the same family as #161 — it doesn't disappear by avoiding coincident top/bottom faces.
Probable root cause
The tangent line where rect's
x=-20face meets cyl_l's lateral surface produces a coplanar/coincident boundary inside the Union solid. The Union's mesh likely contains an interior face along that line that is correctly suppressed for display (the standalone Union volume is right) but confuses the SSI/face-classification step invcad-kernel-booleanswhen the Union becomes the right-hand operand of a Difference: classification incorrectly marks part of the cylinder's lateral surface as "outside the operand" and skips trimming the corresponding plate region.This is in the same family as #160 (Steinmetz cylinder Union dedup) — both cases involve tangent / coplanar boundaries between Union'd primitives — but the failure mode differs: #160 is about Union double-counting overlap, this is about a downstream Difference under-subtracting the Union'd cutter.
Acceptance
Difference(plate, Union(rect, cyl_tangent_to_rect))produces volume within 0.1% of the analytic valuea4-slotted-bracket-01mecheval task (currently 0.83 on Opus/Sonnet for the mass_props check) returns to 1.0Refs
claude/expand-mecheval-tests-Ubvom(planar-tessellator winding heuristic removal)a4-slotted-bracket-01— 0/10 frontier-model pass-rate on the volume check