mirror of
https://github.com/gumyr/build123d.git
synced 2026-02-05 06:50:44 -08:00
Tests & Edge.intersections
This commit is contained in:
parent
b5de144819
commit
fcccedacb0
5 changed files with 235 additions and 139 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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}>"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue