diff --git a/src/build123d/direct_api.py b/src/build123d/direct_api.py index 863c4ec..584175e 100644 --- a/src/build123d/direct_api.py +++ b/src/build123d/direct_api.py @@ -148,6 +148,7 @@ from OCP.gp import ( gp_Dir2d, gp_Elips, gp_EulerSequence, + gp_Quaternion, gp_GTrsf, gp_Lin, gp_Pln, @@ -1021,42 +1022,58 @@ class Location: @overload def __init__(self) -> None: # pragma: no cover - # Empty location with not rotation or translation with respect to the original location. + "Empty location with not rotation or translation with respect to the original location." ... @overload - def __init__(self, translation: VectorLike) -> None: # pragma: no cover - # Location with translation with respect to the original location. + def __init__(self, location: "Location") -> None: # pragma: no cover + "Location with another given location." + ... + + @overload + def __init__( + self, translation: VectorLike, angle: float = 0 + ) -> None: # pragma: no cover + """Location with translation with respect to the original location. + If angle != 0 then the location includes a rotation around z-axis by angle""" + ... + + @overload + def __init__( + self, translation: VectorLike, rotation: RotationLike = None + ) -> None: # pragma: no cover + """Location with translation with respect to the original location. + If rotation is not None then the location includes the rotation (see also Rotation class)""" ... @overload def __init__(self, plane: Plane) -> None: # pragma: no cover - # Location corresponding to the location of the Plane. + "Location corresponding to the location of the Plane." ... @overload def __init__( self, plane: Plane, plane_offset: VectorLike ) -> None: # pragma: no cover - # Location corresponding to the angular location of the Plane with translation plane_offset. + "Location corresponding to the angular location of the Plane with translation plane_offset." ... @overload def __init__(self, top_loc: TopLoc_Location) -> None: # pragma: no cover - # Location wrapping the low-level TopLoc_Location object t + "Location wrapping the low-level TopLoc_Location object t" ... @overload def __init__(self, gp_trsf: gp_Trsf) -> None: # pragma: no cover - # Location wrapping the low-level gp_Trsf object t + "Location wrapping the low-level gp_Trsf object t" ... @overload def __init__( self, translation: VectorLike, axis: VectorLike, angle: float ) -> None: # pragma: no cover - # Location with translation t and rotation around axis by angle - # with respect to the original location.""" + """Location with translation t and rotation around axis by angle + with respect to the original location.""" ... def __init__(self, *args): @@ -1065,6 +1082,7 @@ class Location: if len(args) == 0: pass + elif len(args) == 1: translation = args[0] @@ -1078,6 +1096,9 @@ class Location: ) transform.SetTransformation(coordinate_system) transform.Invert() + elif isinstance(args[0], Location): + self.wrapped = translation.wrapped + return elif isinstance(translation, TopLoc_Location): self.wrapped = translation return @@ -1085,15 +1106,33 @@ class Location: transform = translation else: raise TypeError("Unexpected parameters") + elif len(args) == 2: - translation, origin = args - coordinate_system = gp_Ax3( - Vector(origin).to_pnt(), - translation.z_dir.to_dir(), - translation.x_dir.to_dir(), - ) - transform.SetTransformation(coordinate_system) - transform.Invert() + if isinstance(args[0], (Vector, tuple)): + if isinstance(args[1], (Vector, tuple)): + rotation = [radians(a) for a in args[1]] + q = gp_Quaternion() + q.SetEulerAngles(gp_EulerSequence.gp_Intrinsic_XYZ, *rotation) + transform.SetRotation(q) + elif isinstance(args[0], (Vector, tuple)) and isinstance( + args[1], (int, float) + ): + angle = radians(args[1]) + q = gp_Quaternion() + q.SetEulerAngles(gp_EulerSequence.gp_Intrinsic_XYZ, 0, 0, angle) + transform.SetRotation(q) + + # set translation part after setting rotation (if exists) + transform.SetTranslationPart(Vector(args[0]).wrapped) + else: + translation, origin = args + coordinate_system = gp_Ax3( + Vector(origin).to_pnt(), + translation.z_dir.to_dir(), + translation.x_dir.to_dir(), + ) + transform.SetTransformation(coordinate_system) + transform.Invert() else: translation, axis, angle = args transform.SetRotation( @@ -1123,9 +1162,11 @@ class Location: rot = transformation.GetRotation() rv_trans = (trans.X(), trans.Y(), trans.Z()) - rv_rot = rot.GetEulerAngles(gp_EulerSequence.gp_Extrinsic_XYZ) + rv_rot = [ + degrees(a) for a in rot.GetEulerAngles(gp_EulerSequence.gp_Intrinsic_XYZ) + ] - return rv_trans, rv_rot + return rv_trans, tuple(rv_rot) def __repr__(self): """To String @@ -1136,7 +1177,7 @@ class Location: Location as String """ position_str = ", ".join((f"{v:.2f}" for v in self.to_tuple()[0])) - orientation_str = ", ".join((f"{180*v/pi:.2f}" for v in self.to_tuple()[1])) + orientation_str = ", ".join((f"{v:.2f}" for v in self.to_tuple()[1])) return f"(p=({position_str}), o=({orientation_str}))" def __str__(self): @@ -1148,7 +1189,7 @@ class Location: Location as String """ position_str = ", ".join((f"{v:.2f}" for v in self.to_tuple()[0])) - orientation_str = ", ".join((f"{180*v/pi:.2f}" for v in self.to_tuple()[1])) + orientation_str = ", ".join((f"{v:.2f}" for v in self.to_tuple()[1])) return f"Location: (position=({position_str}), orientation=({orientation_str}))" @@ -1160,14 +1201,16 @@ class Rotation(Location): self.about_y = about_y self.about_z = about_z - # Compute rotation matrix. - rot_x = gp_Trsf() - rot_x.SetRotation(gp_Ax1(gp_Pnt(0, 0, 0), gp_Dir(1, 0, 0)), radians(about_x)) - rot_y = gp_Trsf() - rot_y.SetRotation(gp_Ax1(gp_Pnt(0, 0, 0), gp_Dir(0, 1, 0)), radians(about_y)) - rot_z = gp_Trsf() - rot_z.SetRotation(gp_Ax1(gp_Pnt(0, 0, 0), gp_Dir(0, 0, 1)), radians(about_z)) - super().__init__(rot_x * rot_y * rot_z) + q = gp_Quaternion() + q.SetEulerAngles( + gp_EulerSequence.gp_Intrinsic_XYZ, + radians(about_x), + radians(about_y), + radians(about_z), + ) + t = gp_Trsf() + t.SetRotationPart(q) + super().__init__(t) #:TypeVar("RotationLike"): Three tuple of angles about x, y, z or Rotation diff --git a/tests/direct_api_tests.py b/tests/direct_api_tests.py index 5aff041..682b34c 100644 --- a/tests/direct_api_tests.py +++ b/tests/direct_api_tests.py @@ -13,6 +13,8 @@ from OCP.gp import ( gp_Trsf, gp_Ax1, gp_Dir, + gp_Quaternion, + gp_EulerSequence, ) from OCP.BRepBuilderAPI import BRepBuilderAPI_MakeEdge @@ -414,19 +416,65 @@ class TestCadObjects(unittest.TestCase): with self.assertRaises(TypeError): Location("xy_plane") + # Test that the computed rotation matrix and intrinsic euler angles return the same + + about_x = uniform(-2 * math.pi, 2 * math.pi) + about_y = uniform(-2 * math.pi, 2 * math.pi) + about_z = uniform(-2 * math.pi, 2 * math.pi) + + rot_x = gp_Trsf() + rot_x.SetRotation(gp_Ax1(gp_Pnt(0, 0, 0), gp_Dir(1, 0, 0)), about_x) + rot_y = gp_Trsf() + rot_y.SetRotation(gp_Ax1(gp_Pnt(0, 0, 0), gp_Dir(0, 1, 0)), about_y) + rot_z = gp_Trsf() + rot_z.SetRotation(gp_Ax1(gp_Pnt(0, 0, 0), gp_Dir(0, 0, 1)), about_z) + loc1 = Location(rot_x * rot_y * rot_z) + + q = gp_Quaternion() + q.SetEulerAngles( + gp_EulerSequence.gp_Intrinsic_XYZ, + about_x, + about_y, + about_z, + ) + t = gp_Trsf() + t.SetRotationPart(q) + loc2 = Location(t) + + self.assertTupleAlmostEquals(loc1.to_tuple()[0], loc2.to_tuple()[0], 6) + self.assertTupleAlmostEquals(loc1.to_tuple()[1], loc2.to_tuple()[1], 6) + + loc1 = Location((1, 2), 34) + self.assertTupleAlmostEquals(loc1.to_tuple()[0], (1, 2, 0), 6) + self.assertTupleAlmostEquals(loc1.to_tuple()[1], (0, 0, 34), 6) + + rot_angles = (-115.00, 35.00, -135.00) + loc2 = Location((1, 2, 3), rot_angles) + self.assertTupleAlmostEquals(loc2.to_tuple()[0], (1, 2, 3), 6) + self.assertTupleAlmostEquals(loc2.to_tuple()[1], rot_angles, 6) + + loc3 = Location(loc2) + self.assertTupleAlmostEquals(loc3.to_tuple()[0], (1, 2, 3), 6) + self.assertTupleAlmostEquals(loc3.to_tuple()[1], rot_angles, 6) + def test_location_repr_and_str(self): self.assertEqual( - repr(Location()), "(p=(0.00, 0.00, 0.00), o=(0.00, -0.00, 0.00))" + repr(Location()), "(p=(0.00, 0.00, 0.00), o=(-0.00, 0.00, -0.00))" ) self.assertEqual( str(Location()), - "Location: (position=(0.00, 0.00, 0.00), orientation=(0.00, -0.00, 0.00))", + "Location: (position=(0.00, 0.00, 0.00), orientation=(-0.00, 0.00, -0.00))", + ) + loc = Location((1, 2, 3), (33, 45, 67)) + self.assertEqual( + str(loc), + "Location: (position=(1.00, 2.00, 3.00), orientation=(33.00, 45.00, 67.00))", ) def test_location_inverted(self): loc = Location(Plane.XZ) self.assertTupleAlmostEquals( - loc.inverse().orientation.to_tuple(), (-math.pi / 2, 0, 0), 6 + loc.inverse().orientation.to_tuple(), (-90, 0, 0), 6 ) def test_edge_wrapper_radius(self): @@ -1288,9 +1336,7 @@ class TestAxis(unittest.TestCase): x_location = Axis.X.to_location() self.assertTrue(isinstance(x_location, Location)) self.assertTupleAlmostEquals(x_location.position.to_tuple(), (0, 0, 0), 5) - self.assertTupleAlmostEquals( - x_location.orientation.to_tuple(), (-math.pi, -math.pi / 2, 0), 5 - ) + self.assertTupleAlmostEquals(x_location.orientation.to_tuple(), (0, 90, 180), 5) def test_axis_to_plane(self): x_plane = Axis.X.to_plane()