From 5dd39fad3a1d965d97166f9480e5113a2a914dcf Mon Sep 17 00:00:00 2001 From: Bernhard Date: Sat, 19 Nov 2022 14:34:24 +0100 Subject: [PATCH 1/8] switched to intrinsic_XYZ Euler angles --- src/build123d/direct_api.py | 21 ++++++++++++--------- tests/direct_api_tests.py | 36 +++++++++++++++++++++++++++++++++--- 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/src/build123d/direct_api.py b/src/build123d/direct_api.py index 863c4ec..13b7343 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, @@ -1123,7 +1124,7 @@ class Location: rot = transformation.GetRotation() rv_trans = (trans.X(), trans.Y(), trans.Z()) - rv_rot = rot.GetEulerAngles(gp_EulerSequence.gp_Extrinsic_XYZ) + rv_rot = rot.GetEulerAngles(gp_EulerSequence.gp_Intrinsic_XYZ) return rv_trans, rv_rot @@ -1160,14 +1161,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..2306b56 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,13 +416,41 @@ 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) + 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))", ) def test_location_inverted(self): @@ -1289,7 +1319,7 @@ class TestAxis(unittest.TestCase): 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 + x_location.orientation.to_tuple(), (0, math.pi / 2, math.pi), 5 ) def test_axis_to_plane(self): From 8e050fb1043c3be02aea3d14909a0b860029c914 Mon Sep 17 00:00:00 2001 From: Bernhard Date: Sat, 19 Nov 2022 17:06:13 +0100 Subject: [PATCH 2/8] added mode initializers --- src/build123d/direct_api.py | 62 +++++++++++++++++++++++++++++++------ 1 file changed, 53 insertions(+), 9 deletions(-) diff --git a/src/build123d/direct_api.py b/src/build123d/direct_api.py index 13b7343..9d5ebf7 100644 --- a/src/build123d/direct_api.py +++ b/src/build123d/direct_api.py @@ -1022,50 +1022,77 @@ 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, location: "Location") -> None: # pragma: no cover + "Location with another given location." ... @overload def __init__(self, translation: VectorLike) -> None: # pragma: no cover - # Location with translation with respect to the original location. + "Location with translation with respect to the original location." ... @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): + @overload + def __init__( + self, + translation: VectorLike, + axis: VectorLike = (0, 0, 1), + angle: float = 0, + ) -> None: # pragma: no cover + """Location with translation and rotation around axis by angle + with respect to the original location.""" + ... + + @overload + def __init__( + self, + translation: VectorLike, + rotation: RotationLike, + ) -> None: # pragma: no cover + """Location with translation and rotation + with respect to the original location.""" + ... + + def __init__(self, *args, **kwargs): transform = gp_Trsf() if len(args) == 0: pass + elif len(args) == 1: translation = args[0] @@ -1079,6 +1106,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 @@ -1086,6 +1116,20 @@ class Location: transform = translation else: raise TypeError("Unexpected parameters") + + # translation part of transform is set. Now handle rotation part + + if kwargs.get("rotation") is not None: + rotation = [radians(a) for a in kwargs["rotation"]] + q = gp_Quaternion() + q.SetEulerAngles(gp_EulerSequence.gp_Intrinsic_XYZ, *rotation) + transform.SetRotation(q) + elif kwargs.get("angle") is not None: + angle = radians(kwargs["angle"]) + q = gp_Quaternion() + q.SetEulerAngles(gp_EulerSequence.gp_Intrinsic_XYZ, 0, 0, angle) + transform.SetRotation(q) + elif len(args) == 2: translation, origin = args coordinate_system = gp_Ax3( From fa19def9f579882d14c0649a098a8beed392f952 Mon Sep 17 00:00:00 2001 From: Bernhard Date: Sat, 19 Nov 2022 17:53:21 +0100 Subject: [PATCH 3/8] fix angle and rotation variant and add tests --- src/build123d/direct_api.py | 24 +++++++++++------------- tests/direct_api_tests.py | 13 +++++++++++++ 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/src/build123d/direct_api.py b/src/build123d/direct_api.py index 9d5ebf7..6691db8 100644 --- a/src/build123d/direct_api.py +++ b/src/build123d/direct_api.py @@ -1097,6 +1097,17 @@ class Location: translation = args[0] if isinstance(translation, (Vector, tuple)): + if kwargs.get("rotation") is not None: + rotation = [radians(a) for a in kwargs["rotation"]] + q = gp_Quaternion() + q.SetEulerAngles(gp_EulerSequence.gp_Intrinsic_XYZ, *rotation) + transform.SetRotation(q) + elif kwargs.get("angle") is not None: + angle = radians(kwargs["angle"]) + 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(translation).wrapped) elif isinstance(translation, Plane): coordinate_system = gp_Ax3( @@ -1117,19 +1128,6 @@ class Location: else: raise TypeError("Unexpected parameters") - # translation part of transform is set. Now handle rotation part - - if kwargs.get("rotation") is not None: - rotation = [radians(a) for a in kwargs["rotation"]] - q = gp_Quaternion() - q.SetEulerAngles(gp_EulerSequence.gp_Intrinsic_XYZ, *rotation) - transform.SetRotation(q) - elif kwargs.get("angle") is not None: - angle = radians(kwargs["angle"]) - q = gp_Quaternion() - q.SetEulerAngles(gp_EulerSequence.gp_Intrinsic_XYZ, 0, 0, angle) - transform.SetRotation(q) - elif len(args) == 2: translation, origin = args coordinate_system = gp_Ax3( diff --git a/tests/direct_api_tests.py b/tests/direct_api_tests.py index 2306b56..5dff0a1 100644 --- a/tests/direct_api_tests.py +++ b/tests/direct_api_tests.py @@ -444,6 +444,19 @@ class TestCadObjects(unittest.TestCase): 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), angle=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), rotation=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))" From 95ebcfeee16375657dac8b9469788aee4b7acc4a Mon Sep 17 00:00:00 2001 From: Bernhard Date: Sat, 19 Nov 2022 17:54:17 +0100 Subject: [PATCH 4/8] to_tuple to return degrees --- src/build123d/direct_api.py | 6 ++++-- tests/direct_api_tests.py | 6 ++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/build123d/direct_api.py b/src/build123d/direct_api.py index 6691db8..056f25d 100644 --- a/src/build123d/direct_api.py +++ b/src/build123d/direct_api.py @@ -1166,9 +1166,11 @@ class Location: rot = transformation.GetRotation() rv_trans = (trans.X(), trans.Y(), trans.Z()) - rv_rot = rot.GetEulerAngles(gp_EulerSequence.gp_Intrinsic_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 diff --git a/tests/direct_api_tests.py b/tests/direct_api_tests.py index 5dff0a1..6198ab7 100644 --- a/tests/direct_api_tests.py +++ b/tests/direct_api_tests.py @@ -469,7 +469,7 @@ class TestCadObjects(unittest.TestCase): 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): @@ -1331,9 +1331,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(), (0, math.pi / 2, math.pi), 5 - ) + self.assertTupleAlmostEquals(x_location.orientation.to_tuple(), (0, 90, 180), 5) def test_axis_to_plane(self): x_plane = Axis.X.to_plane() From cb9a0b36d8c3d26a4b5a1ffcf232a7d13b4cadb5 Mon Sep 17 00:00:00 2001 From: Bernhard Date: Sat, 19 Nov 2022 18:01:15 +0100 Subject: [PATCH 5/8] removed unused parameter and changed doc string --- src/build123d/direct_api.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/build123d/direct_api.py b/src/build123d/direct_api.py index 056f25d..38161ea 100644 --- a/src/build123d/direct_api.py +++ b/src/build123d/direct_api.py @@ -1069,11 +1069,9 @@ class Location: def __init__( self, translation: VectorLike, - axis: VectorLike = (0, 0, 1), angle: float = 0, ) -> None: # pragma: no cover - """Location with translation and rotation around axis by angle - with respect to the original location.""" + """Location (usually 2-dim) with an angle to rotate about z-axis""" ... @overload From bc28a2e4512f133bff602580c8c7185ffa449735 Mon Sep 17 00:00:00 2001 From: Bernhard Date: Sat, 19 Nov 2022 18:19:40 +0100 Subject: [PATCH 6/8] removed keywords and made it completely position args only --- src/build123d/direct_api.py | 80 ++++++++++++++++++------------------- 1 file changed, 39 insertions(+), 41 deletions(-) diff --git a/src/build123d/direct_api.py b/src/build123d/direct_api.py index 38161ea..d0ab63a 100644 --- a/src/build123d/direct_api.py +++ b/src/build123d/direct_api.py @@ -1031,8 +1031,19 @@ class Location: ... @overload - def __init__(self, translation: VectorLike) -> None: # pragma: no cover - "Location with translation with respect to the original location." + 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 @@ -1065,26 +1076,7 @@ class Location: with respect to the original location.""" ... - @overload - def __init__( - self, - translation: VectorLike, - angle: float = 0, - ) -> None: # pragma: no cover - """Location (usually 2-dim) with an angle to rotate about z-axis""" - ... - - @overload - def __init__( - self, - translation: VectorLike, - rotation: RotationLike, - ) -> None: # pragma: no cover - """Location with translation and rotation - with respect to the original location.""" - ... - - def __init__(self, *args, **kwargs): + def __init__(self, *args): transform = gp_Trsf() @@ -1095,17 +1087,6 @@ class Location: translation = args[0] if isinstance(translation, (Vector, tuple)): - if kwargs.get("rotation") is not None: - rotation = [radians(a) for a in kwargs["rotation"]] - q = gp_Quaternion() - q.SetEulerAngles(gp_EulerSequence.gp_Intrinsic_XYZ, *rotation) - transform.SetRotation(q) - elif kwargs.get("angle") is not None: - angle = radians(kwargs["angle"]) - 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(translation).wrapped) elif isinstance(translation, Plane): coordinate_system = gp_Ax3( @@ -1127,14 +1108,31 @@ class Location: 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( From 3884727cd0871af7e5c0d0b64a9fdd6d25a46e12 Mon Sep 17 00:00:00 2001 From: Bernhard Date: Sat, 19 Nov 2022 18:23:10 +0100 Subject: [PATCH 7/8] remove keyword args from tests --- tests/direct_api_tests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/direct_api_tests.py b/tests/direct_api_tests.py index 6198ab7..0e6fbd4 100644 --- a/tests/direct_api_tests.py +++ b/tests/direct_api_tests.py @@ -444,12 +444,12 @@ class TestCadObjects(unittest.TestCase): 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), angle=34) + 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), rotation=rot_angles) + 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) From 74e7d5a3c6e3182b33d1d0675b1a6288b1ce1c99 Mon Sep 17 00:00:00 2001 From: Bernhard Date: Sat, 19 Nov 2022 18:31:35 +0100 Subject: [PATCH 8/8] fixed repr and str to not apply rad to deg again --- src/build123d/direct_api.py | 4 ++-- tests/direct_api_tests.py | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/build123d/direct_api.py b/src/build123d/direct_api.py index d0ab63a..584175e 100644 --- a/src/build123d/direct_api.py +++ b/src/build123d/direct_api.py @@ -1177,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): @@ -1189,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}))" diff --git a/tests/direct_api_tests.py b/tests/direct_api_tests.py index 0e6fbd4..682b34c 100644 --- a/tests/direct_api_tests.py +++ b/tests/direct_api_tests.py @@ -465,6 +465,11 @@ class TestCadObjects(unittest.TestCase): str(Location()), "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)