Tests & Edge.intersections

This commit is contained in:
Roger Maitland 2023-02-15 15:52:18 -05:00
parent b5de144819
commit fcccedacb0
5 changed files with 235 additions and 139 deletions

View file

@ -14,20 +14,21 @@ __all__ = [
"FT", "FT",
# Enums # Enums
"Align", "Align",
"Select",
"Kind",
"Keep",
"Mode",
"Transition",
"FontStyle",
"Until",
"SortBy",
"GeomType",
"AngularDirection", "AngularDirection",
"PositionMode",
"FrameMethod",
"Direction",
"CenterOf", "CenterOf",
"Direction",
"FontStyle",
"FrameMethod",
"GeomType",
"Keep",
"Kind",
"LengthMode",
"Mode",
"PositionMode",
"Select",
"SortBy",
"Transition",
"Until",
# Classes # Classes
"Rotation", "Rotation",
"RotationLike", "RotationLike",

View file

@ -40,91 +40,42 @@ class Align(Enum):
return f"<{self.__class__.__name__}.{self.name}>" return f"<{self.__class__.__name__}.{self.name}>"
class Select(Enum): class AngularDirection(Enum):
"""Selector scope - all or last operation""" """Angular rotation direction"""
ALL = auto() CLOCKWISE = auto()
LAST = auto() COUNTER_CLOCKWISE = auto()
def __repr__(self): def __repr__(self):
return f"<{self.__class__.__name__}.{self.name}>" return f"<{self.__class__.__name__}.{self.name}>"
class Kind(Enum): class CenterOf(Enum):
"""Offset corner transition""" """Center Options"""
ARC = auto() GEOMETRY = auto()
INTERSECTION = auto() MASS = auto()
TANGENT = auto() BOUNDING_BOX = auto()
def __repr__(self): def __repr__(self):
return f"<{self.__class__.__name__}.{self.name}>" return f"<{self.__class__.__name__}.{self.name}>"
class Keep(Enum): class Direction(Enum):
"""Split options""" """Face direction"""
TOP = auto() ALONG_AXIS = auto()
BOTTOM = auto() OPPOSITE = auto()
BOTH = auto()
def __repr__(self): def __repr__(self):
return f"<{self.__class__.__name__}.{self.name}>" return f"<{self.__class__.__name__}.{self.name}>"
class Mode(Enum): class FrameMethod(Enum):
"""Combination Mode""" """Moving frame calculation method"""
ADD = auto() FRENET = auto()
SUBTRACT = auto() CORRECTED = auto()
INTERSECT = auto()
REPLACE = auto()
PRIVATE = auto()
def __repr__(self):
return f"<{self.__class__.__name__}.{self.name}>"
class Transition(Enum):
"""Sweep discontinuity handling option"""
RIGHT = auto()
ROUND = auto()
TRANSFORMED = auto()
def __repr__(self):
return f"<{self.__class__.__name__}.{self.name}>"
class FontStyle(Enum):
"""Text Font Styles"""
REGULAR = auto()
BOLD = auto()
ITALIC = auto()
def __repr__(self):
return f"<{self.__class__.__name__}.{self.name}>"
class Until(Enum):
"""Extrude limit"""
NEXT = auto()
LAST = auto()
def __repr__(self):
return f"<{self.__class__.__name__}.{self.name}>"
class SortBy(Enum):
"""Sorting criteria"""
LENGTH = auto()
RADIUS = auto()
AREA = auto()
VOLUME = auto()
DISTANCE = auto()
def __repr__(self): def __repr__(self):
return f"<{self.__class__.__name__}.{self.name}>" return f"<{self.__class__.__name__}.{self.name}>"
@ -154,11 +105,58 @@ class GeomType(Enum):
return f"<{self.__class__.__name__}.{self.name}>" return f"<{self.__class__.__name__}.{self.name}>"
class AngularDirection(Enum): class Keep(Enum):
"""Angular rotation direction""" """Split options"""
CLOCKWISE = auto() TOP = auto()
COUNTER_CLOCKWISE = auto() BOTTOM = auto()
BOTH = auto()
def __repr__(self):
return f"<{self.__class__.__name__}.{self.name}>"
class Kind(Enum):
"""Offset corner transition"""
ARC = auto()
INTERSECTION = auto()
TANGENT = auto()
def __repr__(self):
return f"<{self.__class__.__name__}.{self.name}>"
class Mode(Enum):
"""Combination Mode"""
ADD = auto()
SUBTRACT = auto()
INTERSECT = auto()
REPLACE = auto()
PRIVATE = auto()
def __repr__(self):
return f"<{self.__class__.__name__}.{self.name}>"
class FontStyle(Enum):
"""Text Font Styles"""
REGULAR = auto()
BOLD = auto()
ITALIC = auto()
def __repr__(self):
return f"<{self.__class__.__name__}.{self.name}>"
class LengthMode(Enum):
"""Method of specifying length along PolarLine"""
DIAGONAL = auto()
HORIZONTAL = auto()
VERTICAL = auto()
def __repr__(self): def __repr__(self):
return f"<{self.__class__.__name__}.{self.name}>" return f"<{self.__class__.__name__}.{self.name}>"
@ -174,32 +172,45 @@ class PositionMode(Enum):
return f"<{self.__class__.__name__}.{self.name}>" return f"<{self.__class__.__name__}.{self.name}>"
class FrameMethod(Enum): class Select(Enum):
"""Moving frame calculation method""" """Selector scope - all or last operation"""
FRENET = auto() ALL = auto()
CORRECTED = auto() LAST = auto()
def __repr__(self): def __repr__(self):
return f"<{self.__class__.__name__}.{self.name}>" return f"<{self.__class__.__name__}.{self.name}>"
class Direction(Enum): class SortBy(Enum):
"""Face direction""" """Sorting criteria"""
ALONG_AXIS = auto() LENGTH = auto()
OPPOSITE = auto() RADIUS = auto()
AREA = auto()
VOLUME = auto()
DISTANCE = auto()
def __repr__(self): def __repr__(self):
return f"<{self.__class__.__name__}.{self.name}>" return f"<{self.__class__.__name__}.{self.name}>"
class CenterOf(Enum): class Transition(Enum):
"""Center Options""" """Sweep discontinuity handling option"""
GEOMETRY = auto() RIGHT = auto()
MASS = auto() ROUND = auto()
BOUNDING_BOX = auto() TRANSFORMED = auto()
def __repr__(self):
return f"<{self.__class__.__name__}.{self.name}>"
class Until(Enum):
"""Extrude limit"""
NEXT = auto()
LAST = auto()
def __repr__(self): def __repr__(self):
return f"<{self.__class__.__name__}.{self.name}>" return f"<{self.__class__.__name__}.{self.name}>"

View file

@ -29,7 +29,7 @@ import copy
import inspect import inspect
from math import sin, cos, radians, sqrt, copysign from math import sin, cos, radians, sqrt, copysign
from typing import Union, Iterable from typing import Union, Iterable
from build123d.build_enums import Select, Mode, AngularDirection from build123d.build_enums import AngularDirection, LengthMode, Mode, Select
from build123d.direct_api import ( from build123d.direct_api import (
Axis, Axis,
Edge, Edge,
@ -569,6 +569,7 @@ class PolarLine(Edge):
length (float): line length length (float): line length
angle (float, optional): angle from +v X axis. Defaults to None. angle (float, optional): angle from +v X axis. Defaults to None.
direction (VectorLike, optional): vector direction. Defaults to None. direction (VectorLike, optional): vector direction. Defaults to None.
length_mode (LengthMode, optional):
mode (Mode, optional): combination mode. Defaults to Mode.ADD. mode (Mode, optional): combination mode. Defaults to Mode.ADD.
Raises: Raises:
@ -583,6 +584,7 @@ class PolarLine(Edge):
length: float, length: float,
angle: float = None, angle: float = None,
direction: VectorLike = None, direction: VectorLike = None,
length_mode: LengthMode = LengthMode.DIAGONAL,
mode: Mode = Mode.ADD, mode: Mode = Mode.ADD,
): ):
context: BuildLine = BuildLine._get_context(self) context: BuildLine = BuildLine._get_context(self)
@ -590,6 +592,11 @@ class PolarLine(Edge):
start = WorkplaneList.localize(start) start = WorkplaneList.localize(start)
if length_mode == LengthMode.HORIZONTAL:
length = length / cos(radians(angle))
elif length_mode == LengthMode.VERTICAL:
length = length / sin(radians(angle))
if angle is not None: if angle is not None:
x_val = cos(radians(angle)) * length x_val = cos(radians(angle)) * length
y_val = sin(radians(angle)) * length y_val = sin(radians(angle)) * length

View file

@ -156,7 +156,7 @@ from OCP.Geom import (
Geom_Plane, Geom_Plane,
Geom_Surface, Geom_Surface,
) )
from OCP.Geom2d import Geom2d_Line from OCP.Geom2d import Geom2d_Line, Geom2d_Curve
from OCP.GeomAbs import GeomAbs_C0, GeomAbs_Intersection, GeomAbs_JoinType from OCP.GeomAbs import GeomAbs_C0, GeomAbs_Intersection, GeomAbs_JoinType
from OCP.GeomAPI import ( from OCP.GeomAPI import (
GeomAPI_Interpolate, GeomAPI_Interpolate,
@ -164,6 +164,7 @@ from OCP.GeomAPI import (
GeomAPI_PointsToBSplineSurface, GeomAPI_PointsToBSplineSurface,
GeomAPI_ProjectPointOnSurf, GeomAPI_ProjectPointOnSurf,
) )
from OCP.Geom2dAPI import Geom2dAPI_InterCurveCurve
from OCP.GeomFill import ( from OCP.GeomFill import (
GeomFill_CorrectedFrenet, GeomFill_CorrectedFrenet,
GeomFill_Frenet, GeomFill_Frenet,
@ -203,7 +204,7 @@ from OCP.Precision import Precision
from OCP.Prs3d import Prs3d_IsoAspect from OCP.Prs3d import Prs3d_IsoAspect
from OCP.Quantity import Quantity_Color, Quantity_ColorRGBA from OCP.Quantity import Quantity_Color, Quantity_ColorRGBA
from OCP.RWStl import RWStl from OCP.RWStl import RWStl
from OCP.ShapeAnalysis import ShapeAnalysis_FreeBounds from OCP.ShapeAnalysis import ShapeAnalysis_Edge, ShapeAnalysis_FreeBounds
from OCP.ShapeFix import ShapeFix_Face, ShapeFix_Shape, ShapeFix_Solid from OCP.ShapeFix import ShapeFix_Face, ShapeFix_Shape, ShapeFix_Solid
from OCP.ShapeUpgrade import ShapeUpgrade_UnifySameDomain from OCP.ShapeUpgrade import ShapeUpgrade_UnifySameDomain
@ -4427,36 +4428,12 @@ class Compound(Shape, Mixin3D):
""" """
def __str__(self):
# Calculate the size of the tree labels
size_tuples = [(node.height, len(node.label)) for node in self.descendants]
size_tuples.append((self.height, len(self.label)))
size_tuples_per_level = [
list(filter(lambda ll: ll[0] == l, size_tuples))
for l in range(self.height + 1)
]
max_sizes_per_level = [
max(4, max([l[1] for l in level])) for level in size_tuples_per_level
]
level_sizes_per_level = [
l + i * 4 for i, l in enumerate(reversed(max_sizes_per_level))
]
tree_label_width = max(level_sizes_per_level) + 1
result = ""
for pre, _fill, node in RenderTree(self):
treestr = "%s%s" % (pre, node.label)
result += (
f"{treestr.ljust(tree_label_width)}{node.__class__.__name__.ljust(8)} "
f"at {id(self):#x}, Location{repr(self.location)}\n"
)
return result
def __repr__(self): def __repr__(self):
"""Return Compound info as string"""
if hasattr(self, "label") and hasattr(self, "children"): if hasattr(self, "label") and hasattr(self, "children"):
result = ( result = (
f"Compound at {id(self):#x}, label({self.label}), " f"Compound at {id(self):#x}, label({self.label}), "
f"#children({len(self.children)})" + f"#children({len(self.children)})"
) )
else: else:
result = f"Compound at {id(self):#x}" result = f"Compound at {id(self):#x}"
@ -4509,7 +4486,7 @@ class Compound(Shape, Mixin3D):
else: else:
raise NotImplementedError raise NotImplementedError
elif center_of == CenterOf.BOUNDING_BOX: elif center_of == CenterOf.BOUNDING_BOX:
middle = self.center(CenterOf.BOUNDING_BOX) middle = self.bounding_box().center()
return middle return middle
@classmethod @classmethod
@ -4519,18 +4496,17 @@ class Compound(Shape, Mixin3D):
shapes: Iterable[Shape]: shapes: Iterable[Shape]:
Returns: Returns:
""" """
return cls(cls._make_compound((s.wrapped for s in shapes))) return cls(cls._make_compound((s.wrapped for s in shapes)))
def remove(self, shape: Shape) -> Compound: def _remove(self, shape: Shape) -> Compound:
"""Remove the specified shape. """Return self with the specified shape removed.
Args: Args:
shape: Shape: shape: Shape:
""" """
comp_builder = TopoDS_Builder() comp_builder = TopoDS_Builder()
comp_builder.Remove(self.wrapped, shape.wrapped) comp_builder.Remove(self.wrapped, shape.wrapped)
return self
def _post_detach(self, parent: Compound): def _post_detach(self, parent: Compound):
"""Method call after detaching from `parent`.""" """Method call after detaching from `parent`."""
@ -4960,6 +4936,68 @@ class Edge(Shape, Mixin1D):
return return_value return return_value
def intersections(
self, plane: Plane, edge: Edge = None, tolerance: float = TOLERANCE
) -> list[Vector]:
"""intersections
Determine the points where a 2D edge crosses itself or another 2D edge
Args:
plane (Plane): plane containing edge(s)
edge (Edge): curve to compare with
tolerance (float, optional): defines the precision of computing the intersection points.
Defaults to TOLERANCE.
Returns:
list[Vector]: list of intersection points
"""
# This will be updated by Geom_Surface to the edge location but isn't otherwise used
edge_location = TopLoc_Location()
# Check if self is on the plane
if not all([plane.contains(self.position_at(i / 7)) for i in range(8)]):
raise ValueError("self must be a 2D edge on the given plane")
edge_surface: Geom_Surface = Face.make_plane(plane)._geom_adaptor()
self_parameters = [
BRep_Tool.Parameter_s(self.vertices()[i].wrapped, self.wrapped)
for i in [0, 1]
]
self_2d_curve: Geom2d_Curve = BRep_Tool.CurveOnPlane_s(
self.wrapped,
edge_surface,
edge_location,
*self_parameters,
)
if edge:
# Check if edge is on the plane
if not all([plane.contains(edge.position_at(i / 7)) for i in range(8)]):
raise ValueError("edge must be a 2D edge on the given plane")
edge_parameters = [
BRep_Tool.Parameter_s(edge.vertices()[i].wrapped, edge.wrapped)
for i in [0, 1]
]
edge_2d_curve: Geom2d_Curve = BRep_Tool.CurveOnPlane_s(
edge.wrapped,
edge_surface,
edge_location,
*edge_parameters,
)
intersector = Geom2dAPI_InterCurveCurve(
self_2d_curve, edge_2d_curve, tolerance
)
else:
intersector = Geom2dAPI_InterCurveCurve(self_2d_curve, tolerance)
crosses = [
Vector(intersector.Point(i + 1).X(), intersector.Point(i + 1).Y())
for i in range(intersector.NbPoints())
]
return crosses
@classmethod @classmethod
def make_bezier(cls, *cntl_pnts: VectorLike, weights: list[float] = None) -> Edge: def make_bezier(cls, *cntl_pnts: VectorLike, weights: list[float] = None) -> Edge:
"""make_bezier """make_bezier
@ -6132,6 +6170,7 @@ class Face(Shape):
return cls.cast(face) return cls.cast(face)
class Shell(Shape): class Shell(Shape):
"""the outer boundary of a surface""" """the outer boundary of a surface"""
@ -7814,7 +7853,7 @@ class Joint(ABC):
self.connected_to = other self.connected_to = other
@abstractmethod @abstractmethod
def relative_to(self, other:Joint, *args, **kwargs) -> Location: def relative_to(self, other: Joint, *args, **kwargs) -> Location:
"""Return relative location to another joint""" """Return relative location to another joint"""
return NotImplementedError return NotImplementedError
@ -7854,7 +7893,7 @@ class RigidJoint(Joint):
to_part.joints[label] = self to_part.joints[label] = self
super().__init__(label, to_part) super().__init__(label, to_part)
def relative_to(self, other : Joint, **kwargs) -> Location: def relative_to(self, other: Joint, **kwargs) -> Location:
"""relative_to """relative_to
Return the relative position to move the other. Return the relative position to move the other.
@ -7867,6 +7906,7 @@ class RigidJoint(Joint):
return self.relative_location * other.relative_location.inverse() return self.relative_location * other.relative_location.inverse()
class RevoluteJoint(Joint): class RevoluteJoint(Joint):
"""RevoluteJoint """RevoluteJoint
@ -7948,7 +7988,12 @@ class RevoluteJoint(Joint):
z_dir=(0, 0, 1), z_dir=(0, 0, 1),
) )
) )
return self.relative_axis.to_location() * rotation * other.relative_location.inverse() return (
self.relative_axis.to_location()
* rotation
* other.relative_location.inverse()
)
class LinearJoint(Joint): class LinearJoint(Joint):
"""LinearJoint """LinearJoint
@ -8187,7 +8232,10 @@ class CylindricalJoint(Joint):
) )
) )
return joint_relative_position * joint_rotation * other.relative_location.inverse() return (
joint_relative_position * joint_rotation * other.relative_location.inverse()
)
class BallJoint(Joint): class BallJoint(Joint):
"""BallJoint """BallJoint

View file

@ -130,6 +130,12 @@ class TestAssembly(unittest.TestCase):
self.assertEqual(first, topos[i]) self.assertEqual(first, topos[i])
self.assertEqual(third, locs[i]) self.assertEqual(third, locs[i])
def test_remove_child(self):
assembly = TestAssembly.create_test_assembly()
self.assertEqual(len(assembly.children), 2)
assembly.children = list(assembly.children)[1:]
self.assertEqual(len(assembly.children), 1)
class TestAxis(unittest.TestCase): class TestAxis(unittest.TestCase):
"""Test the Axis class""" """Test the Axis class"""
@ -526,12 +532,35 @@ class TestCompound(unittest.TestCase):
self.assertAlmostEqual(fuzzy.volume, 2, 5) self.assertAlmostEqual(fuzzy.volume, 2, 5)
def test_remove(self): def test_remove(self):
# Doesn't work box1 = Solid.make_box(1, 1, 1)
# box1 = Solid.make_box(1, 1, 1) box2 = Solid.make_box(1, 1, 1, Plane((2, 0, 0)))
# box2 = Solid.make_box(1, 1, 1, Plane((2, 0, 0))) combined = Compound.make_compound([box1, box2])
# combined = Compound.make_compound([box1, box2]) self.assertTrue(len(combined._remove(box2).solids()), 1)
# self.assertTrue(len(combined.remove(box2).solids()), 1)
pass def test_repr(self):
simple = Compound.make_compound([Solid.make_box(1, 1, 1)])
simple_str = repr(simple).split("0x")[0] + repr(simple).split(", ")[1]
self.assertEqual(simple_str, "Compound at label()")
assembly = Compound.make_compound([Solid.make_box(1, 1, 1)])
assembly.children = [Solid.make_box(1, 1, 1)]
assembly.label = "test"
assembly_str = repr(assembly).split("0x")[0] + repr(assembly).split(", l")[1]
self.assertEqual(assembly_str, "Compound at abel(test), #children(1)")
def test_center(self):
test_compound = Compound.make_compound(
[
Solid.make_box(2, 2, 2).locate(Location((-1, -1, -1))),
Solid.make_box(1, 1, 1).locate(Location((8.5, -0.5, -0.5))),
]
)
self.assertTupleAlmostEquals(test_compound.center(CenterOf.MASS), (1, 0, 0), 5)
self.assertTupleAlmostEquals(
test_compound.center(CenterOf.BOUNDING_BOX), (4.25, 0, 0), 5
)
with self.assertRaises(ValueError):
test_compound.center(CenterOf.GEOMETRY)
class TestEdge(unittest.TestCase): class TestEdge(unittest.TestCase):