mirror of
https://github.com/gumyr/build123d.git
synced 2025-12-06 02:30:55 -08:00
Merge pull request #1120 from jwagenet/intersections-2d
Intersect Everything: 2D, 3D, Composite Shapes
This commit is contained in:
commit
df17ae8698
14 changed files with 708 additions and 149 deletions
|
|
@ -68,8 +68,7 @@ with BuildPart() as ex26:
|
||||||
with BuildSketch() as ex26_sk:
|
with BuildSketch() as ex26_sk:
|
||||||
with Locations((0, rev)):
|
with Locations((0, rev)):
|
||||||
Circle(rad)
|
Circle(rad)
|
||||||
revolve(axis=Axis.X, revolution_arc=90)
|
revolve(axis=Axis.X, revolution_arc=180)
|
||||||
mirror(about=Plane.XZ)
|
|
||||||
with BuildSketch() as ex26_sk2:
|
with BuildSketch() as ex26_sk2:
|
||||||
Rectangle(rad, rev)
|
Rectangle(rad, rev)
|
||||||
ex26_target = ex26.part
|
ex26_target = ex26.part
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,8 @@ rad, rev = 3, 25
|
||||||
|
|
||||||
# Extrude last
|
# Extrude last
|
||||||
circle = Pos(0, rev) * Circle(rad)
|
circle = Pos(0, rev) * Circle(rad)
|
||||||
ex26_target = revolve(circle, Axis.X, revolution_arc=90)
|
ex26_target = revolve(circle, Axis.X, revolution_arc=180)
|
||||||
ex26_target = ex26_target + mirror(ex26_target, Plane.XZ)
|
ex26_target = ex26_target
|
||||||
|
|
||||||
rect = Rectangle(rad, rev)
|
rect = Rectangle(rad, rev)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -466,7 +466,7 @@ class Builder(ABC, Generic[ShapeT]):
|
||||||
elif mode == Mode.INTERSECT:
|
elif mode == Mode.INTERSECT:
|
||||||
if self._obj is None:
|
if self._obj is None:
|
||||||
raise RuntimeError("Nothing to intersect with")
|
raise RuntimeError("Nothing to intersect with")
|
||||||
combined = self._obj.intersect(*typed[self._shape])
|
combined = self._obj.intersect(Compound(typed[self._shape]))
|
||||||
elif mode == Mode.REPLACE:
|
elif mode == Mode.REPLACE:
|
||||||
combined = self._sub_class(list(typed[self._shape]))
|
combined = self._sub_class(list(typed[self._shape]))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -453,7 +453,7 @@ class DimensionLine(BaseSketchObject):
|
||||||
if self_intersection is None:
|
if self_intersection is None:
|
||||||
self_intersection_area = 0.0
|
self_intersection_area = 0.0
|
||||||
else:
|
else:
|
||||||
self_intersection_area = self_intersection.area
|
self_intersection_area = sum(f.area for f in self_intersection.faces())
|
||||||
d_line += placed_label
|
d_line += placed_label
|
||||||
bbox_size = d_line.bounding_box().diagonal
|
bbox_size = d_line.bounding_box().diagonal
|
||||||
|
|
||||||
|
|
@ -467,7 +467,7 @@ class DimensionLine(BaseSketchObject):
|
||||||
if line_intersection is None:
|
if line_intersection is None:
|
||||||
common_area = 0.0
|
common_area = 0.0
|
||||||
else:
|
else:
|
||||||
common_area = line_intersection.area
|
common_area = sum(f.area for f in line_intersection.faces())
|
||||||
common_area += self_intersection_area
|
common_area += self_intersection_area
|
||||||
score = (d_line.area - 10 * common_area) / bbox_size
|
score = (d_line.area - 10 * common_area) / bbox_size
|
||||||
d_lines[d_line] = score
|
d_lines[d_line] = score
|
||||||
|
|
|
||||||
|
|
@ -58,13 +58,12 @@ import copy
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import warnings
|
import warnings
|
||||||
from itertools import combinations
|
|
||||||
from typing import Type, Union
|
|
||||||
|
|
||||||
from collections.abc import Iterable, Iterator, Sequence
|
from collections.abc import Iterable, Iterator, Sequence
|
||||||
|
from itertools import combinations
|
||||||
|
from typing_extensions import Self
|
||||||
|
|
||||||
import OCP.TopAbs as ta
|
import OCP.TopAbs as ta
|
||||||
from OCP.BRepAlgoAPI import BRepAlgoAPI_Fuse
|
from OCP.BRepAlgoAPI import BRepAlgoAPI_Common, BRepAlgoAPI_Fuse, BRepAlgoAPI_Section
|
||||||
from OCP.Font import (
|
from OCP.Font import (
|
||||||
Font_FA_Bold,
|
Font_FA_Bold,
|
||||||
Font_FA_BoldItalic,
|
Font_FA_BoldItalic,
|
||||||
|
|
@ -107,7 +106,6 @@ from build123d.geometry import (
|
||||||
VectorLike,
|
VectorLike,
|
||||||
logger,
|
logger,
|
||||||
)
|
)
|
||||||
from typing_extensions import Self
|
|
||||||
|
|
||||||
from .one_d import Edge, Wire, Mixin1D
|
from .one_d import Edge, Wire, Mixin1D
|
||||||
from .shape_core import (
|
from .shape_core import (
|
||||||
|
|
@ -651,11 +649,7 @@ class Compound(Mixin3D[TopoDS_Compound]):
|
||||||
children[child_index_pair[1]]
|
children[child_index_pair[1]]
|
||||||
)
|
)
|
||||||
if obj_intersection is not None:
|
if obj_intersection is not None:
|
||||||
common_volume = (
|
common_volume = sum(s.volume for s in obj_intersection.solids())
|
||||||
0.0
|
|
||||||
if isinstance(obj_intersection, list)
|
|
||||||
else obj_intersection.volume
|
|
||||||
)
|
|
||||||
if common_volume > tolerance:
|
if common_volume > tolerance:
|
||||||
return (
|
return (
|
||||||
True,
|
True,
|
||||||
|
|
@ -711,6 +705,148 @@ class Compound(Mixin3D[TopoDS_Compound]):
|
||||||
|
|
||||||
return results
|
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
|
||||||
|
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
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()
|
||||||
|
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)
|
||||||
|
|
||||||
def unwrap(self, fully: bool = True) -> Self | Shape:
|
def unwrap(self, fully: bool = True) -> Self | Shape:
|
||||||
"""Strip unnecessary Compound wrappers
|
"""Strip unnecessary Compound wrappers
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,12 +52,11 @@ license:
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
import numpy as np
|
|
||||||
import warnings
|
import warnings
|
||||||
from collections.abc import Iterable
|
from collections.abc import Iterable, Sequence
|
||||||
from itertools import combinations
|
from itertools import combinations
|
||||||
from math import atan2, ceil, copysign, cos, floor, inf, isclose, pi, radians
|
from math import atan2, ceil, copysign, cos, floor, inf, isclose, pi, radians
|
||||||
from typing import TYPE_CHECKING, Literal, TypeAlias, overload
|
from typing import TYPE_CHECKING, Literal, overload
|
||||||
from typing import cast as tcast
|
from typing import cast as tcast
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
@ -729,122 +728,103 @@ class Mixin1D(Shape[TOPODS]):
|
||||||
def to_vertex(objs: Iterable) -> ShapeList:
|
def to_vertex(objs: Iterable) -> ShapeList:
|
||||||
return ShapeList([Vertex(v) if isinstance(v, Vector) else v for v in objs])
|
return ShapeList([Vertex(v) if isinstance(v, Vector) else v for v in objs])
|
||||||
|
|
||||||
common_set: ShapeList[Vertex | Edge] = ShapeList(self.edges())
|
def bool_op(
|
||||||
target: ShapeList | Shape | Plane
|
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)])
|
||||||
|
)
|
||||||
|
|
||||||
|
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:
|
for other in to_intersect:
|
||||||
# Conform target type
|
# Conform target type
|
||||||
# Vertices need to be Vector for set()
|
|
||||||
match other:
|
match other:
|
||||||
case Axis():
|
case Axis():
|
||||||
target = ShapeList([Edge(other)])
|
# 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():
|
case Plane():
|
||||||
target = other
|
target = other
|
||||||
case Vector():
|
case Vector():
|
||||||
target = Vertex(other)
|
target = Vertex(other)
|
||||||
case Location():
|
case Location():
|
||||||
target = Vertex(other.position)
|
target = Vertex(other.position)
|
||||||
case Edge():
|
|
||||||
target = ShapeList([other])
|
|
||||||
case Wire():
|
|
||||||
target = ShapeList(other.edges())
|
|
||||||
case _ if issubclass(type(other), Shape):
|
case _ if issubclass(type(other), Shape):
|
||||||
target = other
|
target = other
|
||||||
case _:
|
case _:
|
||||||
raise ValueError(f"Unsupported type to_intersect: {type(other)}")
|
raise ValueError(f"Unsupported type to_intersect: {type(other)}")
|
||||||
|
|
||||||
# Find common matches
|
# Find common matches
|
||||||
common: list[Vector | Edge] = []
|
common: list[Vertex | Edge | Wire] = []
|
||||||
result: ShapeList | Shape | None
|
result: ShapeList | None
|
||||||
for obj in common_set:
|
for obj in common_set:
|
||||||
match (obj, target):
|
match (obj, target):
|
||||||
case obj, Shape() as target:
|
case (_, Plane()):
|
||||||
# Find Shape with Edge/Wire
|
target = Shape(BRepBuilderAPI_MakeFace(other.wrapped).Face())
|
||||||
if isinstance(target, Vertex):
|
operation = BRepAlgoAPI_Section()
|
||||||
result = Shape.intersect(obj, target)
|
result = bool_op((obj,), (target,), operation)
|
||||||
else:
|
operation = BRepAlgoAPI_Common()
|
||||||
|
result.extend(bool_op((obj,), (target,), operation))
|
||||||
|
|
||||||
|
case (_, Vertex() | Edge() | Wire()):
|
||||||
|
operation = BRepAlgoAPI_Section()
|
||||||
|
section = bool_op((obj,), (target,), operation)
|
||||||
|
result = section
|
||||||
|
if not section:
|
||||||
|
operation = BRepAlgoAPI_Common()
|
||||||
|
result.extend(bool_op((obj,), (target,), operation))
|
||||||
|
|
||||||
|
case _ if issubclass(type(target), Shape):
|
||||||
result = target.intersect(obj)
|
result = target.intersect(obj)
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
if not isinstance(result, list):
|
common.extend(result)
|
||||||
result = ShapeList([result])
|
|
||||||
common.extend(to_vector(result))
|
|
||||||
|
|
||||||
case Vertex() as obj, target:
|
|
||||||
if not isinstance(target, ShapeList):
|
|
||||||
target = ShapeList([target])
|
|
||||||
|
|
||||||
for tar in target:
|
|
||||||
if isinstance(tar, Edge):
|
|
||||||
result = Shape.intersect(obj, tar)
|
|
||||||
else:
|
|
||||||
result = obj.intersect(tar)
|
|
||||||
|
|
||||||
if result:
|
|
||||||
if not isinstance(result, list):
|
|
||||||
result = ShapeList([result])
|
|
||||||
common.extend(to_vector(result))
|
|
||||||
|
|
||||||
case Edge() as obj, ShapeList() as targets:
|
|
||||||
# Find any edge / edge intersection points
|
|
||||||
for tar in targets:
|
|
||||||
# Find crossing points
|
|
||||||
try:
|
|
||||||
intersection_points = obj.find_intersection_points(tar)
|
|
||||||
common.extend(intersection_points)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Find common end points
|
|
||||||
obj_end_points = set(Vector(v) for v in obj.vertices())
|
|
||||||
tar_end_points = set(Vector(v) for v in tar.vertices())
|
|
||||||
points = set.intersection(obj_end_points, tar_end_points)
|
|
||||||
common.extend(points)
|
|
||||||
|
|
||||||
# Find Edge/Edge overlaps
|
|
||||||
result = obj._bool_op(
|
|
||||||
(obj,), targets, BRepAlgoAPI_Common()
|
|
||||||
).edges()
|
|
||||||
common.extend(result if isinstance(result, list) else [result])
|
|
||||||
|
|
||||||
case Edge() as obj, Plane() as plane:
|
|
||||||
# Find any edge / plane intersection points & edges
|
|
||||||
# Find point intersections
|
|
||||||
if not obj:
|
|
||||||
continue
|
|
||||||
geom_line = BRep_Tool.Curve_s(
|
|
||||||
obj.wrapped, obj.param_at(0), obj.param_at(1)
|
|
||||||
)
|
|
||||||
geom_plane = Geom_Plane(plane.local_coord_system)
|
|
||||||
intersection_calculator = GeomAPI_IntCS(geom_line, geom_plane)
|
|
||||||
plane_intersection_points: list[Vector] = []
|
|
||||||
if intersection_calculator.IsDone():
|
|
||||||
plane_intersection_points = [
|
|
||||||
Vector(intersection_calculator.Point(i + 1))
|
|
||||||
for i in range(intersection_calculator.NbPoints())
|
|
||||||
]
|
|
||||||
common.extend(plane_intersection_points)
|
|
||||||
|
|
||||||
# Find edge intersections
|
|
||||||
if all(
|
|
||||||
plane.contains(v)
|
|
||||||
for v in obj.positions(i / 7 for i in range(8))
|
|
||||||
): # is a 2D edge
|
|
||||||
common.append(obj)
|
|
||||||
|
|
||||||
if common:
|
if common:
|
||||||
common_set = to_vertex(set(common))
|
common_set = ShapeList()
|
||||||
# Remove Vertex intersections coincident to Edge intersections
|
for shape in common:
|
||||||
vts = common_set.vertices()
|
if isinstance(shape, Wire):
|
||||||
eds = common_set.edges()
|
common_set.extend(shape.edges())
|
||||||
if vts and eds:
|
else:
|
||||||
filtered_vts = ShapeList(
|
common_set.append(shape)
|
||||||
[
|
common_set = to_vertex(set(to_vector(common_set)))
|
||||||
v
|
common_set = filter_shapes_by_order(common_set, [Vertex, Edge])
|
||||||
for v in vts
|
|
||||||
if all(v.distance_to(e) > TOLERANCE for e in eds)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
common_set = filtered_vts + eds
|
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1339,7 +1339,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
|
|
||||||
def intersect(
|
def intersect(
|
||||||
self, *to_intersect: Shape | Vector | Location | Axis | Plane
|
self, *to_intersect: Shape | Vector | Location | Axis | Plane
|
||||||
) -> None | Self | ShapeList[Self]:
|
) -> None | ShapeList[Self]:
|
||||||
"""Intersection of the arguments and this shape
|
"""Intersection of the arguments and this shape
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|
@ -1347,8 +1347,8 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
intersect with
|
intersect with
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Self | ShapeList[Self]: Resulting object may be of a different class than self
|
None | ShapeList[Self]: Resulting ShapeList may contain different class
|
||||||
or a ShapeList if multiple non-Compound object created
|
than self
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def _to_vertex(vec: Vector) -> Vertex:
|
def _to_vertex(vec: Vector) -> Vertex:
|
||||||
|
|
@ -1392,15 +1392,12 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
|
|
||||||
# Find the shape intersections
|
# Find the shape intersections
|
||||||
intersect_op = BRepAlgoAPI_Common()
|
intersect_op = BRepAlgoAPI_Common()
|
||||||
shape_intersections = self._bool_op((self,), objs, intersect_op)
|
intersections = self._bool_op((self,), objs, intersect_op)
|
||||||
if isinstance(shape_intersections, ShapeList) and not shape_intersections:
|
if isinstance(intersections, ShapeList):
|
||||||
|
return intersections or None
|
||||||
|
if isinstance(intersections, Shape) and not intersections.is_null:
|
||||||
|
return ShapeList([intersections])
|
||||||
return None
|
return None
|
||||||
if (
|
|
||||||
not isinstance(shape_intersections, ShapeList)
|
|
||||||
and shape_intersections.is_null
|
|
||||||
):
|
|
||||||
return None
|
|
||||||
return shape_intersections
|
|
||||||
|
|
||||||
def is_equal(self, other: Shape) -> bool:
|
def is_equal(self, other: Shape) -> bool:
|
||||||
"""Returns True if two shapes are equal, i.e. if they share the same
|
"""Returns True if two shapes are equal, i.e. if they share the same
|
||||||
|
|
@ -2138,7 +2135,11 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
args = list(args)
|
args = list(args)
|
||||||
tools = list(tools)
|
tools = list(tools)
|
||||||
# Find the highest order class from all the inputs Solid > Vertex
|
# Find the highest order class from all the inputs Solid > Vertex
|
||||||
order_dict = {type(s): type(s).order for s in [self] + args + tools}
|
order_dict = {
|
||||||
|
type(s): type(s).order
|
||||||
|
for s in [self] + args + tools
|
||||||
|
if hasattr(type(s), "order")
|
||||||
|
}
|
||||||
highest_order = sorted(order_dict.items(), key=lambda item: item[1])[-1]
|
highest_order = sorted(order_dict.items(), key=lambda item: item[1])[-1]
|
||||||
|
|
||||||
# The base of the operation
|
# The base of the operation
|
||||||
|
|
|
||||||
|
|
@ -56,13 +56,13 @@ from __future__ import annotations
|
||||||
|
|
||||||
import platform
|
import platform
|
||||||
import warnings
|
import warnings
|
||||||
|
from collections.abc import Iterable, Sequence
|
||||||
from math import radians, cos, tan
|
from math import radians, cos, tan
|
||||||
from typing import Union, TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
from typing_extensions import Self
|
||||||
from collections.abc import Iterable
|
|
||||||
|
|
||||||
import OCP.TopAbs as ta
|
import OCP.TopAbs as ta
|
||||||
from OCP.BRepAlgoAPI import BRepAlgoAPI_Cut
|
from OCP.BRepAlgoAPI import BRepAlgoAPI_Common, BRepAlgoAPI_Cut, BRepAlgoAPI_Section
|
||||||
from OCP.BRepBuilderAPI import BRepBuilderAPI_MakeSolid
|
from OCP.BRepBuilderAPI import BRepBuilderAPI_MakeSolid
|
||||||
from OCP.BRepClass3d import BRepClass3d_SolidClassifier
|
from OCP.BRepClass3d import BRepClass3d_SolidClassifier
|
||||||
from OCP.BRepFeat import BRepFeat_MakeDPrism
|
from OCP.BRepFeat import BRepFeat_MakeDPrism
|
||||||
|
|
@ -95,6 +95,7 @@ from OCP.gp import gp_Ax2, gp_Pnt
|
||||||
from build123d.build_enums import CenterOf, GeomType, Kind, Transition, Until
|
from build123d.build_enums import CenterOf, GeomType, Kind, Transition, Until
|
||||||
from build123d.geometry import (
|
from build123d.geometry import (
|
||||||
DEG2RAD,
|
DEG2RAD,
|
||||||
|
TOLERANCE,
|
||||||
Axis,
|
Axis,
|
||||||
BoundBox,
|
BoundBox,
|
||||||
Color,
|
Color,
|
||||||
|
|
@ -104,7 +105,6 @@ from build123d.geometry import (
|
||||||
Vector,
|
Vector,
|
||||||
VectorLike,
|
VectorLike,
|
||||||
)
|
)
|
||||||
from typing_extensions import Self
|
|
||||||
|
|
||||||
from .one_d import Edge, Wire, Mixin1D
|
from .one_d import Edge, Wire, Mixin1D
|
||||||
from .shape_core import TOPODS, Shape, ShapeList, Joint, downcast, shapetype
|
from .shape_core import TOPODS, Shape, ShapeList, Joint, downcast, shapetype
|
||||||
|
|
@ -420,6 +420,130 @@ class Mixin3D(Shape[TOPODS]):
|
||||||
|
|
||||||
return return_value
|
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
|
||||||
|
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
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 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)])
|
||||||
|
)
|
||||||
|
|
||||||
|
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 | 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()
|
||||||
|
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)
|
||||||
|
|
||||||
def is_inside(self, point: VectorLike, tolerance: float = 1.0e-6) -> bool:
|
def is_inside(self, point: VectorLike, tolerance: float = 1.0e-6) -> bool:
|
||||||
"""Returns whether or not the point is inside a solid or compound
|
"""Returns whether or not the point is inside a solid or compound
|
||||||
object within the specified tolerance.
|
object within the specified tolerance.
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ import OCP.TopAbs as ta
|
||||||
from OCP.BRep import BRep_Builder, BRep_Tool
|
from OCP.BRep import BRep_Builder, BRep_Tool
|
||||||
from OCP.BRepAdaptor import BRepAdaptor_Curve, BRepAdaptor_Surface
|
from OCP.BRepAdaptor import BRepAdaptor_Curve, BRepAdaptor_Surface
|
||||||
from OCP.BRepAlgo import BRepAlgo
|
from OCP.BRepAlgo import BRepAlgo
|
||||||
from OCP.BRepAlgoAPI import BRepAlgoAPI_Common
|
from OCP.BRepAlgoAPI import BRepAlgoAPI_Common, BRepAlgoAPI_Section
|
||||||
from OCP.BRepBuilderAPI import (
|
from OCP.BRepBuilderAPI import (
|
||||||
BRepBuilderAPI_MakeEdge,
|
BRepBuilderAPI_MakeEdge,
|
||||||
BRepBuilderAPI_MakeFace,
|
BRepBuilderAPI_MakeFace,
|
||||||
|
|
@ -279,6 +279,126 @@ class Mixin2D(ABC, Shape[TOPODS]):
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def intersect(
|
||||||
|
self, *to_intersect: Shape | Vector | Location | Axis | Plane
|
||||||
|
) -> None | ShapeList[Vertex | Edge | Face]:
|
||||||
|
"""Intersect Face with Shape or geometry object
|
||||||
|
|
||||||
|
Args:
|
||||||
|
to_intersect (Shape | Vector | Location | Axis | Plane): objects to intersect
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ShapeList[Vertex | Edge | Face] | None: ShapeList of vertices, edges, and/or
|
||||||
|
faces.
|
||||||
|
"""
|
||||||
|
|
||||||
|
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_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)])
|
||||||
|
)
|
||||||
|
|
||||||
|
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 | 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)}")
|
||||||
|
|
||||||
|
# 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()
|
||||||
|
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])
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return ShapeList(common_set)
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def location_at(self, *args: Any, **kwargs: Any) -> Location:
|
def location_at(self, *args: Any, **kwargs: Any) -> Location:
|
||||||
"""A location from a face or shell"""
|
"""A location from a face or shell"""
|
||||||
|
|
@ -677,15 +797,13 @@ class Face(Mixin2D[TopoDS_Face]):
|
||||||
).sort_by(Axis(cog, cross_dir))
|
).sort_by(Axis(cog, cross_dir))
|
||||||
|
|
||||||
bottom_area = sum(f.area for f in bottom_list)
|
bottom_area = sum(f.area for f in bottom_list)
|
||||||
intersect_area = 0.0
|
|
||||||
for flipped_face, bottom_face in zip(top_flipped_list, bottom_list):
|
for flipped_face, bottom_face in zip(top_flipped_list, bottom_list):
|
||||||
intersection = flipped_face.intersect(bottom_face)
|
intersection = flipped_face.intersect(bottom_face)
|
||||||
if intersection is None or isinstance(intersection, list):
|
if intersection is None:
|
||||||
intersect_area = -1.0
|
intersect_area = -1.0
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
assert isinstance(intersection, Face)
|
intersect_area = sum(f.area for f in intersection.faces())
|
||||||
intersect_area += intersection.area
|
|
||||||
|
|
||||||
if intersect_area == -1.0:
|
if intersect_area == -1.0:
|
||||||
continue
|
continue
|
||||||
|
|
|
||||||
|
|
@ -152,6 +152,7 @@ def test_shape_0d(obj, target, expected):
|
||||||
run_test(obj, target, expected)
|
run_test(obj, target, expected)
|
||||||
|
|
||||||
|
|
||||||
|
# 1d Shapes
|
||||||
ed1 = Line((0, 0), (5, 0)).edge()
|
ed1 = Line((0, 0), (5, 0)).edge()
|
||||||
ed2 = Line((0, -1), (5, 1)).edge()
|
ed2 = Line((0, -1), (5, 1)).edge()
|
||||||
ed3 = Line((0, 0, 5), (5, 0, 5)).edge()
|
ed3 = Line((0, 0, 5), (5, 0, 5)).edge()
|
||||||
|
|
@ -220,6 +221,195 @@ def test_shape_1d(obj, target, expected):
|
||||||
run_test(obj, target, expected)
|
run_test(obj, target, expected)
|
||||||
|
|
||||||
|
|
||||||
|
# 2d Shapes
|
||||||
|
fc1 = Rectangle(5, 5).face()
|
||||||
|
fc2 = Pos(Z=5) * Rectangle(5, 5).face()
|
||||||
|
fc3 = Rot(Y=90) * Rectangle(5, 5).face()
|
||||||
|
fc4 = Rot(Z=45) * Rectangle(5, 5).face()
|
||||||
|
fc5 = Pos(2.5, 2.5, 2.5) * Rot(0, 90) * Rectangle(5, 5).face()
|
||||||
|
fc6 = Pos(2.5, 2.5) * Rot(0, 90, 45, Extrinsic.XYZ) * Rectangle(5, 5).face()
|
||||||
|
fc7 = (Rot(90) * Cylinder(2, 4)).faces().filter_by(GeomType.CYLINDER)[0]
|
||||||
|
|
||||||
|
fc11 = Rectangle(4, 4).face()
|
||||||
|
fc22 = sweep(Rot(90) * CenterArc((0, 0), 2, 0, 180), Line((0, 2), (0, -2)))
|
||||||
|
sh1 = Shell([Pos(-4) * fc11, fc22])
|
||||||
|
sh2 = Pos(Z=1) * sh1
|
||||||
|
sh3 = Shell([Pos(-4) * fc11, fc22, Pos(2, 0, -2) * Rot(0, 90) * fc11])
|
||||||
|
sh4 = Shell([Pos(-4) * fc11, fc22, Pos(4) * fc11])
|
||||||
|
sh5 = Pos(Z=1) * Shell([Pos(-2, 0, -2) * Rot(0, -90) * fc11, fc22, Pos(2, 0, -2) * Rot(0, 90) * fc11])
|
||||||
|
|
||||||
|
shape_2d_matrix = [
|
||||||
|
Case(fc1, vl2, None, "non-coincident", None),
|
||||||
|
Case(fc1, vl1, [Vertex], "coincident", None),
|
||||||
|
|
||||||
|
Case(fc1, lc2, None, "non-coincident", None),
|
||||||
|
Case(fc1, lc1, [Vertex], "coincident", None),
|
||||||
|
|
||||||
|
Case(fc2, ax1, None, "parallel/skew", None),
|
||||||
|
Case(fc3, ax1, [Vertex], "intersecting", None),
|
||||||
|
Case(fc1, ax1, [Edge], "collinear", None),
|
||||||
|
|
||||||
|
Case(fc1, pl3, None, "parallel/skew", None),
|
||||||
|
Case(fc1, pl1, [Edge], "intersecting", None),
|
||||||
|
Case(fc1, pl2, [Face], "collinear", None),
|
||||||
|
Case(fc7, pl1, [Edge, Edge], "multi intersect", None),
|
||||||
|
|
||||||
|
Case(fc1, vt2, None, "non-coincident", None),
|
||||||
|
Case(fc1, vt1, [Vertex], "coincident", None),
|
||||||
|
|
||||||
|
Case(fc1, ed3, None, "parallel/skew", None),
|
||||||
|
Case(Pos(1) * fc3, ed1, [Vertex], "intersecting", None),
|
||||||
|
Case(fc1, ed1, [Edge], "collinear", None),
|
||||||
|
Case(Pos(1.1) * fc3, ed4, [Vertex, Vertex], "multi intersect", None),
|
||||||
|
|
||||||
|
Case(fc1, wi6, None, "parallel/skew", None),
|
||||||
|
Case(Pos(1) * fc3, wi4, [Vertex], "intersecting", None),
|
||||||
|
Case(fc1, wi1, [Edge, Edge], "2 collinear", None),
|
||||||
|
Case(Rot(90) * fc4, wi5, [Vertex, Vertex], "multi intersect", None),
|
||||||
|
Case(Rot(90) * fc4, wi2, [Vertex, Edge], "intersect + collinear", None),
|
||||||
|
|
||||||
|
Case(fc1, fc2, None, "parallel/skew", None),
|
||||||
|
Case(fc1, fc3, [Edge], "intersecting", None),
|
||||||
|
Case(fc1, fc4, [Face], "coplanar", None),
|
||||||
|
Case(fc1, fc5, [Edge], "intersecting edge", None),
|
||||||
|
Case(fc1, fc6, [Vertex], "intersecting vertex", None),
|
||||||
|
Case(fc1, fc7, [Edge, Edge], "multi-intersecting", None),
|
||||||
|
Case(fc7, Pos(Y=2) * fc7, [Face], "cyl intersecting", None),
|
||||||
|
|
||||||
|
Case(sh2, fc1, None, "parallel/skew", None),
|
||||||
|
Case(Pos(Z=1) * sh3, fc1, [Edge], "intersecting", None),
|
||||||
|
Case(sh1, fc1, [Face, Edge], "coplanar + intersecting", None),
|
||||||
|
Case(sh4, fc1, [Face, Face], "2 coplanar", None),
|
||||||
|
Case(sh5, fc1, [Edge, Edge], "2 intersecting", None),
|
||||||
|
|
||||||
|
Case(fc1, [fc4, Pos(2, 2) * fc1], [Face], "multi to_intersect, intersecting", None),
|
||||||
|
Case(fc1, [ed1, Pos(2.5, 2.5) * fc1], [Edge], "multi to_intersect, intersecting", None),
|
||||||
|
Case(fc7, [wi5, fc1], [Vertex], "multi to_intersect, intersecting", None),
|
||||||
|
]
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("obj, target, expected", make_params(shape_2d_matrix))
|
||||||
|
def test_shape_2d(obj, target, expected):
|
||||||
|
run_test(obj, target, expected)
|
||||||
|
|
||||||
|
# 3d Shapes
|
||||||
|
sl1 = Box(2, 2, 2).solid()
|
||||||
|
sl2 = Pos(Z=5) * Box(2, 2, 2).solid()
|
||||||
|
sl3 = Cylinder(2, 1).solid() - Cylinder(1.5, 1).solid()
|
||||||
|
|
||||||
|
wi7 = Wire([l1 := sl3.faces().sort_by(Axis.Z)[-1].edges()[0].trim(.3, .4),
|
||||||
|
l2 := l1.trim(2, 3),
|
||||||
|
RadiusArc(l1 @ 1, l2 @ 0, 1, short_sagitta=False)
|
||||||
|
])
|
||||||
|
|
||||||
|
shape_3d_matrix = [
|
||||||
|
Case(sl2, vl1, None, "non-coincident", None),
|
||||||
|
Case(Pos(2) * sl1, vl1, [Vertex], "contained", None),
|
||||||
|
Case(Pos(1, 1, -1) * sl1, vl1, [Vertex], "coincident", None),
|
||||||
|
|
||||||
|
Case(sl2, lc1, None, "non-coincident", None),
|
||||||
|
Case(Pos(2) * sl1, lc1, [Vertex], "contained", None),
|
||||||
|
Case(Pos(1, 1, -1) * sl1, lc1, [Vertex], "coincident", None),
|
||||||
|
|
||||||
|
Case(sl2, ax1, None, "non-coincident", None),
|
||||||
|
Case(sl1, ax1, [Edge], "intersecting", None),
|
||||||
|
Case(Pos(1, 1, 1) * sl1, ax2, [Edge], "coincident", None),
|
||||||
|
|
||||||
|
Case(sl1, pl3, None, "non-coincident", None),
|
||||||
|
Case(sl1, pl2, [Face], "intersecting", None),
|
||||||
|
|
||||||
|
Case(sl2, vt1, None, "non-coincident", None),
|
||||||
|
Case(Pos(2) * sl1, vt1, [Vertex], "contained", None),
|
||||||
|
Case(Pos(1, 1, -1) * sl1, vt1, [Vertex], "coincident", None),
|
||||||
|
|
||||||
|
Case(sl1, ed3, None, "non-coincident", None),
|
||||||
|
Case(sl1, ed1, [Edge], "intersecting", None),
|
||||||
|
Case(sl1, Pos(0, 1, 1) * ed1, [Edge], "edge collinear", "duplicate edges, BRepAlgoAPI_Common and _Section both return edge"),
|
||||||
|
Case(sl1, Pos(1, 1, 1) * ed1, [Vertex], "corner coincident", None),
|
||||||
|
Case(Pos(2.1, 1) * sl1, ed4, [Edge, Edge], "multi-intersect", None),
|
||||||
|
|
||||||
|
Case(Pos(2, .5, -1) * sl1, wi6, None, "non-coincident", None),
|
||||||
|
Case(Pos(2, .5, 1) * sl1, wi6, [Edge, Edge], "multi-intersecting", None),
|
||||||
|
Case(sl3, wi7, [Edge, Edge], "multi-coincident, is_equal check", None),
|
||||||
|
|
||||||
|
Case(sl2, fc1, None, "non-coincident", None),
|
||||||
|
Case(sl1, fc1, [Face], "intersecting", None),
|
||||||
|
Case(Pos(3.5, 0, 1) * sl1, fc1, [Edge], "edge collinear", None),
|
||||||
|
Case(Pos(3.5, 3.5) * sl1, fc1, [Vertex], "corner coincident", None),
|
||||||
|
Case(Pos(.9) * sl1, fc7, [Face, Face], "multi-intersecting", None),
|
||||||
|
|
||||||
|
Case(sl2, sh1, None, "non-coincident", None),
|
||||||
|
Case(Pos(-2) * sl1, sh1, [Face, Face], "multi-intersecting", None),
|
||||||
|
|
||||||
|
Case(sl1, sl2, None, "non-coincident", None),
|
||||||
|
Case(sl1, Pos(1, 1, 1) * sl1, [Solid], "intersecting", None),
|
||||||
|
Case(sl1, Pos(2, 2, 1) * sl1, [Edge], "edge collinear", None),
|
||||||
|
Case(sl1, Pos(2, 2, 2) * sl1, [Vertex], "corner coincident", None),
|
||||||
|
Case(sl1, Pos(.45) * sl3, [Solid, Solid], "multi-intersect", None),
|
||||||
|
|
||||||
|
Case(Pos(1.5, 1.5) * sl1, [sl3, Pos(.5, .5) * sl1], [Solid], "multi to_intersect, intersecting", None),
|
||||||
|
Case(Pos(1.5, 1.5) * sl1, [sl3, Pos(Z=.5) * fc1], [Face], "multi to_intersect, intersecting", None),
|
||||||
|
]
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("obj, target, expected", make_params(shape_3d_matrix))
|
||||||
|
def test_shape_3d(obj, target, expected):
|
||||||
|
run_test(obj, target, expected)
|
||||||
|
|
||||||
|
# Compound Shapes
|
||||||
|
cp1 = Compound(GridLocations(5, 0, 2, 1) * Vertex())
|
||||||
|
cp2 = Compound(GridLocations(5, 0, 2, 1) * Line((0, -1), (0, 1)))
|
||||||
|
cp3 = Compound(GridLocations(5, 0, 2, 1) * Rectangle(2, 2))
|
||||||
|
cp4 = Compound(GridLocations(5, 0, 2, 1) * Box(2, 2, 2))
|
||||||
|
|
||||||
|
cv1 = Curve() + [ed1, ed2, ed3]
|
||||||
|
sk1 = Sketch() + [fc1, fc2, fc3]
|
||||||
|
pt1 = Part() + [sl1, sl2, sl3]
|
||||||
|
|
||||||
|
|
||||||
|
shape_compound_matrix = [
|
||||||
|
Case(cp1, vl1, None, "non-coincident", None),
|
||||||
|
Case(Pos(-.5) * cp1, vl1, [Vertex], "intersecting", None),
|
||||||
|
|
||||||
|
Case(cp2, lc1, None, "non-coincident", None),
|
||||||
|
Case(Pos(-.5) * cp2, lc1, [Vertex], "intersecting", None),
|
||||||
|
|
||||||
|
Case(Pos(Z=1) * cp3, ax1, None, "non-coincident", None),
|
||||||
|
Case(cp3, ax1, [Edge, Edge], "intersecting", None),
|
||||||
|
|
||||||
|
Case(Pos(Z=3) * cp4, pl2, None, "non-coincident", None),
|
||||||
|
Case(cp4, pl2, [Face, Face], "intersecting", None),
|
||||||
|
|
||||||
|
Case(cp1, vt1, None, "non-coincident", None),
|
||||||
|
Case(Pos(-.5) * cp1, vt1, [Vertex], "intersecting", None),
|
||||||
|
|
||||||
|
Case(Pos(Z=1) * cp2, ed1, None, "non-coincident", None),
|
||||||
|
Case(cp2, ed1, [Vertex], "intersecting", None),
|
||||||
|
|
||||||
|
Case(Pos(Z=1) * cp3, fc1, None, "non-coincident", None),
|
||||||
|
Case(cp3, fc1, [Face, Face], "intersecting", None),
|
||||||
|
|
||||||
|
Case(Pos(Z=5) * cp4, sl1, None, "non-coincident", None),
|
||||||
|
Case(Pos(2) * cp4, sl1, [Solid], "intersecting", None),
|
||||||
|
|
||||||
|
Case(cp1, Pos(Z=1) * cp1, None, "non-coincident", None),
|
||||||
|
Case(cp1, cp2, [Vertex, Vertex], "intersecting", None),
|
||||||
|
Case(cp2, cp3, [Edge, Edge], "intersecting", None),
|
||||||
|
Case(cp3, cp4, [Face, Face], "intersecting", None),
|
||||||
|
|
||||||
|
Case(cp1, Compound(children=cp1.get_type(Vertex)), [Vertex, Vertex], "mixed child type", None),
|
||||||
|
Case(cp4, Compound(children=cp3.get_type(Face)), [Face, Face], "mixed child type", None),
|
||||||
|
|
||||||
|
Case(cp2, [cp3, cp4], [Edge, Edge], "multi to_intersect, intersecting", None),
|
||||||
|
|
||||||
|
Case(cv1, cp3, [Edge, Edge], "intersecting", "duplicate edges, BRepAlgoAPI_Common and _Section both return edge"),
|
||||||
|
Case(sk1, cp3, [Face, Face], "intersecting", None),
|
||||||
|
Case(pt1, cp3, [Face, Face], "intersecting", None),
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("obj, target, expected", make_params(shape_compound_matrix))
|
||||||
|
def test_shape_compound(obj, target, expected):
|
||||||
|
run_test(obj, target, expected)
|
||||||
|
|
||||||
# FreeCAD issue example
|
# FreeCAD issue example
|
||||||
c1 = CenterArc((0, 0), 10, 0, 360).edge()
|
c1 = CenterArc((0, 0), 10, 0, 360).edge()
|
||||||
c2 = CenterArc((19, 0), 10, 0, 360).edge()
|
c2 = CenterArc((19, 0), 10, 0, 360).edge()
|
||||||
|
|
@ -240,7 +430,7 @@ freecad_matrix = [
|
||||||
Case(vert, e1, [Vertex], "vertical, ellipse, tangent", None),
|
Case(vert, e1, [Vertex], "vertical, ellipse, tangent", None),
|
||||||
Case(horz, e1, [Vertex], "horizontal, ellipse, tangent", None),
|
Case(horz, e1, [Vertex], "horizontal, ellipse, tangent", None),
|
||||||
|
|
||||||
Case(c1, c2, [Vertex, Vertex], "circle, skew, intersect", "Should return 2 Vertices"),
|
Case(c1, c2, [Vertex, Vertex], "circle, skew, intersect", None),
|
||||||
Case(c1, horz, [Vertex], "circle, horiz, tangent", None),
|
Case(c1, horz, [Vertex], "circle, horiz, tangent", None),
|
||||||
Case(c2, horz, [Vertex], "circle, horiz, tangent", None),
|
Case(c2, horz, [Vertex], "circle, horiz, tangent", None),
|
||||||
Case(c1, vert, [Vertex], "circle, vert, tangent", None),
|
Case(c1, vert, [Vertex], "circle, vert, tangent", None),
|
||||||
|
|
@ -263,11 +453,11 @@ w1 = Wire.make_circle(0.5)
|
||||||
f1 = Face(Wire.make_circle(0.5))
|
f1 = Face(Wire.make_circle(0.5))
|
||||||
|
|
||||||
issues_matrix = [
|
issues_matrix = [
|
||||||
Case(t, t, [Face, Face], "issue #1015", "Returns Compound"),
|
Case(t, t, [Face, Face], "issue #1015", None),
|
||||||
Case(l, s, [Edge], "issue #945", "Edge.intersect only takes 1D"),
|
Case(l, s, [Edge], "issue #945", None),
|
||||||
Case(a, b, [Edge], "issue #918", "Returns empty Compound"),
|
Case(a, b, [Edge], "issue #918", None),
|
||||||
Case(e1, w1, [Vertex, Vertex], "issue #697"),
|
Case(e1, w1, [Vertex, Vertex], "issue #697", None),
|
||||||
Case(e1, f1, [Edge], "issue #697", "Edge.intersect only takes 1D"),
|
Case(e1, f1, [Edge], "issue #697", None),
|
||||||
]
|
]
|
||||||
|
|
||||||
@pytest.mark.parametrize("obj, target, expected", make_params(issues_matrix))
|
@pytest.mark.parametrize("obj, target, expected", make_params(issues_matrix))
|
||||||
|
|
@ -279,6 +469,9 @@ def test_issues(obj, target, expected):
|
||||||
exception_matrix = [
|
exception_matrix = [
|
||||||
Case(vt1, Color(), None, "Unsupported type", None),
|
Case(vt1, Color(), None, "Unsupported type", None),
|
||||||
Case(ed1, Color(), None, "Unsupported type", None),
|
Case(ed1, Color(), None, "Unsupported type", None),
|
||||||
|
Case(fc1, Color(), None, "Unsupported type", None),
|
||||||
|
Case(sl1, Color(), None, "Unsupported type", None),
|
||||||
|
Case(cp1, Color(), None, "Unsupported type", None),
|
||||||
]
|
]
|
||||||
|
|
||||||
@pytest.mark.skip
|
@pytest.mark.skip
|
||||||
|
|
|
||||||
|
|
@ -229,13 +229,15 @@ class TestOrientedBoundBox(unittest.TestCase):
|
||||||
obb = OrientedBoundBox(rect)
|
obb = OrientedBoundBox(rect)
|
||||||
corners = obb.corners
|
corners = obb.corners
|
||||||
poly = Polygon(*corners, align=None)
|
poly = Polygon(*corners, align=None)
|
||||||
self.assertAlmostEqual(rect.intersect(poly).area, rect.area, 5)
|
area = sum(f.area for f in rect.intersect(poly).faces())
|
||||||
|
self.assertAlmostEqual(area, rect.area, 5)
|
||||||
|
|
||||||
for face in Box(1, 2, 3).faces():
|
for face in Box(1, 2, 3).faces():
|
||||||
obb = OrientedBoundBox(face)
|
obb = OrientedBoundBox(face)
|
||||||
corners = obb.corners
|
corners = obb.corners
|
||||||
poly = Polygon(*corners, align=None)
|
poly = Polygon(*corners, align=None)
|
||||||
self.assertAlmostEqual(face.intersect(poly).area, face.area, 5)
|
area = sum(f.area for f in face.intersect(poly).faces())
|
||||||
|
self.assertAlmostEqual(area, face.area, 5)
|
||||||
|
|
||||||
def test_line_corners(self):
|
def test_line_corners(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -299,7 +299,8 @@ class TestShape(unittest.TestCase):
|
||||||
predicted_location = Location(offset) * Rotation(*rotation)
|
predicted_location = Location(offset) * Rotation(*rotation)
|
||||||
located_shape = Solid.make_box(1, 1, 1).locate(predicted_location)
|
located_shape = Solid.make_box(1, 1, 1).locate(predicted_location)
|
||||||
intersect = shape.intersect(located_shape)
|
intersect = shape.intersect(located_shape)
|
||||||
self.assertAlmostEqual(intersect.volume, 1, 5)
|
volume = sum(s.volume for s in intersect.solids())
|
||||||
|
self.assertAlmostEqual(volume, 1, 5)
|
||||||
|
|
||||||
def test_position_and_orientation(self):
|
def test_position_and_orientation(self):
|
||||||
box = Solid.make_box(1, 1, 1).locate(Location((1, 2, 3), (10, 20, 30)))
|
box = Solid.make_box(1, 1, 1).locate(Location((1, 2, 3), (10, 20, 30)))
|
||||||
|
|
@ -588,7 +589,7 @@ class TestShape(unittest.TestCase):
|
||||||
empty.distance_to_with_closest_points(Vector(1, 1, 1))
|
empty.distance_to_with_closest_points(Vector(1, 1, 1))
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
empty.distance_to(Vector(1, 1, 1))
|
empty.distance_to(Vector(1, 1, 1))
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(AttributeError):
|
||||||
box.intersect(empty_loc)
|
box.intersect(empty_loc)
|
||||||
self.assertEqual(empty._ocp_section(Vertex(1, 1, 1)), ([], []))
|
self.assertEqual(empty._ocp_section(Vertex(1, 1, 1)), ([], []))
|
||||||
self.assertEqual(empty.faces_intersected_by_axis(Axis.Z), ShapeList())
|
self.assertEqual(empty.faces_intersected_by_axis(Axis.Z), ShapeList())
|
||||||
|
|
|
||||||
|
|
@ -153,7 +153,9 @@ class TestSolid(unittest.TestCase):
|
||||||
self.assertAlmostEqual(twist.volume, 1, 5)
|
self.assertAlmostEqual(twist.volume, 1, 5)
|
||||||
top = twist.faces().sort_by(Axis.Z)[-1].rotate(Axis.Z, 45)
|
top = twist.faces().sort_by(Axis.Z)[-1].rotate(Axis.Z, 45)
|
||||||
bottom = twist.faces().sort_by(Axis.Z)[0]
|
bottom = twist.faces().sort_by(Axis.Z)[0]
|
||||||
self.assertAlmostEqual(top.translate((0, 0, -1)).intersect(bottom).area, 1, 5)
|
intersect = top.translate((0, 0, -1)).intersect(bottom)
|
||||||
|
area = sum(f.area for f in intersect.faces())
|
||||||
|
self.assertAlmostEqual(area, 1, 5)
|
||||||
# Wire
|
# Wire
|
||||||
base = Wire.make_rect(1, 1)
|
base = Wire.make_rect(1, 1)
|
||||||
twist = Solid.extrude_linear_with_rotation(
|
twist = Solid.extrude_linear_with_rotation(
|
||||||
|
|
@ -162,7 +164,9 @@ class TestSolid(unittest.TestCase):
|
||||||
self.assertAlmostEqual(twist.volume, 1, 5)
|
self.assertAlmostEqual(twist.volume, 1, 5)
|
||||||
top = twist.faces().sort_by(Axis.Z)[-1].rotate(Axis.Z, 45)
|
top = twist.faces().sort_by(Axis.Z)[-1].rotate(Axis.Z, 45)
|
||||||
bottom = twist.faces().sort_by(Axis.Z)[0]
|
bottom = twist.faces().sort_by(Axis.Z)[0]
|
||||||
self.assertAlmostEqual(top.translate((0, 0, -1)).intersect(bottom).area, 1, 5)
|
intersect = top.translate((0, 0, -1)).intersect(bottom)
|
||||||
|
area = sum(f.area for f in intersect.faces())
|
||||||
|
self.assertAlmostEqual(area, 1, 5)
|
||||||
|
|
||||||
def test_make_loft(self):
|
def test_make_loft(self):
|
||||||
loft = Solid.make_loft(
|
loft = Solid.make_loft(
|
||||||
|
|
|
||||||
|
|
@ -231,7 +231,8 @@ class DimensionLineTestCase(unittest.TestCase):
|
||||||
],
|
],
|
||||||
draft=metric,
|
draft=metric,
|
||||||
)
|
)
|
||||||
self.assertGreater(hole.intersect(d_line).area, 0)
|
area = sum(f.area for f in hole.intersect(d_line).faces())
|
||||||
|
self.assertGreater(area, 0)
|
||||||
|
|
||||||
def test_outside_arrows(self):
|
def test_outside_arrows(self):
|
||||||
d_line = DimensionLine([(0, 0, 0), (15, 0, 0)], draft=metric)
|
d_line = DimensionLine([(0, 0, 0), (15, 0, 0)], draft=metric)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue