Add kernel booleans: union, subtract and intersect for Solid3D

SolidOperation::Boolean runs synchronously, surfaces the kernel's refusal
with a plain reason and leaves the operands untouched, consumes them on
success unless keep_operands, and records one undo step. The lifecycle test
uses planar boxes with exact volumes and covers chaining, refusals, both
formats and undo; an ignored test shows a curved refusal reaching the script.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
Felix RiestraandClaude Sonnet 5 committed 2026-09-20 17:58:38 +02:00
1 parent 69047549e7
commit 5a39052397
8 files changed
+320 -1

No files matched your search

@@ -146,6 +146,26 @@ class _Solids:
handle = profile.handle if isinstance(profile, _Entity) else int(profile)
return self._document.entities[ocs.solid_region(handle, layer, bool(delete_source))]
def _boolean(self, operation, first, second, layer, keep_operands):
a = first.handle if isinstance(first, _Entity) else int(first)
b = second.handle if isinstance(second, _Entity) else int(second)
return self._document.entities[ocs.solid_boolean(a, b, operation, layer, bool(keep_operands))]
def union(self, first, second, layer=None, keep_operands=False):
"""Join two solids into a new one. The operands are consumed unless
`keep_operands`. The kernel may refuse (the error says why) and a
refusal changes nothing. Curved operands can take a second or two,
and a whole script has a 30 second budget."""
return self._boolean("union", first, second, layer, keep_operands)
def subtract(self, first, second, layer=None, keep_operands=False):
"""Remove `second` from `first`, giving a new solid."""
return self._boolean("subtract", first, second, layer, keep_operands)
def intersect(self, first, second, layer=None, keep_operands=False):
"""Keep only the volume both solids share, as a new solid."""
return self._boolean("intersect", first, second, layer, keep_operands)
def surface(self, profile, layer=None, delete_source=False):
"""Make a plane surface from one closed planar profile."""
handle = profile.handle if isinstance(profile, _Entity) else int(profile)