mirror of
https://github.com/gumyr/build123d.git
synced 2026-01-21 03:51:30 -08:00
reimplement intersect as intersect+touch top-down to avoid expensive filtering
This commit is contained in:
parent
2f92b2e03e
commit
d3ab5b24fa
6 changed files with 600 additions and 556 deletions
|
|
@ -713,149 +713,78 @@ class Compound(Mixin3D[TopoDS_Compound]):
|
|||
|
||||
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,
|
||||
tolerance: float = 1e-6,
|
||||
include_touched: bool = False,
|
||||
) -> ShapeList | None:
|
||||
"""Single-object intersection for Compound (OR semantics).
|
||||
|
||||
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)
|
||||
|
||||
Handles both build123d assemblies (children) and OCCT Compounds (list()).
|
||||
Nested Compounds are handled by recursion.
|
||||
|
||||
Args:
|
||||
to_intersect (Shape | Vector | Location | Axis | Plane): objects to intersect
|
||||
other: Shape to intersect with
|
||||
tolerance: tolerance for intersection detection
|
||||
include_touched: if True, include boundary contacts
|
||||
(only relevant when Solids are involved)
|
||||
"""
|
||||
# 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]
|
||||
|
||||
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)
|
||||
|
||||
# Remove duplicates using Shape's __hash__
|
||||
unique = ShapeList(set(results))
|
||||
|
||||
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[Vertex | Edge | Face | Solid] | None: ShapeList of vertices, edges,
|
||||
faces, and/or solids.
|
||||
ShapeList of boundary contact geometry (empty if no contact)
|
||||
"""
|
||||
results: ShapeList = ShapeList()
|
||||
|
||||
def to_vector(objs: Iterable) -> ShapeList:
|
||||
return ShapeList([Vector(v) if isinstance(v, Vertex) else v for v in objs])
|
||||
# Get elements: assembly children or OCCT direct children
|
||||
elements = self.children if self.children else list(self)
|
||||
|
||||
def to_vertex(objs: Iterable) -> ShapeList:
|
||||
return ShapeList([Vertex(v) if isinstance(v, Vector) else v for v in objs])
|
||||
for elem in elements:
|
||||
results.extend(elem.touch(other, tolerance))
|
||||
|
||||
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()
|
||||
|
||||
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)])
|
||||
)
|
||||
|
||||
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)
|
||||
)
|
||||
)
|
||||
|
||||
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 ShapeList(common_set)
|
||||
return ShapeList(set(results))
|
||||
|
||||
def project_to_viewport(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -88,7 +88,12 @@ from OCP.BRepOffsetAPI import BRepOffsetAPI_MakeOffset
|
|||
from OCP.BRepPrimAPI import BRepPrimAPI_MakeHalfSpace
|
||||
from OCP.BRepProj import BRepProj_Projection
|
||||
from OCP.BRepTools import BRepTools, BRepTools_WireExplorer
|
||||
from OCP.GC import GC_MakeArcOfCircle, GC_MakeArcOfEllipse, GC_MakeArcOfParabola, GC_MakeArcOfHyperbola
|
||||
from OCP.GC import (
|
||||
GC_MakeArcOfCircle,
|
||||
GC_MakeArcOfEllipse,
|
||||
GC_MakeArcOfParabola,
|
||||
GC_MakeArcOfHyperbola,
|
||||
)
|
||||
from OCP.GCPnts import (
|
||||
GCPnts_AbscissaPoint,
|
||||
GCPnts_QuasiUniformDeflection,
|
||||
|
|
@ -708,126 +713,53 @@ class Mixin1D(Shape[TOPODS]):
|
|||
|
||||
return Vector(curve.Value(umax))
|
||||
|
||||
def intersect(
|
||||
self, *to_intersect: Shape | Vector | Location | Axis | Plane
|
||||
) -> None | ShapeList[Vertex | Edge]:
|
||||
"""Intersect Edge with Shape or geometry object
|
||||
def _intersect(
|
||||
self,
|
||||
other: Shape,
|
||||
tolerance: float = 1e-6,
|
||||
include_touched: bool = False,
|
||||
) -> ShapeList | None:
|
||||
"""Single-object intersection for Edge/Wire.
|
||||
|
||||
Returns same-dimension overlap or crossing geometry:
|
||||
- 1D + 1D → Edge (collinear overlap) + Vertex (crossing)
|
||||
- 1D + Face/Solid/Compound → delegates to other._intersect(self)
|
||||
|
||||
Args:
|
||||
to_intersect (Shape | Vector | Location | Axis | Plane): objects to intersect
|
||||
|
||||
Returns:
|
||||
ShapeList[Vertex | Edge] | None: ShapeList of vertices and/or edges
|
||||
other: Shape to intersect with
|
||||
tolerance: tolerance for intersection detection
|
||||
include_touched: if True, include boundary contacts
|
||||
(only relevant when Solids are involved)
|
||||
"""
|
||||
|
||||
def to_vector(objs: Iterable) -> ShapeList:
|
||||
return ShapeList([Vector(v) if isinstance(v, Vertex) else v for v in objs])
|
||||
results: ShapeList = ShapeList()
|
||||
|
||||
def to_vertex(objs: Iterable) -> ShapeList:
|
||||
return ShapeList([Vertex(v) if isinstance(v, Vector) else v for v in objs])
|
||||
# 1D + 1D: Common (collinear overlap) + Section (crossing vertices)
|
||||
if isinstance(other, (Edge, Wire)):
|
||||
common = self._bool_op(
|
||||
(self,), (other,), BRepAlgoAPI_Common(), as_list=True
|
||||
)
|
||||
results.extend(common.expand())
|
||||
section = self._bool_op(
|
||||
(self,), (other,), BRepAlgoAPI_Section(), as_list=True
|
||||
)
|
||||
# Extract vertices from section (edges already in Common for wires)
|
||||
for shape in section:
|
||||
if isinstance(shape, Vertex) and not shape.is_null:
|
||||
results.append(shape)
|
||||
|
||||
def bool_op(
|
||||
args: Sequence,
|
||||
tools: Sequence,
|
||||
operation: BRepAlgoAPI_Section | BRepAlgoAPI_Common,
|
||||
) -> ShapeList:
|
||||
# Wrap Shape._bool_op for corrected output
|
||||
intersections: Shape | ShapeList = Shape()._bool_op(args, tools, operation)
|
||||
if isinstance(intersections, ShapeList):
|
||||
return intersections or ShapeList()
|
||||
if isinstance(intersections, Shape) and not intersections.is_null:
|
||||
return ShapeList([intersections])
|
||||
return ShapeList()
|
||||
# 1D + Vertex: point containment on edge
|
||||
elif isinstance(other, Vertex):
|
||||
if other.distance_to(self) <= tolerance:
|
||||
results.append(other)
|
||||
|
||||
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)])
|
||||
)
|
||||
# Delegate to higher-order shapes (Face, Solid, etc.)
|
||||
else:
|
||||
result = other._intersect(self, tolerance, include_touched)
|
||||
if result:
|
||||
results.extend(result)
|
||||
|
||||
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)
|
||||
)
|
||||
)
|
||||
|
||||
return filtered_shapes
|
||||
|
||||
common_set: ShapeList[Vertex | Edge | Wire] = ShapeList([self])
|
||||
target: Shape | Plane
|
||||
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 = other
|
||||
case Vector():
|
||||
target = Vertex(other)
|
||||
case Location():
|
||||
target = Vertex(other.position)
|
||||
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] = []
|
||||
result: ShapeList | None
|
||||
for obj in common_set:
|
||||
match (obj, target):
|
||||
case (_, Plane()):
|
||||
assert isinstance(other.wrapped, gp_Pln)
|
||||
target = Shape(BRepBuilderAPI_MakeFace(other.wrapped).Face())
|
||||
operation1 = BRepAlgoAPI_Section()
|
||||
result = bool_op((obj,), (target,), operation1)
|
||||
operation2 = BRepAlgoAPI_Common()
|
||||
result.extend(bool_op((obj,), (target,), operation2))
|
||||
|
||||
case (_, Vertex() | Edge() | Wire()):
|
||||
operation1 = BRepAlgoAPI_Section()
|
||||
section = bool_op((obj,), (target,), operation1)
|
||||
result = section
|
||||
if not section:
|
||||
operation2 = BRepAlgoAPI_Common()
|
||||
result.extend(bool_op((obj,), (target,), operation2))
|
||||
|
||||
case _ if issubclass(type(target), Shape):
|
||||
result = target.intersect(obj)
|
||||
|
||||
if result:
|
||||
common.extend(result)
|
||||
|
||||
if common:
|
||||
common_set = ShapeList()
|
||||
for shape in common:
|
||||
if isinstance(shape, Wire):
|
||||
common_set.extend(shape.edges())
|
||||
else:
|
||||
common_set.append(shape)
|
||||
common_set = to_vertex(set(to_vector(common_set)))
|
||||
common_set = filter_shapes_by_order(common_set, [Vertex, Edge])
|
||||
else:
|
||||
return None
|
||||
|
||||
return ShapeList(common_set)
|
||||
return results if results else None
|
||||
|
||||
def location_at(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -1343,66 +1343,66 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
)
|
||||
|
||||
def intersect(
|
||||
self, *to_intersect: Shape | Vector | Location | Axis | Plane
|
||||
) -> None | ShapeList[Self]:
|
||||
"""Intersection of the arguments and this shape
|
||||
self,
|
||||
*to_intersect: Shape | Vector | Location | Axis | Plane,
|
||||
tolerance: float = 1e-6,
|
||||
include_touched: bool = False,
|
||||
) -> ShapeList | None:
|
||||
"""Find where bodies/interiors meet (overlap or crossing geometry).
|
||||
|
||||
This is the main entry point for intersection operations. Handles
|
||||
geometry conversion and delegates to subclass _intersect() implementations.
|
||||
|
||||
Semantics:
|
||||
- Multiple arguments use AND (chaining): c.intersect(s1, s2) = c ∩ s1 ∩ s2
|
||||
- Compound arguments use OR (distribution): c.intersect(Compound([s1, s2]))
|
||||
= (c ∩ s1) ∪ (c ∩ s2)
|
||||
|
||||
Args:
|
||||
to_intersect (sequence of Union[Shape, Axis, Plane]): Shape(s) to
|
||||
intersect with
|
||||
to_intersect: Shape(s) or geometry objects to intersect with
|
||||
tolerance: tolerance for intersection detection
|
||||
include_touched: if True, include boundary contacts without interior
|
||||
overlap (only relevant when Solids are involved)
|
||||
|
||||
Returns:
|
||||
None | ShapeList[Self]: Resulting ShapeList may contain different class
|
||||
than self
|
||||
ShapeList of intersection results, or None if no intersection
|
||||
"""
|
||||
|
||||
def _to_vertex(vec: Vector) -> Vertex:
|
||||
"""Helper method to convert vector to shape"""
|
||||
return self.__class__.cast(
|
||||
downcast(
|
||||
BRepBuilderAPI_MakeVertex(gp_Pnt(vec.X, vec.Y, vec.Z)).Vertex()
|
||||
)
|
||||
)
|
||||
if not to_intersect:
|
||||
return None
|
||||
|
||||
def _to_edge(axis: Axis) -> Edge:
|
||||
"""Helper method to convert axis to shape"""
|
||||
return self.__class__.cast(
|
||||
BRepBuilderAPI_MakeEdge(
|
||||
Geom_Line(
|
||||
axis.position.to_pnt(),
|
||||
axis.direction.to_dir(),
|
||||
)
|
||||
).Edge()
|
||||
)
|
||||
# Runtime import to avoid circular imports. Allows type safe actions in helpers
|
||||
from build123d.topology.helpers import convert_to_shapes
|
||||
|
||||
def _to_face(plane: Plane) -> Face:
|
||||
"""Helper method to convert plane to shape"""
|
||||
return self.__class__.cast(BRepBuilderAPI_MakeFace(plane.wrapped).Face())
|
||||
shapes = convert_to_shapes(self, to_intersect)
|
||||
|
||||
# Convert any geometry objects into their respective topology objects
|
||||
objs = []
|
||||
for obj in to_intersect:
|
||||
if isinstance(obj, Vector):
|
||||
objs.append(_to_vertex(obj))
|
||||
elif isinstance(obj, Axis):
|
||||
objs.append(_to_edge(obj))
|
||||
elif isinstance(obj, Plane):
|
||||
objs.append(_to_face(obj))
|
||||
elif isinstance(obj, Location):
|
||||
if obj.wrapped is None:
|
||||
raise ValueError("Cannot intersect with an empty location")
|
||||
objs.append(_to_vertex(tcast(Vector, obj.position)))
|
||||
else:
|
||||
objs.append(obj)
|
||||
# Chained iteration for AND semantics: c.intersect(s1, s2) = c ∩ s1 ∩ s2
|
||||
common_set = ShapeList([self])
|
||||
for other in shapes:
|
||||
next_set = ShapeList()
|
||||
for obj in common_set:
|
||||
result = obj._intersect(other, tolerance, include_touched)
|
||||
if result:
|
||||
next_set.extend(result.expand())
|
||||
if not next_set:
|
||||
return None # AND semantics: if any step fails, no intersection
|
||||
common_set = ShapeList(set(next_set)) # deduplicate
|
||||
return common_set if common_set else None
|
||||
|
||||
# Find the shape intersections
|
||||
intersect_op = BRepAlgoAPI_Common()
|
||||
intersections = self._bool_op((self,), objs, intersect_op)
|
||||
if isinstance(intersections, ShapeList):
|
||||
return intersections or None
|
||||
if isinstance(intersections, Shape) and not intersections.is_null:
|
||||
return ShapeList([intersections])
|
||||
return None
|
||||
def touch(self, other: Shape, tolerance: float = 1e-6) -> ShapeList:
|
||||
"""Find boundary contacts between this shape and another.
|
||||
|
||||
Base implementation returns empty ShapeList. Subclasses (Mixin2D, Mixin3D,
|
||||
Compound) override this to provide actual touch detection.
|
||||
|
||||
Args:
|
||||
other: Shape to find contacts with
|
||||
tolerance: tolerance for contact detection
|
||||
|
||||
Returns:
|
||||
ShapeList of contact shapes (empty for base implementation)
|
||||
"""
|
||||
return ShapeList()
|
||||
|
||||
def is_equal(self, other: Shape) -> bool:
|
||||
"""Returns True if two shapes are equal, i.e. if they share the same
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ from typing_extensions import Self
|
|||
import OCP.TopAbs as ta
|
||||
from OCP.BRepAlgoAPI import BRepAlgoAPI_Common, BRepAlgoAPI_Cut, BRepAlgoAPI_Section
|
||||
from OCP.BRepBuilderAPI import BRepBuilderAPI_MakeSolid
|
||||
from OCP.BRepExtrema import BRepExtrema_DistShapeShape
|
||||
from OCP.BRepClass3d import BRepClass3d_SolidClassifier
|
||||
from OCP.BRepFeat import BRepFeat_MakeDPrism
|
||||
from OCP.BRepFilletAPI import BRepFilletAPI_MakeChamfer, BRepFilletAPI_MakeFillet
|
||||
|
|
@ -95,6 +96,7 @@ from OCP.TopoDS import (
|
|||
TopoDS_Shell,
|
||||
TopoDS_Solid,
|
||||
TopoDS_Wire,
|
||||
TopoDS_Compound,
|
||||
)
|
||||
from OCP.gp import gp_Ax2, gp_Pnt
|
||||
from build123d.build_enums import CenterOf, GeomType, Keep, Kind, Transition, Until
|
||||
|
|
@ -425,131 +427,68 @@ class Mixin3D(Shape[TOPODS]):
|
|||
|
||||
return return_value
|
||||
|
||||
def intersect(
|
||||
self, *to_intersect: Shape | Vector | Location | Axis | Plane
|
||||
) -> None | ShapeList[Vertex | Edge | Face | Solid]:
|
||||
"""Intersect Solid with Shape or geometry object
|
||||
def _intersect(
|
||||
self,
|
||||
other: Shape,
|
||||
tolerance: float = 1e-6,
|
||||
include_touched: bool = False,
|
||||
) -> ShapeList | None:
|
||||
"""Single-object intersection for Solid.
|
||||
|
||||
Returns same-dimension overlap or crossing geometry:
|
||||
- Solid + Solid → Solid (volume overlap)
|
||||
- Solid + Face → Face (portion in/on solid)
|
||||
- Solid + Edge → Edge (portion through solid)
|
||||
|
||||
Args:
|
||||
to_intersect (Shape | Vector | Location | Axis | Plane): objects to intersect
|
||||
|
||||
Returns:
|
||||
ShapeList[Vertex | Edge | Face | Solid] | None: ShapeList of vertices, edges,
|
||||
faces, and/or solids.
|
||||
other: Shape to intersect with
|
||||
tolerance: tolerance for intersection detection
|
||||
include_touched: if True, include boundary contacts
|
||||
(shapes touching the solid's surface without penetrating)
|
||||
"""
|
||||
|
||||
def to_vector(objs: Iterable) -> ShapeList:
|
||||
return ShapeList([Vector(v) if isinstance(v, Vertex) else v for v in objs])
|
||||
results: ShapeList = ShapeList()
|
||||
|
||||
def to_vertex(objs: Iterable) -> ShapeList:
|
||||
return ShapeList([Vertex(v) if isinstance(v, Vector) else v for v in objs])
|
||||
# Solid + Solid/Face/Shell/Edge/Wire: use Common
|
||||
if isinstance(other, (Solid, Face, Shell, Edge, Wire)):
|
||||
intersection = self._bool_op(
|
||||
(self,), (other,), BRepAlgoAPI_Common(), as_list=True
|
||||
)
|
||||
results.extend(intersection.expand())
|
||||
# Solid + Vertex: point containment check
|
||||
elif isinstance(other, Vertex):
|
||||
if self.is_inside(Vector(other), tolerance):
|
||||
results.append(other)
|
||||
|
||||
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 or ShapeList()
|
||||
if isinstance(intersections, Shape) and not intersections.is_null:
|
||||
return ShapeList([intersections])
|
||||
return ShapeList()
|
||||
# Delegate to higher-order shapes (Compound)
|
||||
else:
|
||||
result = other._intersect(self, tolerance, include_touched)
|
||||
if result:
|
||||
results.extend(result)
|
||||
|
||||
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)])
|
||||
# Add boundary contacts if requested (only Solid has touch method)
|
||||
if include_touched and isinstance(self, Solid):
|
||||
results.extend(self.touch(other, tolerance))
|
||||
results = ShapeList(set(results))
|
||||
# Filter: remove lower-dimensional shapes that are boundaries of
|
||||
# higher-dimensional results (e.g., vertices on edges, edges on faces)
|
||||
edges_in_results = [r for r in results if isinstance(r, Edge)]
|
||||
faces_in_results = [r for r in results if isinstance(r, Face)]
|
||||
results = ShapeList(
|
||||
r
|
||||
for r in results
|
||||
if not (
|
||||
isinstance(r, Vertex)
|
||||
and any(r.distance_to(e) <= tolerance for e in edges_in_results)
|
||||
)
|
||||
|
||||
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)
|
||||
and not (
|
||||
isinstance(r, Edge)
|
||||
and any(
|
||||
r.center().distance_to(f) <= tolerance for f in faces_in_results
|
||||
)
|
||||
)
|
||||
|
||||
return filtered_shapes
|
||||
|
||||
common_set: ShapeList[Vertex | Edge | Face | Solid] = ShapeList([self])
|
||||
target: 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 _ 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 | None
|
||||
for obj in common_set:
|
||||
match (obj, target):
|
||||
case (_, Vertex() | Edge() | Wire() | Face() | Shell() | Solid()):
|
||||
operation: BRepAlgoAPI_Section | BRepAlgoAPI_Common = (
|
||||
BRepAlgoAPI_Section()
|
||||
)
|
||||
result = bool_op((obj,), (target,), operation)
|
||||
if (
|
||||
not isinstance(obj, Edge | Wire)
|
||||
and not isinstance(target, (Edge | Wire))
|
||||
) or (isinstance(obj, Solid) or isinstance(target, Solid)):
|
||||
# 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,), (target,), operation))
|
||||
|
||||
case _ if issubclass(type(target), Shape):
|
||||
result = target.intersect(obj)
|
||||
|
||||
if result:
|
||||
common.extend(result)
|
||||
|
||||
if common:
|
||||
common_set = ShapeList()
|
||||
for shape in common:
|
||||
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 ShapeList(common_set)
|
||||
)
|
||||
return results if results else None
|
||||
|
||||
def is_inside(self, point: VectorLike, tolerance: float = 1.0e-6) -> bool:
|
||||
"""Returns whether or not the point is inside a solid or compound
|
||||
|
|
@ -794,6 +733,178 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
|||
# when density == 1, mass == volume
|
||||
return Shape.compute_mass(self)
|
||||
|
||||
# ---- Instance Methods ----
|
||||
|
||||
def touch(
|
||||
self, other: Shape, tolerance: float = 1e-6
|
||||
) -> ShapeList[Vertex | Edge | Face]:
|
||||
"""Find where this Solid's boundary contacts another shape.
|
||||
|
||||
Returns geometry where boundaries contact without interior overlap:
|
||||
- Solid + Solid → Face + Edge + Vertex (all boundary contacts)
|
||||
- Solid + Face/Shell → Edge + Vertex (face boundary on solid boundary)
|
||||
- Solid + Edge/Wire → Vertex (edge endpoints on solid boundary)
|
||||
- Solid + Vertex → Vertex if on boundary
|
||||
- Solid + Compound → distributes over compound elements
|
||||
|
||||
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()
|
||||
|
||||
if isinstance(other, Solid):
|
||||
# Solid + Solid: find all boundary contacts (faces, edges, vertices)
|
||||
# Pre-calculate bounding boxes (optimal=False for speed, used only for filtering)
|
||||
self_faces = [(f, f.bounding_box(optimal=False)) for f in self.faces()]
|
||||
other_faces = [(f, f.bounding_box(optimal=False)) for f in other.faces()]
|
||||
self_edges = [(e, e.bounding_box(optimal=False)) for e in self.edges()]
|
||||
other_edges = [(e, e.bounding_box(optimal=False)) for e in other.edges()]
|
||||
|
||||
# Face-Face contacts (collect first)
|
||||
found_faces: ShapeList = ShapeList()
|
||||
for sf, sf_bb in self_faces:
|
||||
for of, of_bb in other_faces:
|
||||
if not sf_bb.overlaps(of_bb, tolerance):
|
||||
continue
|
||||
common = self._bool_op(
|
||||
(sf,), (of,), BRepAlgoAPI_Common(), as_list=True
|
||||
)
|
||||
found_faces.extend(s for s in common if not s.is_null)
|
||||
results.extend(found_faces)
|
||||
|
||||
# Edge-Edge contacts (skip if on any found face)
|
||||
found_edges: ShapeList = ShapeList()
|
||||
for se, se_bb in self_edges:
|
||||
for oe, oe_bb in other_edges:
|
||||
if not se_bb.overlaps(oe_bb, tolerance):
|
||||
continue
|
||||
common = self._bool_op(
|
||||
(se,), (oe,), BRepAlgoAPI_Common(), as_list=True
|
||||
)
|
||||
for s in common:
|
||||
if s.is_null:
|
||||
continue
|
||||
# Skip if edge is on any found face (use midpoint)
|
||||
mid = s.center()
|
||||
on_face = any(
|
||||
f.distance_to(mid) <= tolerance for f in found_faces
|
||||
)
|
||||
if not on_face:
|
||||
found_edges.append(s)
|
||||
results.extend(found_edges)
|
||||
|
||||
# Vertex-Vertex contacts (skip if on any found face or edge)
|
||||
found_vertices: ShapeList = ShapeList()
|
||||
for sv in self.vertices():
|
||||
for ov in other.vertices():
|
||||
if sv.distance_to(ov) <= tolerance:
|
||||
on_face = any(
|
||||
sv.distance_to(f) <= tolerance for f in found_faces
|
||||
)
|
||||
on_edge = any(
|
||||
sv.distance_to(e) <= tolerance for e in found_edges
|
||||
)
|
||||
already = any(
|
||||
sv.distance_to(v) <= tolerance for v in found_vertices
|
||||
)
|
||||
if not on_face and not on_edge and not already:
|
||||
results.append(sv)
|
||||
found_vertices.append(sv)
|
||||
break
|
||||
|
||||
# Tangent contacts (skip if on any found face or edge)
|
||||
# Use Mixin2D.touch() on face pairs to find tangent contacts (e.g., sphere touching box)
|
||||
for sf, sf_bb in self_faces:
|
||||
for of, of_bb in other_faces:
|
||||
if not sf_bb.overlaps(of_bb, tolerance):
|
||||
continue
|
||||
tangent_vertices = sf.touch(of, tolerance, found_faces, found_edges)
|
||||
for v in tangent_vertices:
|
||||
already_found = any(
|
||||
v.distance_to(existing) <= tolerance
|
||||
for existing in found_vertices
|
||||
)
|
||||
if not already_found:
|
||||
results.append(v)
|
||||
found_vertices.append(v)
|
||||
|
||||
elif isinstance(other, (Face, Shell)):
|
||||
# Solid + Face: find where face boundary meets solid boundary
|
||||
# Pre-calculate bounding boxes (optimal=False for speed, used only for filtering)
|
||||
self_faces = [(f, f.bounding_box(optimal=False)) for f in self.faces()]
|
||||
other_edges = [(e, e.bounding_box(optimal=False)) for e in other.edges()]
|
||||
|
||||
# Check face's edges touching solid's faces
|
||||
for oe, oe_bb in other_edges:
|
||||
for sf, sf_bb in self_faces:
|
||||
if not oe_bb.overlaps(sf_bb, tolerance):
|
||||
continue
|
||||
common = self._bool_op(
|
||||
(oe,), (sf,), BRepAlgoAPI_Common(), as_list=True
|
||||
)
|
||||
results.extend(s for s in common if not s.is_null)
|
||||
# Check face's vertices touching solid's edges (corner coincident)
|
||||
for ov in other.vertices():
|
||||
for se in self.edges():
|
||||
if ov.distance_to(se) <= tolerance:
|
||||
results.append(ov)
|
||||
break
|
||||
|
||||
elif isinstance(other, (Edge, Wire)):
|
||||
# Solid + Edge: find where edge endpoints touch solid boundary
|
||||
# Pre-calculate bounding boxes (optimal=False for speed, used only for filtering)
|
||||
self_faces = [(f, f.bounding_box(optimal=False)) for f in self.faces()]
|
||||
other_bb = other.bounding_box(optimal=False)
|
||||
|
||||
for ov in other.vertices():
|
||||
for sf, _ in self_faces:
|
||||
if ov.distance_to(sf) <= tolerance:
|
||||
results.append(ov)
|
||||
break
|
||||
# Use BRepExtrema to find tangent contacts (edge tangent to surface)
|
||||
# Only valid if edge doesn't penetrate solid (Common returns nothing)
|
||||
# If Common returns something, contact points are entry/exit (intersect, not touches)
|
||||
common_result = self._bool_op(
|
||||
(self,), (other,), BRepAlgoAPI_Common(), as_list=True
|
||||
)
|
||||
if not common_result: # No penetration - could be tangent
|
||||
for sf, sf_bb in self_faces:
|
||||
if not sf_bb.overlaps(other_bb, tolerance):
|
||||
continue
|
||||
extrema = BRepExtrema_DistShapeShape(sf.wrapped, other.wrapped)
|
||||
if extrema.IsDone() and extrema.Value() <= tolerance:
|
||||
for i in range(1, extrema.NbSolution() + 1):
|
||||
pnt1 = extrema.PointOnShape1(i)
|
||||
pnt2 = extrema.PointOnShape2(i)
|
||||
# Verify points are actually close
|
||||
if pnt1.Distance(pnt2) > tolerance:
|
||||
continue
|
||||
new_vertex = Vertex(pnt1.X(), pnt1.Y(), pnt1.Z())
|
||||
# Only add if not already covered by existing results
|
||||
already_found = any(
|
||||
new_vertex.distance_to(r) <= tolerance for r in results
|
||||
)
|
||||
if not already_found:
|
||||
results.append(new_vertex)
|
||||
|
||||
elif isinstance(other, Vertex):
|
||||
# Solid + Vertex: check if vertex is on boundary
|
||||
for sf in self.faces():
|
||||
if other.distance_to(sf) <= tolerance:
|
||||
results.append(other)
|
||||
break
|
||||
|
||||
# Delegate to other shapes (Compound iterates, others return empty)
|
||||
else:
|
||||
results.extend(other.touch(self, tolerance))
|
||||
|
||||
# Remove duplicates using Shape's __hash__ and __eq__
|
||||
return ShapeList(set(results))
|
||||
|
||||
# ---- Class Methods ----
|
||||
|
||||
@classmethod
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ from OCP.BRep import BRep_Builder, BRep_Tool
|
|||
from OCP.BRepAdaptor import BRepAdaptor_Curve, BRepAdaptor_Surface
|
||||
from OCP.BRepAlgo import BRepAlgo
|
||||
from OCP.BRepAlgoAPI import BRepAlgoAPI_Common, BRepAlgoAPI_Section
|
||||
from OCP.BRepExtrema import BRepExtrema_DistShapeShape
|
||||
from OCP.BRepBuilderAPI import (
|
||||
BRepBuilderAPI_MakeEdge,
|
||||
BRepBuilderAPI_MakeFace,
|
||||
|
|
@ -115,7 +116,13 @@ from OCP.TColStd import (
|
|||
TColStd_HArray2OfReal,
|
||||
)
|
||||
from OCP.TopExp import TopExp
|
||||
from OCP.TopoDS import TopoDS, TopoDS_Face, TopoDS_Shape, TopoDS_Shell, TopoDS_Solid
|
||||
from OCP.TopoDS import (
|
||||
TopoDS,
|
||||
TopoDS_Face,
|
||||
TopoDS_Shape,
|
||||
TopoDS_Shell,
|
||||
TopoDS_Solid,
|
||||
)
|
||||
from OCP.TopTools import TopTools_IndexedDataMapOfShapeListOfShape, TopTools_ListOfShape
|
||||
from ocp_gordon import interpolate_curve_network
|
||||
from typing_extensions import Self
|
||||
|
|
@ -274,127 +281,205 @@ class Mixin2D(ABC, Shape[TOPODS]):
|
|||
|
||||
return result
|
||||
|
||||
def intersect(
|
||||
self, *to_intersect: Shape | Vector | Location | Axis | Plane
|
||||
) -> None | ShapeList[Vertex | Edge | Face]:
|
||||
"""Intersect Face with Shape or geometry object
|
||||
def _intersect(
|
||||
self,
|
||||
other: Shape,
|
||||
tolerance: float = 1e-6,
|
||||
include_touched: bool = False,
|
||||
) -> ShapeList | None:
|
||||
"""Single-object intersection for Face/Shell.
|
||||
|
||||
Returns same-dimension overlap or crossing geometry:
|
||||
- 2D + 2D → Face (coplanar overlap) + Edge (crossing curves)
|
||||
- 2D + Edge → Edge (on surface) + Vertex (piercing)
|
||||
- 2D + Solid/Compound → delegates to other._intersect(self)
|
||||
|
||||
Args:
|
||||
to_intersect (Shape | Vector | Location | Axis | Plane): objects to intersect
|
||||
|
||||
Returns:
|
||||
ShapeList[Vertex | Edge | Face] | None: ShapeList of vertices, edges, and/or
|
||||
faces.
|
||||
other: Shape to intersect with
|
||||
tolerance: tolerance for intersection detection
|
||||
include_touched: if True, include boundary contacts
|
||||
(only relevant when Solids are involved)
|
||||
"""
|
||||
|
||||
def to_vector(objs: Iterable) -> ShapeList:
|
||||
return ShapeList([Vector(v) if isinstance(v, Vertex) else v for v in objs])
|
||||
def filter_edges(
|
||||
section_edges: ShapeList[Edge], common_edges: ShapeList[Edge]
|
||||
) -> ShapeList[Edge]:
|
||||
"""Filter section edges, keeping only edges not on common face boundaries."""
|
||||
# Pre-compute bounding boxes for both sets (optimal=False for speed, filtering only)
|
||||
section_bboxes = [(e, e.bounding_box(optimal=False)) for e in section_edges]
|
||||
common_bboxes = [
|
||||
(ce, ce.bounding_box(optimal=False)) for ce in common_edges
|
||||
]
|
||||
|
||||
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_Section | BRepAlgoAPI_Common,
|
||||
) -> ShapeList:
|
||||
# Wrap Shape._bool_op for corrected output
|
||||
intersections: Shape | ShapeList = Shape()._bool_op(args, tools, operation)
|
||||
if isinstance(intersections, ShapeList):
|
||||
return intersections or ShapeList()
|
||||
if isinstance(intersections, Shape) and not intersections.is_null:
|
||||
return ShapeList([intersections])
|
||||
return ShapeList()
|
||||
|
||||
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)])
|
||||
# Filter: remove section edges that coincide with common face boundaries
|
||||
filtered: ShapeList = ShapeList()
|
||||
for edge, edge_bbox in section_bboxes:
|
||||
is_common = any(
|
||||
edge_bbox.overlaps(ce_bbox, tolerance)
|
||||
and edge.distance_to(ce) <= tolerance
|
||||
for ce, ce_bbox in common_bboxes
|
||||
)
|
||||
if not is_common:
|
||||
filtered.append(edge)
|
||||
return filtered
|
||||
|
||||
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)
|
||||
)
|
||||
)
|
||||
results: ShapeList = ShapeList()
|
||||
|
||||
return filtered_shapes
|
||||
# 2D + 2D: Common (coplanar overlap) AND Section (crossing curves)
|
||||
if isinstance(other, (Face, Shell)):
|
||||
# Common for coplanar overlap
|
||||
common = self._bool_op(
|
||||
(self,), (other,), BRepAlgoAPI_Common(), as_list=True
|
||||
)
|
||||
common_faces = common.expand()
|
||||
results.extend(common_faces)
|
||||
|
||||
common_set: ShapeList[Vertex | Edge | Face | Shell] = ShapeList([self])
|
||||
target: 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 _ if issubclass(type(other), Shape):
|
||||
target = other
|
||||
case _:
|
||||
raise ValueError(f"Unsupported type to_intersect: {type(other)}")
|
||||
# Section for crossing curves (only edges, not vertices)
|
||||
# Vertices from Section are boundary contacts (touch), not intersections
|
||||
section = self._bool_op(
|
||||
(self,), (other,), BRepAlgoAPI_Section(), as_list=True
|
||||
)
|
||||
section_edges = ShapeList(
|
||||
[s for s in section if isinstance(s, Edge)]
|
||||
).expand()
|
||||
|
||||
# Find common matches
|
||||
common: list[Vertex | Edge | Wire | Face | Shell] = []
|
||||
result: ShapeList | None
|
||||
for obj in common_set:
|
||||
match (obj, target):
|
||||
case (_, Vertex() | Edge() | Wire() | Face() | Shell()):
|
||||
operation: BRepAlgoAPI_Section | BRepAlgoAPI_Common = (
|
||||
BRepAlgoAPI_Section()
|
||||
)
|
||||
result = bool_op((obj,), (target,), operation)
|
||||
if not isinstance(obj, Edge | Wire) and not isinstance(
|
||||
target, (Edge | Wire)
|
||||
):
|
||||
# Face + Edge combinations may produce an intersection
|
||||
# with Common but always with Section.
|
||||
# No easy way to deduplicate
|
||||
operation = BRepAlgoAPI_Common()
|
||||
result.extend(bool_op((obj,), (target,), operation))
|
||||
|
||||
case _ if issubclass(type(target), Shape):
|
||||
result = target.intersect(obj)
|
||||
|
||||
if result:
|
||||
common.extend(result)
|
||||
|
||||
if common:
|
||||
common_set = ShapeList()
|
||||
for shape in common:
|
||||
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])
|
||||
if not common_faces:
|
||||
# No coplanar overlap - all section edges are valid crossings
|
||||
results.extend(section_edges)
|
||||
else:
|
||||
return None
|
||||
# Filter out edges on common face boundaries
|
||||
# (Section returns boundary of overlap region which are not crossings)
|
||||
common_edges: ShapeList[Edge] = ShapeList()
|
||||
for face in common_faces:
|
||||
common_edges.extend(face.edges())
|
||||
results.extend(filter_edges(section_edges, common_edges))
|
||||
|
||||
return ShapeList(common_set)
|
||||
# 2D + Edge: Section for intersection
|
||||
elif isinstance(other, (Edge, Wire)):
|
||||
section = self._bool_op(
|
||||
(self,), (other,), BRepAlgoAPI_Section(), as_list=True
|
||||
)
|
||||
results.extend(section)
|
||||
|
||||
# 2D + Vertex: point containment on surface
|
||||
elif isinstance(other, Vertex):
|
||||
if other.distance_to(self) <= tolerance:
|
||||
results.append(other)
|
||||
|
||||
# Delegate to higher-order shapes (Solid, etc.)
|
||||
else:
|
||||
result = other._intersect(self, tolerance, include_touched)
|
||||
if result:
|
||||
results.extend(result)
|
||||
|
||||
# Add boundary contacts if requested
|
||||
if include_touched and isinstance(other, (Face, Shell)):
|
||||
found_faces = ShapeList(r for r in results if isinstance(r, Face))
|
||||
found_edges = ShapeList(r for r in results if isinstance(r, Edge))
|
||||
results.extend(self.touch(other, tolerance, found_faces, found_edges))
|
||||
|
||||
return results if results else None
|
||||
|
||||
def touch(
|
||||
self,
|
||||
other: Shape,
|
||||
tolerance: float = 1e-6,
|
||||
_found_faces: ShapeList | None = None,
|
||||
_found_edges: ShapeList | None = None,
|
||||
) -> ShapeList:
|
||||
"""Find boundary contacts between this 2D shape and another shape.
|
||||
|
||||
Returns the highest-dimensional contact at each location, filtered to
|
||||
avoid returning lower-dimensional boundaries of higher-dimensional contacts.
|
||||
|
||||
For Face/Shell:
|
||||
- Face + Face → Vertex (shared corner or crossing point without edge/face overlap)
|
||||
- Face + Edge/Vertex → no touch (intersect already returns dim 0)
|
||||
|
||||
Args:
|
||||
other: Shape to find contacts with
|
||||
tolerance: tolerance for contact detection
|
||||
_found_faces: (internal) pre-found faces to filter against
|
||||
_found_edges: (internal) pre-found edges to filter against
|
||||
|
||||
Returns:
|
||||
ShapeList of contact shapes (Vertex only for 2D+2D)
|
||||
"""
|
||||
results: ShapeList = ShapeList()
|
||||
|
||||
if isinstance(other, (Face, Shell)):
|
||||
# Get intersect results to filter against if not provided
|
||||
if _found_faces is None or _found_edges is None:
|
||||
_intersect_results = self._intersect(
|
||||
other, tolerance, include_touched=False
|
||||
)
|
||||
_found_faces = ShapeList()
|
||||
_found_edges = ShapeList()
|
||||
if _intersect_results:
|
||||
for r in _intersect_results:
|
||||
if isinstance(r, Face):
|
||||
_found_faces.append(r)
|
||||
elif isinstance(r, Edge):
|
||||
_found_edges.append(r)
|
||||
|
||||
found_faces = _found_faces
|
||||
found_edges = _found_edges
|
||||
|
||||
# Use BRepExtrema to find all contact points (vertex-vertex, vertex-edge, vertex-face)
|
||||
found_vertices: ShapeList = ShapeList()
|
||||
extrema = BRepExtrema_DistShapeShape(self.wrapped, other.wrapped)
|
||||
if extrema.IsDone() and extrema.Value() <= tolerance:
|
||||
for i in range(1, extrema.NbSolution() + 1):
|
||||
pnt1 = extrema.PointOnShape1(i)
|
||||
pnt2 = extrema.PointOnShape2(i)
|
||||
if pnt1.Distance(pnt2) > tolerance:
|
||||
continue
|
||||
|
||||
contact_pt = Vector(pnt1.X(), pnt1.Y(), pnt1.Z())
|
||||
new_vertex = Vertex(pnt1.X(), pnt1.Y(), pnt1.Z())
|
||||
|
||||
# Check if point is on edge boundary of either face
|
||||
on_self_edge = any(
|
||||
new_vertex.distance_to(e) <= tolerance for e in self.edges()
|
||||
)
|
||||
on_other_edge = any(
|
||||
new_vertex.distance_to(e) <= tolerance for e in other.edges()
|
||||
)
|
||||
|
||||
# Skip if point is on edges of both faces (edge-edge intersection)
|
||||
if on_self_edge and on_other_edge:
|
||||
continue
|
||||
|
||||
# For tangent contacts (point in interior of both faces),
|
||||
# verify normals are parallel or anti-parallel (tangent surfaces)
|
||||
if not on_self_edge and not on_other_edge:
|
||||
normal1 = self.normal_at(contact_pt)
|
||||
normal2 = other.normal_at(contact_pt)
|
||||
dot = normal1.dot(normal2)
|
||||
if abs(dot) < 0.99: # Not parallel or anti-parallel
|
||||
continue
|
||||
|
||||
# Filter: only keep vertices that are not boundaries of
|
||||
# higher-dimensional contacts (faces or edges) and not duplicates
|
||||
on_face = any(
|
||||
new_vertex.distance_to(f) <= tolerance for f in found_faces
|
||||
)
|
||||
on_edge = any(
|
||||
new_vertex.distance_to(e) <= tolerance for e in found_edges
|
||||
)
|
||||
already = any(
|
||||
new_vertex.distance_to(v) <= tolerance for v in found_vertices
|
||||
)
|
||||
if not on_face and not on_edge and not already:
|
||||
results.append(new_vertex)
|
||||
found_vertices.append(new_vertex)
|
||||
|
||||
# Face + Edge/Vertex: no touch (intersect already covers dim 0)
|
||||
# Delegate to other shapes (Compound iterates, others return empty)
|
||||
else:
|
||||
results.extend(other.touch(self, tolerance))
|
||||
|
||||
return results
|
||||
|
||||
@abstractmethod
|
||||
def location_at(self, *args: Any, **kwargs: Any) -> Location:
|
||||
|
|
|
|||
|
|
@ -168,44 +168,31 @@ class Vertex(Shape[TopoDS_Vertex]):
|
|||
"""extrude - invalid operation for Vertex"""
|
||||
raise NotImplementedError("Vertices can't be created by extrusion")
|
||||
|
||||
def intersect(
|
||||
self, *to_intersect: Shape | Vector | Location | Axis | Plane
|
||||
) -> ShapeList[Vertex] | None:
|
||||
"""Intersection of vertex and geometric objects or shapes.
|
||||
def _intersect(
|
||||
self,
|
||||
other: Shape,
|
||||
tolerance: float = 1e-6,
|
||||
include_touched: bool = False,
|
||||
) -> ShapeList | None:
|
||||
"""Single-object intersection for Vertex.
|
||||
|
||||
For a vertex (0D), intersection means the vertex lies on/in the other shape.
|
||||
|
||||
Args:
|
||||
to_intersect (sequence of [Shape | Vector | Location | Axis | Plane]):
|
||||
Objects(s) to intersect with
|
||||
|
||||
Returns:
|
||||
ShapeList[Vertex] | None: Vertex intersection in a ShapeList or None
|
||||
other: Shape to intersect with
|
||||
tolerance: tolerance for intersection detection
|
||||
include_touched: if True, include boundary contacts
|
||||
(only relevant when Solids are involved)
|
||||
"""
|
||||
common = Vector(self)
|
||||
result: Shape | ShapeList[Shape] | Vector | None
|
||||
for obj in to_intersect:
|
||||
# Treat as Vector, otherwise call intersection from Shape
|
||||
match obj:
|
||||
case Vertex():
|
||||
result = common.intersect(Vector(obj))
|
||||
case Vector() | Location() | Axis() | Plane():
|
||||
result = obj.intersect(common)
|
||||
case _ if issubclass(type(obj), Shape):
|
||||
result = obj.intersect(self)
|
||||
case _:
|
||||
raise ValueError(f"Unsupported type to_intersect:: {type(obj)}")
|
||||
|
||||
if isinstance(result, Vector) and result == common:
|
||||
pass
|
||||
elif (
|
||||
isinstance(result, list)
|
||||
and len(result) == 1
|
||||
and Vector(result[0]) == common
|
||||
):
|
||||
pass
|
||||
else:
|
||||
return None
|
||||
if isinstance(other, Vertex):
|
||||
# Vertex + Vertex: check distance
|
||||
if self.distance_to(other) <= tolerance:
|
||||
return ShapeList([self])
|
||||
return None
|
||||
|
||||
return ShapeList([self])
|
||||
# Delegate to higher-dimensional shape (including Compound)
|
||||
return other._intersect(self, tolerance, include_touched)
|
||||
|
||||
# ---- Instance Methods ----
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue