Skip to content

Cut each body by the later bodies that reach it, not by all of them (#177) - #178

Merged
revarbat merged 1 commit into
mainfrom
perf/export-split-overlap-only
Sep 16, 2026
Merged

revarbat merged 1 commit into
mainfrom
perf/export-split-overlap-only

Conversation

@revarbat

Copy link
Copy Markdown
Member

Fixes #177.

The problem

splitBodiesForExport's multi-colour path subtracted every body against a running union of all the bodies after it:

if (claimed) {
    if (boxesOverlap(s.man, *claimed)) piece = s.man - *claimed;
}
claimed = claimed ? (*claimed + s.man) : s.man;

claimed grows monotonically, so by body k it holds the union of all n−k bodies after it. That is n booleans whose second operand reaches the size of the whole model — quadratic in body count, regardless of how much the parts actually touch. The boxesOverlap guard stops discriminating as soon as the union spans the model, which it does almost immediately.

The fix

Only a later body that actually intersects this one can remove anything from it, and bounding boxes settle that in nanoseconds. Each body is now cut against just those:

for (size_t m = n + 1; m < solids.size(); ++m) {
    if (boxesOverlap(boxes[n], boxes[m])) blockers.push_back(solids[m].man);
}
manifold::Manifold piece = s.man;
if (!blockers.empty()) piece = s.man - addAll(blockers);

The result is identical — subtracting a disjoint solid removes no volume — and the work becomes proportional to how much the model overlaps rather than to how many parts it has. The O(n²) bounding-box scan that replaces it is nanoseconds per pair.

Skipping the subtraction entirely when nothing overlaps also preserves the triangle list, so per-triangle colours survive on bodies that nothing cuts. That is the same property the old bounding-box guard existed to protect, now applied per body instead of against the accumulated blob.

Measurements

100 coloured overlapping spheres, ~212,000 facets held constant, only the body count varying:

bodies 3MF before 3MF after
10 1.714 s 1.472 s
25 2.599 s 1.615 s
50 4.103 s 1.750 s
100 6.582 s 1.843 s

3.6x at 100 bodies, and near-flat in body count where it had been quadratic. BelfrySCAD's Dalek model (119 bodies, 224,220 facets) goes 5.96 s → 2.07 s.

STL is unaffected in both directions: it is not a multi-object format and never reaches this path.

Output is identical, checked rather than assumed

  • Every STL is byte-identical (sha256) across Dalek, CosBox, Anklet and the 100-sphere case.
  • Dalek's 3MF keeps all 6 objects with the same bounding boxes. Total volume 403426.1883 → 403426.1882 (3e-10 relative). It has 48 fewer triangles out of 151,074 and 0.002% less surface area — slivers the old cut against the big union left behind.
  • The 100-sphere case matches object for object, same triangle counts, volumes agreeing to 4e-15.

Tests

SplitColors.AFarAwayBodyChangesNothing and SplitColors.LaterWinsAcrossAnInterveningColour.

Both pass on the old code as well — they pin semantics, not speed, and I am not claiming they would have caught this. They are there to stop a future rewrite getting it wrong. In particular the obvious cheaper idea, "union the same-coloured bodies first and subtract per colour", is wrong: red/blue/red overlapping in that order must let the blue beat the first red, which per-colour grouping cannot express. That is exactly what the second test encodes.

The speed claim rests on the measurements above, not on a unit test.

Full C++ suite: 1262 passed. Python bindings: 42 passed.

🤖 Generated with Claude Code

…177)

splitBodiesForExport's multi-colour path subtracted every body against a
running union of all the bodies after it. That union grows as the loop
goes, so an n-body export did n booleans whose second operand reached the
size of the whole model -- quadratic in body count, however little the
parts actually touched.

Only a later body that intersects this one can remove anything from it,
and bounding boxes settle that in nanoseconds. Cutting against just those
leaves the result identical, because subtracting a disjoint solid removes
no volume, and leaves the work proportional to how much the model really
overlaps rather than to how many parts it has.

Measured on 100 coloured overlapping spheres, ~212,000 facets:

  bodies   3MF before   3MF after
      10       1.714s      1.472s
      25       2.599s      1.615s
      50       4.103s      1.750s
     100       6.582s      1.843s

3.6x at 100 bodies, and near-flat in body count where it had been
quadratic. BelfrySCAD's Dalek model (119 bodies, 224k facets) goes
5.96s -> 2.07s. STL is untouched: it is not a multi-object format and
never reaches this path.

Identical output, checked rather than assumed. Every STL is byte-identical.
Dalek's 3MF keeps all 6 objects and the same bounding boxes, with total
volume 403426.1883 -> 403426.1882 and 48 fewer triangles out of 151,074 --
slivers the old cut against the big union left behind. The 100-sphere case
matches object for object to 4e-15.

The two new tests pin semantics, not speed, and both pass on the old code:
they are there to stop a future rewrite getting this wrong. The obvious
cheaper idea -- union same-coloured bodies first, then subtract per colour
-- is wrong, because red/blue/red overlapping in that order must let the
blue beat the first red; LaterWinsAcrossAnInterveningColour is exactly that
case.
@revarbat
revarbat merged commit 5a436a3 into main Sep 16, 2026
3 checks passed
@revarbat
revarbat deleted the perf/export-split-overlap-only branch September 16, 2026 00: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.

splitBodiesForExport is quadratic in body count for multi-colour multi-object export (3MF 3.8x slower at same geometry)

1 participant