From 992de4074bf3de34bbc225e1263b636bf0cfad8f Mon Sep 17 00:00:00 2001 From: Roger Maitland Date: Mon, 2 Mar 2026 13:37:25 -0500 Subject: [PATCH] Fix Issue #586 translate/rotate --- docs/moving_objects.rst | 8 +++---- src/build123d/topology/shape_core.py | 29 +++++++++++++++++++----- tests/test_direct_api/test_shape.py | 34 ++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 10 deletions(-) diff --git a/docs/moving_objects.rst b/docs/moving_objects.rst index b36dde4..580b9e1 100644 --- a/docs/moving_objects.rst +++ b/docs/moving_objects.rst @@ -113,15 +113,15 @@ Transformation a.k.a. Translation and Rotation ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. note:: - These methods don't work in the same way as the previous methods in that they don't just change - the object's internal :class:`~geometry.Location` but transform the base object itself which - is quite slow and potentially problematic. + These methods have an optional ``transform`` parameter which allows the user to transform the base + object itself which is quite slow and potentially problematic as opposed to just changing the + object's internal :class:`~geometry.Location`. - **Translation:** Move a shape relative to its current position. .. code-block:: build123d - relocated_shape = shape.translate(x, y, z) + relocated_shape = shape.translate((x, y, z)) - **Rotation:** Rotate a shape around a specified axis by a given angle. diff --git a/src/build123d/topology/shape_core.py b/src/build123d/topology/shape_core.py index 1a425ea..11a7216 100644 --- a/src/build123d/topology/shape_core.py +++ b/src/build123d/topology/shape_core.py @@ -1711,7 +1711,7 @@ class Shape(NodeMixin, Generic[TOPODS]): self.wrapped = tcast(TOPODS, downcast(builder.Shape())) self.wrapped.Location(loc.wrapped) - def rotate(self, axis: Axis, angle: float) -> Self: + def rotate(self, axis: Axis, angle: float, transform: bool = False) -> Self: """rotate a copy Rotates a shape around an axis. @@ -1719,14 +1719,23 @@ class Shape(NodeMixin, Generic[TOPODS]): Args: axis (Axis): rotation Axis angle (float): angle to rotate, in degrees + transform (bool): regenerate the shape instead of just changing its location. + Defaults to False. Returns: a copy of the shape, rotated """ + if self._wrapped is None: # For backwards compatibility + return self + transformation = gp_Trsf() transformation.SetRotation(axis.wrapped, angle * DEG2RAD) - return self._apply_transform(transformation) + if transform: + rotated_self = self._apply_transform(transformation) + else: + rotated_self = self.moved(Location(TopLoc_Location(transformation))) + return rotated_self def scale(self, factor: float) -> Self: """Scales this shape through a transformation. @@ -2239,20 +2248,28 @@ class Shape(NodeMixin, Generic[TOPODS]): t_o.SetTranslation(Vector(offset).wrapped) return self._apply_transform(t_o * t_rx * t_ry * t_rz) - def translate(self, vector: VectorLike) -> Self: + def translate(self, vector: VectorLike, transform: bool = False) -> Self: """Translates this shape through a transformation. Args: - vector: VectorLike: + vector (VectorLike): relative movement vector + transform (bool): regenerate the shape instead of just changing its location + Defaults to False. Returns: - + object with a relative move applied """ + if self._wrapped is None: # For backwards compatibility + return self transformation = gp_Trsf() transformation.SetTranslation(Vector(vector).wrapped) - return self._apply_transform(transformation) + if transform: + self_translated = self._apply_transform(transformation) + else: + self_translated = self.moved(Location(TopLoc_Location(transformation))) + return self_translated def wire(self) -> Wire | None: """Return the Wire""" diff --git a/tests/test_direct_api/test_shape.py b/tests/test_direct_api/test_shape.py index a261f8f..113422e 100644 --- a/tests/test_direct_api/test_shape.py +++ b/tests/test_direct_api/test_shape.py @@ -28,6 +28,7 @@ license: # Always equal to any other object, to test that __eq__ cooperation is working import unittest +import math from random import uniform from unittest.mock import PropertyMock, patch @@ -634,6 +635,39 @@ class TestShape(unittest.TestCase): self.assertIsNone(Vertex(1, 1, 1).solid()) self.assertIsNone(Vertex(1, 1, 1).compound()) + def test_rotate(self): + line = Edge.make_line((0, 0), (1, 0)) + rotated_line = line.rotate(Axis((1, 0, 0), (0, 0, 1)), 45) + root_2o2 = math.sqrt(2) / 2 + self.assertAlmostEqual(rotated_line @ 0, (1 - root_2o2, -root_2o2)) + self.assertAlmostEqual(rotated_line @ 1, (1, 0)) + self.assertTrue(line.wrapped.IsPartner(rotated_line.wrapped)) + + rotated_line = line.rotate(Axis((1, 0, 0), (0, 0, 1)), 45, transform=True) + self.assertAlmostEqual(rotated_line @ 0, (1 - root_2o2, -root_2o2)) + self.assertAlmostEqual(rotated_line @ 1, (1, 0)) + self.assertFalse(line.wrapped.IsPartner(rotated_line.wrapped)) + + line._wrapped = None + rotated_line = line.rotate(Axis((1, 0, 0), (0, 0, 1)), 45) + self.assertIsNone(rotated_line._wrapped) + + def test_translate(self): + line = Edge.make_line((0, 0), (1, 0)) + translated_line = line.translate((0, 1, 0)) + self.assertAlmostEqual(translated_line @ 0, (0, 1, 0)) + self.assertAlmostEqual(translated_line @ 1, (1, 1, 0)) + self.assertTrue(line.wrapped.IsPartner(translated_line.wrapped)) + + translated_line = line.translate((0, 1, 0), transform=True) + self.assertAlmostEqual(translated_line @ 0, (0, 1, 0)) + self.assertAlmostEqual(translated_line @ 1, (1, 1, 0)) + self.assertFalse(line.wrapped.IsPartner(translated_line.wrapped)) + + line._wrapped = None + translated_line = line.translate((0, 1, 0)) + self.assertIsNone(translated_line._wrapped) + class TestGlobalLocation(unittest.TestCase): def test_global_location_hierarchy(self):