diff --git a/docs/tutorial_joints.py b/docs/tutorial_joints.py index e6d2fd3..ba1b149 100644 --- a/docs/tutorial_joints.py +++ b/docs/tutorial_joints.py @@ -159,7 +159,7 @@ class Hinge(Compound): for hole, hole_location in enumerate(hole_locations): CylindricalJoint( label="hole" + str(hole), - axis=hole_location.to_axis(), + axis=Axis(hole_location), linear_range=(-2 * CM, 2 * CM), angular_range=(0, 360), ) diff --git a/examples/joints.py b/examples/joints.py index f143af5..c51fb50 100644 --- a/examples/joints.py +++ b/examples/joints.py @@ -1,6 +1,7 @@ """ Experimental Joint development file """ + from build123d import * from ocp_vscode import * @@ -72,9 +73,9 @@ swing_arm_hinge_edge: Edge = ( .sort_by(Axis.X)[-2:] .sort_by(Axis.Y)[0] ) -swing_arm_hinge_axis = swing_arm_hinge_edge.to_axis() +swing_arm_hinge_axis = Axis(swing_arm_hinge_edge) base_corner_edge = base.edges().sort_by(Axis((0, 0, 0), (1, 1, 0)))[-1] -base_hinge_axis = base_corner_edge.to_axis() +base_hinge_axis = Axis(base_corner_edge) j3 = RevoluteJoint("hinge", base, axis=base_hinge_axis, angular_range=(0, 180)) j4 = RigidJoint("corner", hinge_arm, swing_arm_hinge_axis.location) base.joints["hinge"].connect_to(hinge_arm.joints["corner"], angle=90) @@ -86,7 +87,7 @@ slider_arm = JointBox(4, 1, 2, 0.2) s1 = LinearJoint( "slide", base, - axis=Edge.make_mid_way(*base_top_edges, 0.67).to_axis(), + axis=Axis(Edge.make_mid_way(*base_top_edges, 0.67)), linear_range=(0, base_top_edges[0].length), ) s2 = RigidJoint("slide", slider_arm, Location(Vector(0, 0, 0))) @@ -111,7 +112,7 @@ j5.connect_to(j6, position=-1, angle=90) j7 = LinearJoint( "slot", base, - axis=Edge.make_mid_way(*base_top_edges, 0.33).to_axis(), + axis=Axis(Edge.make_mid_way(*base_top_edges, 0.33)), linear_range=(0, base_top_edges[0].length), ) pin_arm = JointBox(2, 1, 2) diff --git a/examples/joints_algebra.py b/examples/joints_algebra.py index c0da394..1484329 100644 --- a/examples/joints_algebra.py +++ b/examples/joints_algebra.py @@ -62,9 +62,9 @@ swing_arm_hinge_edge = ( .sort_by(Axis.X)[-2:] .sort_by(Axis.Y)[0] ) -swing_arm_hinge_axis = swing_arm_hinge_edge.to_axis() +swing_arm_hinge_axis = Axis(swing_arm_hinge_edge) base_corner_edge = base.edges().sort_by(Axis((0, 0, 0), (1, 1, 0)))[-1] -base_hinge_axis = base_corner_edge.to_axis() +base_hinge_axis = Axis(base_corner_edge) j3 = RevoluteJoint("hinge", base, axis=base_hinge_axis, angular_range=(0, 180)) j4 = RigidJoint("corner", hinge_arm, swing_arm_hinge_axis.location) base.joints["hinge"].connect_to(hinge_arm.joints["corner"], angle=90) @@ -77,7 +77,7 @@ slider_arm = JointBox(4, 1, 2, 0.2) s1 = LinearJoint( "slide", base, - axis=Edge.make_mid_way(*base_top_edges, 0.67).to_axis(), + axis=Axis(Edge.make_mid_way(*base_top_edges, 0.67)), linear_range=(0, base_top_edges[0].length), ) s2 = RigidJoint("slide", slider_arm, Location(Vector(0, 0, 0))) @@ -102,7 +102,7 @@ j5.connect_to(j6, position=-1, angle=90) j7 = LinearJoint( "slot", base, - axis=Edge.make_mid_way(*base_top_edges, 0.33).to_axis(), + axis=Axis(Edge.make_mid_way(*base_top_edges, 0.33)), linear_range=(0, base_top_edges[0].length), ) pin_arm = JointBox(2, 1, 2) diff --git a/src/build123d/geometry.py b/src/build123d/geometry.py index bc18517..414c1aa 100644 --- a/src/build123d/geometry.py +++ b/src/build123d/geometry.py @@ -593,6 +593,7 @@ class Axis(metaclass=AxisMeta): origin (VectorLike): start point direction (VectorLike): direction edge (Edge): origin & direction defined by start of edge + location (Location): location to convert to axis Attributes: position (Vector): the global position of the axis origin @@ -603,75 +604,84 @@ class Axis(metaclass=AxisMeta): _dim = 1 @overload - def __init__(self, gp_ax1: gp_Ax1): # pragma: no cover + def __init__(self, gp_ax1: gp_Ax1): """Axis: point and direction""" @overload - def __init__(self, origin: VectorLike, direction: VectorLike): # pragma: no cover + def __init__(self, location: Location): + """Axis from location""" + + @overload + def __init__(self, origin: VectorLike, direction: VectorLike): """Axis: point and direction""" @overload - def __init__(self, edge: Edge): # pragma: no cover + def __init__(self, edge: Edge): """Axis: start of Edge""" - def __init__(self, *args, **kwargs): + def __init__( + self, *args, **kwargs + ): # pylint: disable=too-many-branches, too-many-locals gp_ax1 = kwargs.pop("gp_ax1", None) origin = kwargs.pop("origin", None) direction = kwargs.pop("direction", None) edge = kwargs.pop("edge", None) + location = kwargs.pop("location", None) # Handle unexpected kwargs if kwargs: raise ValueError(f"Unexpected argument(s): {', '.join(kwargs.keys())}") + # Handle positional arguments if len(args) == 1: - if isinstance(args[0], gp_Ax1): - gp_ax1 = args[0] - elif ( - hasattr(args[0], "wrapped") - and args[0].wrapped is not None - and isinstance(args[0].wrapped, TopoDS_Edge) - ): - edge = args[0] + arg = args[0] + if isinstance(arg, gp_Ax1): + gp_ax1 = arg + elif isinstance(arg, Location): + location = arg + elif hasattr(arg, "wrapped") and isinstance(arg.wrapped, TopoDS_Edge): + edge = arg + elif isinstance(arg, (Vector, tuple)): + origin = arg else: - origin = args[0] + raise ValueError(f"Unrecognized single argument: {arg}") elif len(args) == 2: origin, direction = args + # Handle edge-based construction if edge is not None: - if ( - hasattr(edge, "wrapped") - and edge.wrapped is not None - and isinstance(edge.wrapped, TopoDS_Edge) - ): - # Extract the start point and tangent - topods_edge: TopoDS_Edge = edge.wrapped # type: ignore[annotation-unchecked] - curve = BRep_Tool.Curve_s(topods_edge, float(), float()) - param_min, _ = BRep_Tool.Range_s(topods_edge) - origin_pnt = gp_Pnt() - tangent_vec = gp_Vec() - curve.D1(param_min, origin_pnt, tangent_vec) - origin = Vector(origin_pnt) - direction = Vector(gp_Dir(tangent_vec)) - else: - raise ValueError(f"Invalid argument {edge}") + if not (hasattr(edge, "wrapped") and isinstance(edge.wrapped, TopoDS_Edge)): + raise ValueError(f"Invalid edge argument: {edge}") - if gp_ax1 is not None: - if not isinstance(gp_ax1, gp_Ax1): - raise ValueError(f"Invalid Axis parameter {gp_ax1}") - self.wrapped: gp_Ax1 = gp_ax1 # type: ignore[annotation-unchecked] - else: + topods_edge: TopoDS_Edge = edge.wrapped # type: ignore[annotation-unchecked] + curve = BRep_Tool.Curve_s(topods_edge, float(), float()) + param_min, _ = BRep_Tool.Range_s(topods_edge) + origin_pnt = gp_Pnt() + tangent_vec = gp_Vec() + curve.D1(param_min, origin_pnt, tangent_vec) + origin = Vector(origin_pnt) + direction = Vector(gp_Dir(tangent_vec)) + + # Convert location to axis + if location is not None: + gp_ax1 = Axis.Z.located(location).wrapped + + # Construct self.wrapped from gp_ax1 or origin/direction + if gp_ax1 is None: try: origin_vector = Vector(origin) direction_vector = Vector(direction) - except TypeError as exc: + gp_ax1 = gp_Ax1( + origin_vector.to_pnt(), + gp_Dir(*tuple(direction_vector.normalized())), + ) + except Exception as exc: raise ValueError("Invalid Axis parameters") from exc + elif not isinstance(gp_ax1, gp_Ax1): + raise ValueError(f"Invalid Axis parameter: {gp_ax1}") - self.wrapped = gp_Ax1( - origin_vector.to_pnt(), - gp_Dir(*tuple(direction_vector.normalized())), - ) + self.wrapped: gp_Ax1 = gp_ax1 # type: ignore[annotation-unchecked] @property def position(self): @@ -1425,7 +1435,9 @@ class Location: """Location with translation t and rotation around direction by angle with respect to the original location.""" - def __init__(self, *args, **kwargs): + def __init__( + self, *args, **kwargs + ): # pylint: disable=too-many-branches, too-many-locals, too-many-statements position = kwargs.pop("position", None) orientation = kwargs.pop("orientation", None) ordering = kwargs.pop("ordering", None) @@ -1751,6 +1763,12 @@ class Location: def to_axis(self) -> Axis: """Convert the location into an Axis""" + warnings.warn( + "to_axis is deprecated and will be removed in a future version. " + "Use 'Axis(Location)' instead.", + DeprecationWarning, + stacklevel=2, + ) return Axis.Z.located(self) def to_tuple(self) -> tuple[tuple[float, float, float], tuple[float, float, float]]: diff --git a/src/build123d/topology/one_d.py b/src/build123d/topology/one_d.py index 51fae46..1c6faae 100644 --- a/src/build123d/topology/one_d.py +++ b/src/build123d/topology/one_d.py @@ -1563,7 +1563,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]): Returns: Edge: linear Edge between two Edges """ - flip = first.to_axis().is_opposite(second.to_axis()) + flip = Axis(first).is_opposite(Axis(second)) pnts = [ Edge.make_line( first.position_at(i), second.position_at(1 - i if flip else i) @@ -2184,6 +2184,12 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]): def to_axis(self) -> Axis: """Translate a linear Edge to an Axis""" + warnings.warn( + "to_axis is deprecated and will be removed in a future version. " + "Use 'Axis(Edge)' instead.", + DeprecationWarning, + stacklevel=2, + ) if self.geom_type != GeomType.LINE: raise ValueError( f"to_axis is only valid for linear Edges not {self.geom_type}" diff --git a/tests/test_direct_api/test_axis.py b/tests/test_direct_api/test_axis.py index bdc921e..2f76612 100644 --- a/tests/test_direct_api/test_axis.py +++ b/tests/test_direct_api/test_axis.py @@ -33,7 +33,7 @@ import unittest import numpy as np from OCP.gp import gp_Ax1, gp_Dir, gp_Pnt from build123d.geometry import Axis, Location, Plane, Vector -from build123d.topology import Edge +from build123d.topology import Edge, Vertex class AlwaysEqual: @@ -65,10 +65,18 @@ class TestAxis(unittest.TestCase): self.assertAlmostEqual(test_axis.position, (1, 2, 3), 5) self.assertAlmostEqual(test_axis.direction, (0, 0, 1), 5) + with self.assertRaises(ValueError): + Axis("one") with self.assertRaises(ValueError): Axis("one", "up") with self.assertRaises(ValueError): Axis(one="up") + with self.assertRaises(ValueError): + bad_edge = Edge() + bad_edge.wrapped = Vertex(0, 1, 2).wrapped + Axis(edge=bad_edge) + with self.assertRaises(ValueError): + Axis(gp_ax1=Edge.make_line((0, 0), (1, 0))) def test_axis_from_occt(self): occt_axis = gp_Ax1(gp_Pnt(1, 1, 1), gp_Dir(0, 1, 0)) @@ -100,6 +108,11 @@ class TestAxis(unittest.TestCase): self.assertAlmostEqual(y_axis.position, (0, 0, 1), 5) self.assertAlmostEqual(y_axis.direction, (0, 1, 0), 5) + def test_from_location(self): + axis = Axis(Location((1, 2, 3), (-90, 0, 0))) + self.assertAlmostEqual(axis.position, (1, 2, 3), 6) + self.assertAlmostEqual(axis.direction, (0, 1, 0), 6) + def test_axis_to_plane(self): x_plane = Axis.X.to_plane() self.assertTrue(isinstance(x_plane, Plane)) diff --git a/tests/test_direct_api/test_location.py b/tests/test_direct_api/test_location.py index 46369a7..1c6e666 100644 --- a/tests/test_direct_api/test_location.py +++ b/tests/test_direct_api/test_location.py @@ -270,10 +270,11 @@ class TestLocation(unittest.TestCase): self.assertAlmostEqual(loc1.position, loc3.position.to_tuple(), 6) self.assertAlmostEqual(loc1.orientation, loc3.orientation.to_tuple(), 6) - def test_to_axis(self): - axis = Location((1, 2, 3), (-90, 0, 0)).to_axis() - self.assertAlmostEqual(axis.position, (1, 2, 3), 6) - self.assertAlmostEqual(axis.direction, (0, 1, 0), 6) + # deprecated + # def test_to_axis(self): + # axis = Location((1, 2, 3), (-90, 0, 0)).to_axis() + # self.assertAlmostEqual(axis.position, (1, 2, 3), 6) + # self.assertAlmostEqual(axis.direction, (0, 1, 0), 6) def test_equal(self): loc = Location((1, 2, 3), (4, 5, 6)) diff --git a/tests/test_direct_api/test_projection.py b/tests/test_direct_api/test_projection.py index 5fbb7bd..8b0da03 100644 --- a/tests/test_direct_api/test_projection.py +++ b/tests/test_direct_api/test_projection.py @@ -94,10 +94,6 @@ class TestProjection(unittest.TestCase): self.assertAlmostEqual(projection[0].position_at(0), (0, 1, 0), 5) self.assertAlmostEqual(projection[0].arc_center, (0, 0, 0), 5) - def test_to_axis(self): - with self.assertRaises(ValueError): - Edge.make_circle(1, end_angle=30).to_axis() - if __name__ == "__main__": unittest.main()