Skip to content

Implement 2D minkowski() as a boundary sweep - #89

Merged
revarbat merged 2 commits into
mainfrom
fix-2d-minkowski
Aug 11, 2026
Merged

Implement 2D minkowski() as a boundary sweep#89
revarbat merged 2 commits into
mainfrom
fix-2d-minkowski

Conversation

@revarbat

@revarbat revarbat commented Aug 11, 2026

Copy link
Copy Markdown
Member

minkowski() only ever looked at 3D bodies. 2D sections among its children were dropped on the floor — silently:

linear_extrude(6) minkowski() { square([30,20], center=true); circle(4, $fn=24); }

produced no geometry at all, where the reference produces the rounded square asked for. No warning; the whole statement just vanished from the model. Found while writing an example for minkowski(), which rendered two of its three copies and nothing where that one should have been.

How

Manifold has no 2D Minkowski, so this builds one from the boundary-sweep identity: for B convex and containing the origin,

A (+) B  =  A  ∪  (∂A (+) B)

and ∂A is a chain of segments, each of which sums with a convex B to the hull of B at its two ends. So A is never cut up at all — however concave it is, however many holes it has, only its edges are walked. Only B is decomposed, and only because the per-segment hull needs it convex.

That is one hull per edge of A per convex piece of B, and the piece count is one in the case that actually turns up: a circle swept over something.

Both conditions on B are met rather than assumed, and each is silently wrong on its own, so each has its own test:

skipped step what it costs
decomposing a concave B (hulling it instead) computes A ⊕ hull(B) — 5092 against the reference's 4992
shifting B onto the origin and back keeps an untranslated copy of A: starts at x=0 where the reference starts at x=16
walking every contour a hole is left unswept

Matching the reference

Six cases, all matching the reference exactly:

ours reference
convex + circle 6298.16 6298.16
concave A + circle 5120.47 5120.47
concave A + concave B 4992.00 4992.00
B away from the origin 5120.47 5120.47
three operands 4670.21 4670.21
A with a hole 4141.98 4141.98

A mix of 2D and 3D children still takes the 3D path, as before.

Testing

Four tests: a convex outline rounded to the right size and bounding box, a concave one that must stay concave, a single section handed back untouched, and 3D still working.

The concave test took three attempts to become worth having, and the negative control is why. Substituting Hull(parts) for the union — the obvious wrong implementation — first passed a volume bound, because hulling lands within 1% of the right answer on this shape. It then passed a probe of the notch, because I had put the probe outside both shapes, where it proved nothing. It now sits inside what a hull would cover and outside the real shape, and catches the substitution.

825 tests pass. Negative-controlled: hulling the sweeper, skipping the origin shift, and walking only the first contour each fail exactly their own test and nothing else.

🤖 Generated with Claude Code

revarbat and others added 2 commits August 10, 2026 22:58
It only ever looked at 3D bodies. 2D sections among its children were
dropped on the floor, silently, so

    linear_extrude(6) minkowski() { square([30,20]); circle(4); }

produced no geometry at all where the reference produces the rounded
square asked for. No warning either -- the whole statement simply
vanished from the model.

Manifold has no 2D Minkowski, so this builds one: cut both shapes into
convex pieces, take the convex hull of every pairwise sum of points, and
union the results. Correct because the sum of two convex sets is the hull
of their pairwise sums, and Minkowski distributes over union. A convex
outline with no holes stays whole rather than being triangulated, which
is the case that actually turns up -- a circle swept over something --
and much cheaper.

Matches the reference exactly on both a convex and a concave outline:
same facet count (108 and 100) and the same volume to five figures.

A mix of 2D and 3D children still takes the 3D path, as before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The first version cut both operands into convex pieces and hulled every
pair. The boundary-sweep identity is better: for B convex and containing
the origin,

    A (+) B  =  A  union  (boundary of A (+) B)

and the boundary is a chain of segments, each of which sums with a convex
B to the hull of B at its two ends. So A is never cut up at all, however
concave it is or however many holes it has -- only its edges are walked.
Only B is decomposed, and only because the per-segment hull needs it
convex.

Both conditions on B are met rather than assumed, and each is its own
test because each is silently wrong on its own:

  hulling B instead of decomposing it computes A (+) hull(B), 5092
  against the reference's 4992

  leaving B where it is, when it does not contain the origin, keeps an
  untranslated copy of A: the result starts at x=0 where the reference
  starts at x=16

  walking one contour leaves a hole unswept

Matches the reference exactly on all six: convex, concave A, concave B,
an off-origin B, three operands, and a shape with a hole.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@revarbat revarbat changed the title Implement minkowski() for 2D shapes Implement 2D minkowski() as a boundary sweep Aug 11, 2026
@revarbat
revarbat merged commit 390b8d6 into main Aug 11, 2026
3 checks passed
@revarbat
revarbat deleted the fix-2d-minkowski branch August 11, 2026 06:51
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