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",
# Enums
"Align",
"Select",
"Kind",
"Keep",
"Mode",
"Transition",
"FontStyle",
"Until",
"SortBy",
"GeomType",
"AngularDirection",
"PositionMode",
"FrameMethod",
"Direction",
"CenterOf",
"Direction",
"FontStyle",
"FrameMethod",
"GeomType",
"Keep",
"Kind",
"LengthMode",
"Mode",
"PositionMode",
"Select",
"SortBy",
"Transition",
"Until",
# Classes
"Rotation",
"RotationLike",

View file

@ -40,91 +40,42 @@ class Align(Enum):
return f"<{self.__class__.__name__}.{self.name}>"
class Select(Enum):
"""Selector scope - all or last operation"""
class AngularDirection(Enum):
"""Angular rotation direction"""
ALL = auto()
LAST = auto()
CLOCKWISE = auto()
COUNTER_CLOCKWISE = auto()
def __repr__(self):
return f"<{self.__class__.__name__}.{self.name}>"
class Kind(Enum):
"""Offset corner transition"""
class CenterOf(Enum):
"""Center Options"""
ARC = auto()
INTERSECTION = auto()
TANGENT = auto()
GEOMETRY = auto()
MASS = auto()
BOUNDING_BOX = auto()
def __repr__(self):
return f"<{self.__class__.__name__}.{self.name}>"
class Keep(Enum):
"""Split options"""
class Direction(Enum):
"""Face direction"""
TOP = auto()
BOTTOM = auto()
BOTH = auto()
ALONG_AXIS = auto()
OPPOSITE = auto()
def __repr__(self):
return f"<{self.__class__.__name__}.{self.name}>"
class Mode(Enum):
"""Combination Mode"""
class FrameMethod(Enum):
"""Moving frame calculation method"""
ADD = auto()
SUBTRACT = 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()
FRENET = auto()
CORRECTED = auto()
def __repr__(self):
return f"<{self.__class__.__name__}.{self.name}>"
@ -154,11 +105,58 @@ class GeomType(Enum):
return f"<{self.__class__.__name__}.{self.name}>"
class AngularDirection(Enum):
"""Angular rotation direction"""
class Keep(Enum):
"""Split options"""
CLOCKWISE = auto()
COUNTER_CLOCKWISE = auto()
TOP = 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):
return f"<{self.__class__.__name__}.{self.name}>"
@ -174,32 +172,45 @@ class PositionMode(Enum):
return f"<{self.__class__.__name__}.{self.name}>"
class FrameMethod(Enum):
"""Moving frame calculation method"""
class Select(Enum):
"""Selector scope - all or last operation"""
FRENET = auto()
CORRECTED = auto()
ALL = auto()
LAST = auto()
def __repr__(self):
return f"<{self.__class__.__name__}.{self.name}>"
class Direction(Enum):
"""Face direction"""
class SortBy(Enum):
"""Sorting criteria"""
ALONG_AXIS = auto()
OPPOSITE = auto()
LENGTH = auto()
RADIUS = auto()
AREA = auto()
VOLUME = auto()
DISTANCE = auto()
def __repr__(self):
return f"<{self.__class__.__name__}.{self.name}>"
class CenterOf(Enum):
"""Center Options"""
class Transition(Enum):
"""Sweep discontinuity handling option"""
GEOMETRY = auto()
MASS = auto()
BOUNDING_BOX = auto()
RIGHT = auto()
ROUND = 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):
return f"<{self.__class__.__name__}.{self.name}>"

View file

@ -29,7 +29,7 @@ import copy
import inspect
from math import sin, cos, radians, sqrt, copysign
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 (
Axis,
Edge,
@ -569,6 +569,7 @@ class PolarLine(Edge):
length (float): line length
angle (float, optional): angle from +v X axis. Defaults to None.
direction (VectorLike, optional): vector direction. Defaults to None.
length_mode (LengthMode, optional):
mode (Mode, optional): combination mode. Defaults to Mode.ADD.
Raises:
@ -583,6 +584,7 @@ class PolarLine(Edge):
length: float,
angle: float = None,
direction: VectorLike = None,
length_mode: LengthMode = LengthMode.DIAGONAL,
mode: Mode = Mode.ADD,
):
context: BuildLine = BuildLine._get_context(self)
@ -590,6 +592,11 @@ class PolarLine(Edge):
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:
x_val = cos(radians(angle)) * length
y_val = sin(radians(angle)) * length

View file

@ -156,7 +156,7 @@ from OCP.Geom import (
Geom_Plane,
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.GeomAPI import (
GeomAPI_Interpolate,
@ -164,6 +164,7 @@ from OCP.GeomAPI import (
GeomAPI_PointsToBSplineSurface,
GeomAPI_ProjectPointOnSurf,
)
from OCP.Geom2dAPI import Geom2dAPI_InterCurveCurve
from OCP.GeomFill import (
GeomFill_CorrectedFrenet,
GeomFill_Frenet,
@ -203,7 +204,7 @@ from OCP.Precision import Precision
from OCP.Prs3d import Prs3d_IsoAspect
from OCP.Quantity import Quantity_Color, Quantity_ColorRGBA
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.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):
"""Return Compound info as string"""
if hasattr(self, "label") and hasattr(self, "children"):
result = (
f"Compound at {id(self):#x}, label({self.label}), "
f"#children({len(self.children)})"
+ f"#children({len(self.children)})"
)
else:
result = f"Compound at {id(self):#x}"
@ -4509,7 +4486,7 @@ class Compound(Shape, Mixin3D):
else:
raise NotImplementedError
elif center_of == CenterOf.BOUNDING_BOX:
middle = self.center(CenterOf.BOUNDING_BOX)
middle = self.bounding_box().center()
return middle
@classmethod
@ -4519,18 +4496,17 @@ class Compound(Shape, Mixin3D):
shapes: Iterable[Shape]:
Returns:
"""
return cls(cls._make_compound((s.wrapped for s in shapes)))
def remove(self, shape: Shape) -> Compound:
"""Remove the specified shape.
def _remove(self, shape: Shape) -> Compound:
"""Return self with the specified shape removed.
Args:
shape: Shape:
"""
comp_builder = TopoDS_Builder()
comp_builder.Remove(self.wrapped, shape.wrapped)
return self
def _post_detach(self, parent: Compound):
"""Method call after detaching from `parent`."""
@ -4960,6 +4936,68 @@ class Edge(Shape, Mixin1D):
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
def make_bezier(cls, *cntl_pnts: VectorLike, weights: list[float] = None) -> Edge:
"""make_bezier
@ -6132,6 +6170,7 @@ class Face(Shape):
return cls.cast(face)
class Shell(Shape):
"""the outer boundary of a surface"""
@ -7814,7 +7853,7 @@ class Joint(ABC):
self.connected_to = other
@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 NotImplementedError
@ -7854,7 +7893,7 @@ class RigidJoint(Joint):
to_part.joints[label] = self
super().__init__(label, to_part)
def relative_to(self, other : Joint, **kwargs) -> Location:
def relative_to(self, other: Joint, **kwargs) -> Location:
"""relative_to
Return the relative position to move the other.
@ -7867,6 +7906,7 @@ class RigidJoint(Joint):
return self.relative_location * other.relative_location.inverse()
class RevoluteJoint(Joint):
"""RevoluteJoint
@ -7948,7 +7988,12 @@ class RevoluteJoint(Joint):
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):
"""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):
"""BallJoint

View file

@ -130,6 +130,12 @@ class TestAssembly(unittest.TestCase):
self.assertEqual(first, topos[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):
"""Test the Axis class"""
@ -526,12 +532,35 @@ class TestCompound(unittest.TestCase):
self.assertAlmostEqual(fuzzy.volume, 2, 5)
def test_remove(self):
# Doesn't work
# box1 = Solid.make_box(1, 1, 1)
# box2 = Solid.make_box(1, 1, 1, Plane((2, 0, 0)))
# combined = Compound.make_compound([box1, box2])
# self.assertTrue(len(combined.remove(box2).solids()), 1)
pass
box1 = Solid.make_box(1, 1, 1)
box2 = Solid.make_box(1, 1, 1, Plane((2, 0, 0)))
combined = Compound.make_compound([box1, box2])
self.assertTrue(len(combined._remove(box2).solids()), 1)
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):