Skip to content

kernel: Difference doesn't fully subtract Union'd cutter when components are tangent #165

@ecto

Description

@ecto

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions