Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
c946d62
Add an overlaps method to Bounding Boxes, helpful for filtering
bernhard-42 Jan 16, 2026
50a06f7
Add a method to expand Shell, Wire and Compound objects in ShapeLists
bernhard-42 Jan 16, 2026
b5ddeea
add a flag keywork as_list to _bool_op to simplify intersection resul…
bernhard-42 Jan 16, 2026
2f92b2e
Add a helper function that allows type safe actions in Shape for type…
bernhard-42 Jan 16, 2026
d3ab5b2
reimplement intersect as intersect+touch top-down to avoid expensive …
bernhard-42 Jan 16, 2026
2d38e5d
add include_touched to intersect for tests that relied on getting toc…
bernhard-42 Jan 16, 2026
792a87a
add suuport for include_touched keyword, split some tests into inters…
bernhard-42 Jan 16, 2026
d4ba9ab
temporarily add the summary of the approach
bernhard-42 Jan 16, 2026
de9ddf5
fuse extrude results in builder mode to match algebra mode
bernhard-42 Jan 17, 2026
499510a
update after aligning extrude both for builder mode with fuse to alge…
bernhard-42 Jan 17, 2026
1bdfff2
fix typing issues
bernhard-42 Jan 17, 2026
8ddb6ff
create Shape._bool_op_list to make ty type checking happy
bernhard-42 Jan 19, 2026
293e4e8
add base _intersect method to Shape to make tyy type checking happy
bernhard-42 Jan 19, 2026
d17b7d3
handle normal check for Shell in touch()
bernhard-42 Jan 19, 2026
318f273
deduplicate edges in Solid.touch for Face/Shell
bernhard-42 Jan 19, 2026
06ebe6c
encapsulate filtering into sub-function
bernhard-42 Jan 19, 2026
d9abb03
add more touch test cases
bernhard-42 Jan 19, 2026
2e5d81d
update to ,latest state of tests and performance run
bernhard-42 Jan 19, 2026
bfb018e
removed analysis file
bernhard-42 Jan 19, 2026
859aeca
rename variable to make mymp happy
bernhard-42 Jan 19, 2026
69d1398
Merge branch 'dev' into intersect-optimized
bernhard-42 Jan 19, 2026
34ca825
add a geometrical comparision method for edges
bernhard-42 Jan 20, 2026
47a3f24
improve readability of touch and use the geometric comparision
bernhard-42 Jan 20, 2026
441aef0
make mypy happy
bernhard-42 Jan 20, 2026
1d55475
add location and axis check cor conic sections
bernhard-42 Jan 20, 2026
e2b182a
remove vector and location from geom_equal
bernhard-42 Jan 20, 2026
0c31a24
integrate geom_hash into Wire and Edge class and add tests
bernhard-42 Jan 21, 2026
216cbdd
add more tests for splines and bezier curves
bernhard-42 Jan 21, 2026
1434788
improve test coverage
bernhard-42 Jan 21, 2026
1a6929b
move infinite edge check from helpers to the _intersect methods to al…
bernhard-42 Jan 21, 2026
1d2003e
add a type validation step
bernhard-42 Jan 21, 2026
340e8a1
remove helpers.py and the runtime import by duplicating some conversi…
bernhard-42 Jan 21, 2026
63cb049
remove default branch from test coverage, since in Pyhton it can't be…
bernhard-42 Jan 21, 2026
36da0d0
remove lines coverd from comment
bernhard-42 Jan 21, 2026
f4b18dd
remove leading _ from variable names
bernhard-42 Jan 22, 2026
89b16ef
improve filtering against vertices on edges
bernhard-42 Jan 22, 2026
7993558
Use higher precision for BRepExtrema_DistShapeShape to avoid duplicates
bernhard-42 Jan 22, 2026
4b5d43e
filter empty faces
bernhard-42 Jan 22, 2026
07f6c47
streamline touch logic while fixing some missing touch edge cases
bernhard-42 Jan 23, 2026
f895373
code foramtting
bernhard-42 Jan 23, 2026
410bbd1
add new test cases for the missing touch results
bernhard-42 Jan 23, 2026
8b5afa9
fix wrong test expectation
bernhard-42 Jan 23, 2026
0bb7bca
Ensure that faces_equal gets called with a new test
bernhard-42 Jan 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/build123d/geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -1182,6 +1182,20 @@ def is_inside(self, second_box: BoundBox) -> bool:
and second_box.max.Z < self.max.Z
)

def overlaps(self, other: BoundBox, tolerance: float = TOLERANCE) -> bool:
"""Check if this bounding box overlaps with another.

Args:
other: BoundBox to check overlap with
tolerance: Distance tolerance for overlap detection

Returns:
True if bounding boxes overlap (share any volume), False otherwise
"""
if self.wrapped is None or other.wrapped is None:
return False
return self.wrapped.Distance(other.wrapped) <= tolerance

def to_align_offset(self, align: Align2DType | Align3DType) -> Vector:
"""Amount to move object to achieve the desired alignment"""
return to_align_offset(self.min, self.max, align)
Expand Down
14 changes: 6 additions & 8 deletions src/build123d/operations_part.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,16 +230,14 @@ def extrude(
)
)

if both and len(new_solids) > 1:
fused_solids = new_solids.pop().fuse(*new_solids)
new_solids = fused_solids if isinstance(fused_solids, list) else [fused_solids]
if clean:
new_solids = [solid.clean() for solid in new_solids]

if context is not None:
context._add_to_context(*new_solids, clean=clean, mode=mode)
else:
if len(new_solids) > 1:
fused_solids = new_solids.pop().fuse(*new_solids)
new_solids = (
fused_solids if isinstance(fused_solids, list) else [fused_solids]
)
if clean:
new_solids = [solid.clean() for solid in new_solids]

return Part(ShapeList(new_solids).solids())

Expand Down
209 changes: 74 additions & 135 deletions src/build123d/topology/composite.py
Original file line number Diff line number Diff line change
Expand Up @@ -713,149 +713,88 @@ def get_type(

return results

def intersect(
self, *to_intersect: Shape | Vector | Location | Axis | Plane
) -> None | ShapeList[Vertex | Edge | Face | Solid]:
"""Intersect Compound with Shape or geometry object
def _intersect(
self,
other: Shape | Vector | Location | Axis | Plane,
tolerance: float = 1e-6,
include_touched: bool = False,
) -> ShapeList | None:
"""Single-object intersection for Compound (OR semantics).

Args:
to_intersect (Shape | Vector | Location | Axis | Plane): objects to intersect
Distributes intersection over elements, collecting all results:
Compound([a, b]).intersect(s) = (a ∩ s) ∪ (b ∩ s)
Compound([a, b]).intersect(Compound([c, d])) = (a ∩ c) ∪ (a ∩ d) ∪ (b ∩ c) ∪ (b ∩ d)

Returns:
ShapeList[Vertex | Edge | Face | Solid] | None: ShapeList of vertices, edges,
faces, and/or solids.
"""
Handles both build123d assemblies (children) and OCCT Compounds (list()).
Nested Compounds are handled by recursion.

def to_vector(objs: Iterable) -> ShapeList:
return ShapeList([Vector(v) if isinstance(v, Vertex) else v for v in objs])

def to_vertex(objs: Iterable) -> ShapeList:
return ShapeList([Vertex(v) if isinstance(v, Vector) else v for v in objs])

def bool_op(
args: Sequence,
tools: Sequence,
operation: BRepAlgoAPI_Common | BRepAlgoAPI_Section,
) -> ShapeList:
# Wrap Shape._bool_op for corrected output
intersections: Shape | ShapeList = Shape()._bool_op(args, tools, operation)
if isinstance(intersections, ShapeList):
return intersections
if isinstance(intersections, Shape) and not intersections.is_null:
return ShapeList([intersections])
return ShapeList()
Args:
other: Shape or geometry object to intersect with
tolerance: tolerance for intersection detection
include_touched: if True, include boundary contacts
(only relevant when Solids are involved)
"""
# Convert geometry objects
if isinstance(other, Vector):
other = Vertex(other)
elif isinstance(other, Location):
other = Vertex(other.position)
elif isinstance(other, Axis):
other = Edge(other)
elif isinstance(other, Plane):
other = Face(other)

# Get self elements: assembly children or OCCT direct children
self_elements = self.children if self.children else list(self)

if not self_elements:
return None

results: ShapeList = ShapeList()

# Distribute over elements (OR semantics for Compound arguments)
if isinstance(other, Compound):
other_elements = other.children if other.children else list(other)
else:
other_elements = [other]

def expand_compound(compound: Compound) -> ShapeList:
shapes = ShapeList(compound.children)
for shape_type in [Vertex, Edge, Wire, Face, Shell, Solid]:
shapes.extend(compound.get_type(shape_type))
return shapes

def filter_shapes_by_order(shapes: ShapeList, orders: list) -> ShapeList:
# Remove lower order shapes from list which *appear* to be part of
# a higher order shape using a lazy distance check
# (sufficient for vertices, may be an issue for higher orders)
order_groups = []
for order in orders:
order_groups.append(
ShapeList([s for s in shapes if isinstance(s, order)])
for self_elem in self_elements:
for other_elem in other_elements:
intersection = self_elem._intersect(
other_elem, tolerance, include_touched
)
if intersection:
results.extend(intersection)

filtered_shapes = order_groups[-1]
for i in range(len(order_groups) - 1):
los = order_groups[i]
his: list = sum(order_groups[i + 1 :], [])
filtered_shapes.extend(
ShapeList(
lo
for lo in los
if all(lo.distance_to(hi) > TOLERANCE for hi in his)
)
)
# Remove duplicates using Shape's __hash__
unique = ShapeList(set(results))

return filtered_shapes

common_set: ShapeList[Shape] = expand_compound(self)
target: ShapeList | Shape
for other in to_intersect:
# Conform target type
match other:
case Axis():
# BRepAlgoAPI_Section seems happier if Edge isnt infinite
bbox = self.bounding_box()
dist = self.distance_to(other.position)
dist = dist if dist >= 1 else 1
target = Edge.make_line(
other.position - other.direction * bbox.diagonal * dist,
other.position + other.direction * bbox.diagonal * dist,
)
case Plane():
target = Face(other)
case Vector():
target = Vertex(other)
case Location():
target = Vertex(other.position)
case Compound():
target = expand_compound(other)
case _ if issubclass(type(other), Shape):
target = other
case _:
raise ValueError(f"Unsupported type to_intersect: {type(other)}")

# Find common matches
common: list[Vertex | Edge | Wire | Face | Shell | Solid] = []
result: ShapeList
for obj in common_set:
if isinstance(target, Shape):
target = ShapeList([target])
result = ShapeList()
for t in target:
operation: BRepAlgoAPI_Section | BRepAlgoAPI_Common = (
BRepAlgoAPI_Section()
)
result.extend(bool_op((obj,), (t,), operation))
if (
not isinstance(obj, Edge | Wire)
and not isinstance(t, Edge | Wire)
) or (
isinstance(obj, Solid | Compound)
or isinstance(t, Solid | Compound)
):
# Face + Edge combinations may produce an intersection
# with Common but always with Section.
# No easy way to deduplicate
# Many Solid + Edge combinations need Common
operation = BRepAlgoAPI_Common()
result.extend(bool_op((obj,), (t,), operation))

if result:
common.extend(result)

expanded: ShapeList = ShapeList()
if common:
for shape in common:
if isinstance(shape, Compound):
expanded.extend(expand_compound(shape))
else:
expanded.append(shape)

if expanded:
common_set = ShapeList()
for shape in expanded:
if isinstance(shape, Wire):
common_set.extend(shape.edges())
elif isinstance(shape, Shell):
common_set.extend(shape.faces())
else:
common_set.append(shape)
common_set = to_vertex(set(to_vector(common_set)))
common_set = filter_shapes_by_order(
common_set, [Vertex, Edge, Face, Solid]
)
else:
return None
return unique if unique else None

def touch(
self, other: Shape, tolerance: float = 1e-6
) -> ShapeList[Vertex | Edge | Face]:
"""Distribute touch over compound elements.

Iterates over elements and collects touch results. Only Solid and
Face elements produce boundary contacts; other shapes return empty.

Args:
other: Shape to check boundary contacts with
tolerance: tolerance for contact detection

Returns:
ShapeList of boundary contact geometry (empty if no contact)
"""
results: ShapeList = ShapeList()

# Get elements: assembly children or OCCT direct children
elements = self.children if self.children else list(self)

for elem in elements:
results.extend(elem.touch(other, tolerance))

return ShapeList(common_set)
return ShapeList(set(results))

def project_to_viewport(
self,
Expand Down
Loading
Loading