From b4903d4ea260125eea8dd357b16c2b09f0e56ff9 Mon Sep 17 00:00:00 2001 From: Joschua-Conrad <32811308+Joschua-Conrad@users.noreply.github.com> Date: Thu, 9 Oct 2025 10:25:06 +0200 Subject: [PATCH 01/26] Added new __init__ multimethod of Plane --- cadquery/occ_impl/geom.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/cadquery/occ_impl/geom.py b/cadquery/occ_impl/geom.py index b519d97b4..e4359772a 100644 --- a/cadquery/occ_impl/geom.py +++ b/cadquery/occ_impl/geom.py @@ -576,6 +576,7 @@ def bottom(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)): plane._setPlaneDir(xDir) return plane + @multimethod def __init__( self, origin: Union[Tuple[float, float, float], Vector], @@ -606,6 +607,30 @@ def __init__( self._setPlaneDir(xDir) self.origin = Vector(origin) + + @__init__.register + def __init__( + self, + loc: 'Location', + ): + """Create a Plane from Location loc""" + + loctuple = loc.toTuple() + pl = Plane(loctuple[0]) + #But that can be rotated. The problem is, that the tuple is applied + #on x, y and z direction in that order, but we need the reverse + #order. So we need to apply angles manually. + rx, ry, rz = loctuple[1] + pl = pl.rotated((0, 0, rz)) + pl = pl.rotated((0, ry, 0)) + pl = pl.rotated((rx, 0, 0)) + + + self.__init__(pl.origin) + self.xDir = pl.xDir + self.yDir = pl.yDir + self.zDir = pl.zDir + def _eq_iter(self, other): """Iterator to successively test equality""" From 6195872942a2a4b68cf67c26dfa74da177041a3e Mon Sep 17 00:00:00 2001 From: Joschua-Conrad <32811308+Joschua-Conrad@users.noreply.github.com> Date: Thu, 9 Oct 2025 10:26:00 +0200 Subject: [PATCH 02/26] Changed type hints of exisitng Plane.__init__ Otherwise creating of Plane from tuple of int does not work anymore due to introduced @multimethod --- cadquery/occ_impl/geom.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cadquery/occ_impl/geom.py b/cadquery/occ_impl/geom.py index e4359772a..d47232e29 100644 --- a/cadquery/occ_impl/geom.py +++ b/cadquery/occ_impl/geom.py @@ -579,9 +579,9 @@ def bottom(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)): @multimethod def __init__( self, - origin: Union[Tuple[float, float, float], Vector], - xDir: Optional[Union[Tuple[float, float, float], Vector]] = None, - normal: Union[Tuple[float, float, float], Vector] = (0, 0, 1), + origin: Union[Tuple[Real, Real, Real], Vector], + xDir: Optional[Union[Tuple[Real, Real, Real], Vector]] = None, + normal: Union[Tuple[Real, Real, Real], Vector] = (0, 0, 1), ): """ Create a Plane with an arbitrary orientation From 8fe6267d98d1434b2e3b4cdc5f8c90d9335eba8f Mon Sep 17 00:00:00 2001 From: Joschua-Conrad <32811308+Joschua-Conrad@users.noreply.github.com> Date: Thu, 9 Oct 2025 10:27:18 +0200 Subject: [PATCH 03/26] Introduced test of new Plane.__init__ --- tests/test_geom.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 tests/test_geom.py diff --git a/tests/test_geom.py b/tests/test_geom.py new file mode 100644 index 000000000..5fe5d326b --- /dev/null +++ b/tests/test_geom.py @@ -0,0 +1,33 @@ +from cadquery.occ_impl.shapes import Location, Plane +import pytest +import itertools + +def test_Plane_from_Location(): + originalloc = Location(x=3, y=5, z=6, rx=15, ry=25, rz=40) + originalpl = Plane((3, 5, 6), (2, 4, 7), (9, 8, 1)) + + plforth = Plane(originalloc) + locforth = Location(originalpl) + + locback = Location(plforth) + plback = Plane(locforth) + + locraws = list() + for loc in (originalloc, locback): + loc = loc.toTuple() + loc = tuple(itertools.chain(*loc)) + locraws.append(loc) + + plraws = list() + for pl in (originalpl, plback): + pl = ( + pl.origin.toTuple(), + pl.xDir.toTuple(), + pl.yDir.toTuple(), + pl.zDir.toTuple(), + ) + pl = tuple(itertools.chain(*pl)) + plraws.append(pl) + + assert locraws[0] == pytest.approx(locraws[1]) + assert plraws[0] == pytest.approx(plraws[1]) From 871fd11326b172294aa15838fcbb99cfee11a9e1 Mon Sep 17 00:00:00 2001 From: Joschua-Conrad <32811308+Joschua-Conrad@users.noreply.github.com> Date: Thu, 9 Oct 2025 10:33:13 +0200 Subject: [PATCH 04/26] Comments on introduced test --- tests/test_geom.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/test_geom.py b/tests/test_geom.py index 5fe5d326b..cdfdb6e4e 100644 --- a/tests/test_geom.py +++ b/tests/test_geom.py @@ -3,21 +3,26 @@ import itertools def test_Plane_from_Location(): + + #Star from random lcation and plane originalloc = Location(x=3, y=5, z=6, rx=15, ry=25, rz=40) originalpl = Plane((3, 5, 6), (2, 4, 7), (9, 8, 1)) + #Convert back and forth, such that comparable pairs are created plforth = Plane(originalloc) locforth = Location(originalpl) - locback = Location(plforth) plback = Plane(locforth) + #Create raw locations, which are flat tuples of raw numbers, suitable for + #assertion with pytes.approx locraws = list() for loc in (originalloc, locback): loc = loc.toTuple() loc = tuple(itertools.chain(*loc)) locraws.append(loc) + #Same for planes plraws = list() for pl in (originalpl, plback): pl = ( @@ -29,5 +34,6 @@ def test_Plane_from_Location(): pl = tuple(itertools.chain(*pl)) plraws.append(pl) + #Perform assertions assert locraws[0] == pytest.approx(locraws[1]) assert plraws[0] == pytest.approx(plraws[1]) From aed6fb18ac128715a58ace4aee1fb75f5b1482e1 Mon Sep 17 00:00:00 2001 From: Joschua-Conrad <32811308+Joschua-Conrad@users.noreply.github.com> Date: Thu, 9 Oct 2025 11:18:23 +0200 Subject: [PATCH 05/26] Test now has only a single initial Plane to start conversiosn from --- tests/test_geom.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_geom.py b/tests/test_geom.py index cdfdb6e4e..8e7a65837 100644 --- a/tests/test_geom.py +++ b/tests/test_geom.py @@ -4,20 +4,20 @@ def test_Plane_from_Location(): - #Star from random lcation and plane - originalloc = Location(x=3, y=5, z=6, rx=15, ry=25, rz=40) + #Start from random Plane with classical __init__ originalpl = Plane((3, 5, 6), (2, 4, 7), (9, 8, 1)) #Convert back and forth, such that comparable pairs are created - plforth = Plane(originalloc) + #plforth = Plane(originalloc) locforth = Location(originalpl) - locback = Location(plforth) + #locback = Location(plforth) plback = Plane(locforth) + locback = Location(plback) #Create raw locations, which are flat tuples of raw numbers, suitable for #assertion with pytes.approx locraws = list() - for loc in (originalloc, locback): + for loc in (locforth, locback): loc = loc.toTuple() loc = tuple(itertools.chain(*loc)) locraws.append(loc) From ba6abe4a33eb1087145abcbc45bd5d2abdd3c233 Mon Sep 17 00:00:00 2001 From: Joschua-Conrad <32811308+Joschua-Conrad@users.noreply.github.com> Date: Thu, 9 Oct 2025 13:50:42 +0200 Subject: [PATCH 06/26] Tests pass now, after enforcing that tested xDir and normal are orthogonal --- tests/test_geom.py | 52 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/tests/test_geom.py b/tests/test_geom.py index 8e7a65837..653fd07c4 100644 --- a/tests/test_geom.py +++ b/tests/test_geom.py @@ -1,16 +1,60 @@ -from cadquery.occ_impl.shapes import Location, Plane +from cadquery.occ_impl.shapes import Location, Plane, Vector import pytest import itertools +import math -def test_Plane_from_Location(): +#Use instead of 1 to create a vector with two non-zero elements and length 1 +twocomp = math.sqrt(1./2.) + +#Create different test cases from different initial plane. +#Testing the different components is mainly useful for debugging if things +#do not work. +@pytest.mark.parametrize( + ["plargs",], + [ + #Default plane + (((3, 5, 6),),), + #Just xDir specified + (((3, 5, 6), (1, 0, 0),),), + (((3, 5, 6), (0, 1, 0),),), + #xDir and normal specified + (((3, 5, 6), (1, 0, 0), (0, 0, 1),),), + (((3, 5, 6), (1, 0, 0), (0, 1, 0),),), + (((3, 5, 6), (0, 1, 0), (0, 0, 1),),), + (((3, 5, 6), (0, 1, 0), (1, 0, 0),),), + #JUst xDir, but with multiple vector components + (((3, 5, 6), (1, 1, 0),),), + (((3, 5, 6), (1, 0, 1),),), + (((3, 5, 6), (0, 1, 1),),), + #Vectors with random non-trivial directions + (((3, 5, 6), (2, 4, 7), (9, 8, 1),),), + ] +) +def test_Plane_from_Location(plargs): + #Test conversion between Plane and Location by converting multiple + #times between them, such that two Plane and two Location can be + #compared respectively. + + #If there are three things in plargs, ensure that xDir and normal are + #orthogonal. That should be ensured by an exception in Plane.__init__. + #This here makes the normal orthogonal to xDir by subtracting its + #projection on xDir. + #If no normal is given, the default normal is assumed. + if len(plargs) == 2: + plargs = (*plargs, (0, 0, 1)) + if len(plargs) == 3: + origin, xDir, normal = plargs + xDir = Vector(xDir) + normal = Vector(normal) + normal -= normal.projectToLine(xDir) + plargs = (origin, xDir, normal) #Start from random Plane with classical __init__ - originalpl = Plane((3, 5, 6), (2, 4, 7), (9, 8, 1)) + originalpl = Plane(*plargs) #Convert back and forth, such that comparable pairs are created #plforth = Plane(originalloc) locforth = Location(originalpl) - #locback = Location(plforth) plback = Plane(locforth) locback = Location(plback) From 95608fedd440967853367a46b47d28a9e9212b61 Mon Sep 17 00:00:00 2001 From: Joschua-Conrad <32811308+Joschua-Conrad@users.noreply.github.com> Date: Thu, 9 Oct 2025 13:57:07 +0200 Subject: [PATCH 07/26] New constructor is now also available as property --- cadquery/occ_impl/geom.py | 5 +++++ tests/test_geom.py | 25 +++++++++++++++++++------ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/cadquery/occ_impl/geom.py b/cadquery/occ_impl/geom.py index d47232e29..ea35faece 100644 --- a/cadquery/occ_impl/geom.py +++ b/cadquery/occ_impl/geom.py @@ -1132,6 +1132,11 @@ def toTuple(self) -> Tuple[Tuple[float, float, float], Tuple[float, float, float rx, ry, rz = rot.GetEulerAngles(gp_EulerSequence.gp_Extrinsic_XYZ) return rv_trans, (degrees(rx), degrees(ry), degrees(rz)) + + @property + def plane(self) -> "Plane": + + return Plane(self) def __getstate__(self) -> BytesIO: diff --git a/tests/test_geom.py b/tests/test_geom.py index 653fd07c4..7470ec687 100644 --- a/tests/test_geom.py +++ b/tests/test_geom.py @@ -6,6 +6,14 @@ #Use instead of 1 to create a vector with two non-zero elements and length 1 twocomp = math.sqrt(1./2.) +#Conversion can betriggered from explicit constructor, or from property +@pytest.mark.parametrize( + ["useproperty",], + [ + (False,), + (True,), + ] +) #Create different test cases from different initial plane. #Testing the different components is mainly useful for debugging if things #do not work. @@ -30,7 +38,7 @@ (((3, 5, 6), (2, 4, 7), (9, 8, 1),),), ] ) -def test_Plane_from_Location(plargs): +def test_Plane_from_Location(plargs, useproperty): #Test conversion between Plane and Location by converting multiple #times between them, such that two Plane and two Location can be #compared respectively. @@ -52,11 +60,16 @@ def test_Plane_from_Location(plargs): #Start from random Plane with classical __init__ originalpl = Plane(*plargs) - #Convert back and forth, such that comparable pairs are created - #plforth = Plane(originalloc) - locforth = Location(originalpl) - plback = Plane(locforth) - locback = Location(plback) + #Convert back and forth, such that comparable pairs are created. + #Depending on test fixture, call constructor directly or use properties + if useproperty: + locforth = originalpl.location + plback = locforth.plane + locback = plback.location + else: + locforth = Location(originalpl) + plback = Plane(locforth) + locback = Location(plback) #Create raw locations, which are flat tuples of raw numbers, suitable for #assertion with pytes.approx From c5319641ca12e1fc891510c8262729eb21096596 Mon Sep 17 00:00:00 2001 From: Joschua-Conrad <32811308+Joschua-Conrad@users.noreply.github.com> Date: Thu, 9 Oct 2025 14:38:12 +0200 Subject: [PATCH 08/26] Removed obsolete test code lines --- tests/test_geom.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/test_geom.py b/tests/test_geom.py index 7470ec687..2eca6d740 100644 --- a/tests/test_geom.py +++ b/tests/test_geom.py @@ -1,10 +1,6 @@ from cadquery.occ_impl.shapes import Location, Plane, Vector import pytest import itertools -import math - -#Use instead of 1 to create a vector with two non-zero elements and length 1 -twocomp = math.sqrt(1./2.) #Conversion can betriggered from explicit constructor, or from property @pytest.mark.parametrize( From 86b4d4067812775bc7c91d05bb57dbe8ba1a4a15 Mon Sep 17 00:00:00 2001 From: Joschua-Conrad <32811308+Joschua-Conrad@users.noreply.github.com> Date: Thu, 9 Oct 2025 14:42:03 +0200 Subject: [PATCH 09/26] Removed redundant test parametrizations --- tests/test_geom.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_geom.py b/tests/test_geom.py index 2eca6d740..e38f7915c 100644 --- a/tests/test_geom.py +++ b/tests/test_geom.py @@ -21,10 +21,10 @@ #Just xDir specified (((3, 5, 6), (1, 0, 0),),), (((3, 5, 6), (0, 1, 0),),), - #xDir and normal specified - (((3, 5, 6), (1, 0, 0), (0, 0, 1),),), + #xDir and normal specified. + #Omit normals, that were included as default, and once which + #have no component orthogonal to xDir (((3, 5, 6), (1, 0, 0), (0, 1, 0),),), - (((3, 5, 6), (0, 1, 0), (0, 0, 1),),), (((3, 5, 6), (0, 1, 0), (1, 0, 0),),), #JUst xDir, but with multiple vector components (((3, 5, 6), (1, 1, 0),),), From d4b3b2d7daa03ae4a110dc7f79ab454b96ace7a0 Mon Sep 17 00:00:00 2001 From: Joschua-Conrad <32811308+Joschua-Conrad@users.noreply.github.com> Date: Thu, 9 Oct 2025 14:52:46 +0200 Subject: [PATCH 10/26] Gave new Plane.__init__ a more elaborate logic. Instead of just creating a temporary plane and rotating it --- cadquery/occ_impl/geom.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/cadquery/occ_impl/geom.py b/cadquery/occ_impl/geom.py index ea35faece..0dffc8307 100644 --- a/cadquery/occ_impl/geom.py +++ b/cadquery/occ_impl/geom.py @@ -615,22 +615,21 @@ def __init__( ): """Create a Plane from Location loc""" - loctuple = loc.toTuple() - pl = Plane(loctuple[0]) - #But that can be rotated. The problem is, that the tuple is applied - #on x, y and z direction in that order, but we need the reverse - #order. So we need to apply angles manually. - rx, ry, rz = loctuple[1] - pl = pl.rotated((0, 0, rz)) - pl = pl.rotated((0, ry, 0)) - pl = pl.rotated((rx, 0, 0)) + origin, rotations = loc.toTuple() + transformation = Matrix() + transformation.rotateX(rotations[0] * pi / 180.0) + transformation.rotateY(rotations[1] * pi / 180.0) + transformation.rotateZ(rotations[2] * pi / 180.0) - self.__init__(pl.origin) - self.xDir = pl.xDir - self.yDir = pl.yDir - self.zDir = pl.zDir - + dirs = ((1, 0, 0), (0, 1, 0), (0, 0, 1)) + dirs = (Vector(*i).normalized() for i in dirs) + dirs = (i.transform(transformation) for i in dirs) + + self.xDir, self.yDir, self.zDir = dirs + #Set origin last, as that triggers self._calcTransforms via + #origin property, and that needs the dirs to be recent. + self.origin = origin def _eq_iter(self, other): """Iterator to successively test equality""" From c94bd36bd31e0a415cea223804ab6f0f283bc02d Mon Sep 17 00:00:00 2001 From: Joschua-Conrad <32811308+Joschua-Conrad@users.noreply.github.com> Date: Thu, 9 Oct 2025 15:22:07 +0200 Subject: [PATCH 11/26] Fixed rotation order in new Plane.__init__ --- cadquery/occ_impl/geom.py | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/cadquery/occ_impl/geom.py b/cadquery/occ_impl/geom.py index 0dffc8307..563384321 100644 --- a/cadquery/occ_impl/geom.py +++ b/cadquery/occ_impl/geom.py @@ -615,17 +615,36 @@ def __init__( ): """Create a Plane from Location loc""" + #Ask location for its information origin, rotations = loc.toTuple() + #Origin is easy, but the rotational angles of the location need to be + #turned into xDir and normal vectors. + #This is done by multiplying a standard cooridnate system by the given + #angles. + #Rotation of vectors is done by a transformation matrix. + #The order in which rotational angles are introduced is crucial: + #If u is our vector, Rx is rotation around x axis etc, we want the + #following: + #u' = Rz * Ry * Rx * u = R * u + #That way, all rotational angles refer to a global coordinate system, + #and e.g. Ry does not refer to a rotation direction, which already + #was rotated around Rx. + #This definition in the global system is called extrinsic, and it is + #how the Location class wants it to be done. + #And this is why we introduce the rotations from left to right + #and from Z to X. transformation = Matrix() - transformation.rotateX(rotations[0] * pi / 180.0) - transformation.rotateY(rotations[1] * pi / 180.0) transformation.rotateZ(rotations[2] * pi / 180.0) + transformation.rotateY(rotations[1] * pi / 180.0) + transformation.rotateX(rotations[0] * pi / 180.0) + #Apply rotation on the cadquery global coordinate system. + #These vectors are already unit vectors and require no .normalized() dirs = ((1, 0, 0), (0, 1, 0), (0, 0, 1)) - dirs = (Vector(*i).normalized() for i in dirs) - dirs = (i.transform(transformation) for i in dirs) + dirs = (Vector(*i).transform(transformation) for i in dirs) + #Unpack vectors and set attributes self.xDir, self.yDir, self.zDir = dirs #Set origin last, as that triggers self._calcTransforms via #origin property, and that needs the dirs to be recent. From 371655bf78a6fee0d137e8ce7cba0b0e9ebeb243 Mon Sep 17 00:00:00 2001 From: Joschua-Conrad <32811308+Joschua-Conrad@users.noreply.github.com> Date: Thu, 9 Oct 2025 15:28:33 +0200 Subject: [PATCH 12/26] Added more test parametrizations --- tests/test_geom.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/test_geom.py b/tests/test_geom.py index e38f7915c..ac351f536 100644 --- a/tests/test_geom.py +++ b/tests/test_geom.py @@ -30,6 +30,20 @@ (((3, 5, 6), (1, 1, 0),),), (((3, 5, 6), (1, 0, 1),),), (((3, 5, 6), (0, 1, 1),),), + #Multiple components in xdir and normal + (((3, 5, 6), (1, 1, 0), (1, 0, 1),),), + (((3, 5, 6), (1, 1, 0), (0, 1, 1),),), + (((3, 5, 6), (1, 0, 1), (1, 1, 0),),), + (((3, 5, 6), (1, 0, 1), (0, 1, 1),),), + (((3, 5, 6), (0, 1, 1), (1, 1, 0),),), + (((3, 5, 6), (0, 1, 1), (1, 0, 1),),), + #Same, but introduce negative directions + (((3, 5, 6), (-1, 1, 0), (-1, 0, -1),),), + (((3, 5, 6), (1, -1, 0), (0, -1, -1),),), + (((3, 5, 6), (1, 0, -1), (1, -1, 0),),), + (((3, 5, 6), (1, 0, -1), (0, -1, 1),),), + (((3, 5, 6), (0, -1, -1), (-1, 1, 0),),), + (((3, 5, 6), (0, -1, -1), (1, 0, -1),),), #Vectors with random non-trivial directions (((3, 5, 6), (2, 4, 7), (9, 8, 1),),), ] From f102c36ce6ea24014b80837ee8de6951af8206f8 Mon Sep 17 00:00:00 2001 From: Joschua-Conrad <32811308+Joschua-Conrad@users.noreply.github.com> Date: Thu, 9 Oct 2025 15:30:13 +0200 Subject: [PATCH 13/26] Comment --- tests/test_geom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_geom.py b/tests/test_geom.py index ac351f536..c8c4f19de 100644 --- a/tests/test_geom.py +++ b/tests/test_geom.py @@ -18,7 +18,7 @@ [ #Default plane (((3, 5, 6),),), - #Just xDir specified + #Just xDir specified, but never parallel to the default normal (((3, 5, 6), (1, 0, 0),),), (((3, 5, 6), (0, 1, 0),),), #xDir and normal specified. From f3437f7eba5fb71b109c70939ec7aa0d6f285ca1 Mon Sep 17 00:00:00 2001 From: Joschua-Conrad <32811308+Joschua-Conrad@users.noreply.github.com> Date: Thu, 9 Oct 2025 18:30:56 +0200 Subject: [PATCH 14/26] Resolved all flake8 linting warnings: whitespace only --- cadquery/occ_impl/geom.py | 54 +++++++++++++++--------------- tests/test_geom.py | 69 ++++++++++++++++++++------------------- 2 files changed, 62 insertions(+), 61 deletions(-) diff --git a/cadquery/occ_impl/geom.py b/cadquery/occ_impl/geom.py index 563384321..5fce1f2e6 100644 --- a/cadquery/occ_impl/geom.py +++ b/cadquery/occ_impl/geom.py @@ -607,47 +607,47 @@ def __init__( self._setPlaneDir(xDir) self.origin = Vector(origin) - + @__init__.register def __init__( self, loc: 'Location', ): """Create a Plane from Location loc""" - - #Ask location for its information + + # Ask location for its information origin, rotations = loc.toTuple() - - #Origin is easy, but the rotational angles of the location need to be - #turned into xDir and normal vectors. - #This is done by multiplying a standard cooridnate system by the given - #angles. - #Rotation of vectors is done by a transformation matrix. - #The order in which rotational angles are introduced is crucial: - #If u is our vector, Rx is rotation around x axis etc, we want the - #following: - #u' = Rz * Ry * Rx * u = R * u - #That way, all rotational angles refer to a global coordinate system, - #and e.g. Ry does not refer to a rotation direction, which already - #was rotated around Rx. - #This definition in the global system is called extrinsic, and it is - #how the Location class wants it to be done. - #And this is why we introduce the rotations from left to right - #and from Z to X. + + # Origin is easy, but the rotational angles of the location need to be + # turned into xDir and normal vectors. + # This is done by multiplying a standard cooridnate system by the given + # angles. + # Rotation of vectors is done by a transformation matrix. + # The order in which rotational angles are introduced is crucial: + # If u is our vector, Rx is rotation around x axis etc, we want the + # following: + # u' = Rz * Ry * Rx * u = R * u + # That way, all rotational angles refer to a global coordinate system, + # and e.g. Ry does not refer to a rotation direction, which already + # was rotated around Rx. + # This definition in the global system is called extrinsic, and it is + # how the Location class wants it to be done. + # And this is why we introduce the rotations from left to right + # and from Z to X. transformation = Matrix() transformation.rotateZ(rotations[2] * pi / 180.0) transformation.rotateY(rotations[1] * pi / 180.0) transformation.rotateX(rotations[0] * pi / 180.0) - - #Apply rotation on the cadquery global coordinate system. - #These vectors are already unit vectors and require no .normalized() + + # Apply rotation on the cadquery global coordinate system. + # These vectors are already unit vectors and require no .normalized() dirs = ((1, 0, 0), (0, 1, 0), (0, 0, 1)) dirs = (Vector(*i).transform(transformation) for i in dirs) - #Unpack vectors and set attributes + # Unpack vectors and set attributes self.xDir, self.yDir, self.zDir = dirs - #Set origin last, as that triggers self._calcTransforms via - #origin property, and that needs the dirs to be recent. + # Set origin last, as that triggers self._calcTransforms via + # origin property, and that needs the dirs to be recent. self.origin = origin def _eq_iter(self, other): @@ -1150,7 +1150,7 @@ def toTuple(self) -> Tuple[Tuple[float, float, float], Tuple[float, float, float rx, ry, rz = rot.GetEulerAngles(gp_EulerSequence.gp_Extrinsic_XYZ) return rv_trans, (degrees(rx), degrees(ry), degrees(rz)) - + @property def plane(self) -> "Plane": diff --git a/tests/test_geom.py b/tests/test_geom.py index c8c4f19de..9d980d86b 100644 --- a/tests/test_geom.py +++ b/tests/test_geom.py @@ -2,7 +2,8 @@ import pytest import itertools -#Conversion can betriggered from explicit constructor, or from property + +# Conversion can betriggered from explicit constructor, or from property @pytest.mark.parametrize( ["useproperty",], [ @@ -10,54 +11,54 @@ (True,), ] ) -#Create different test cases from different initial plane. -#Testing the different components is mainly useful for debugging if things -#do not work. +# Create different test cases from different initial plane. +# Testing the different components is mainly useful for debugging if things +# do not work. @pytest.mark.parametrize( ["plargs",], [ - #Default plane + # Default plane (((3, 5, 6),),), - #Just xDir specified, but never parallel to the default normal + # Just xDir specified, but never parallel to the default normal (((3, 5, 6), (1, 0, 0),),), (((3, 5, 6), (0, 1, 0),),), - #xDir and normal specified. - #Omit normals, that were included as default, and once which - #have no component orthogonal to xDir + # xDir and normal specified. + # Omit normals, that were included as default, and once which + # have no component orthogonal to xDir (((3, 5, 6), (1, 0, 0), (0, 1, 0),),), (((3, 5, 6), (0, 1, 0), (1, 0, 0),),), - #JUst xDir, but with multiple vector components + # JUst xDir, but with multiple vector components (((3, 5, 6), (1, 1, 0),),), (((3, 5, 6), (1, 0, 1),),), (((3, 5, 6), (0, 1, 1),),), - #Multiple components in xdir and normal + # Multiple components in xdir and normal (((3, 5, 6), (1, 1, 0), (1, 0, 1),),), (((3, 5, 6), (1, 1, 0), (0, 1, 1),),), (((3, 5, 6), (1, 0, 1), (1, 1, 0),),), (((3, 5, 6), (1, 0, 1), (0, 1, 1),),), (((3, 5, 6), (0, 1, 1), (1, 1, 0),),), (((3, 5, 6), (0, 1, 1), (1, 0, 1),),), - #Same, but introduce negative directions + # Same, but introduce negative directions (((3, 5, 6), (-1, 1, 0), (-1, 0, -1),),), (((3, 5, 6), (1, -1, 0), (0, -1, -1),),), (((3, 5, 6), (1, 0, -1), (1, -1, 0),),), (((3, 5, 6), (1, 0, -1), (0, -1, 1),),), (((3, 5, 6), (0, -1, -1), (-1, 1, 0),),), (((3, 5, 6), (0, -1, -1), (1, 0, -1),),), - #Vectors with random non-trivial directions + # Vectors with random non-trivial directions (((3, 5, 6), (2, 4, 7), (9, 8, 1),),), ] ) def test_Plane_from_Location(plargs, useproperty): - #Test conversion between Plane and Location by converting multiple - #times between them, such that two Plane and two Location can be - #compared respectively. - - #If there are three things in plargs, ensure that xDir and normal are - #orthogonal. That should be ensured by an exception in Plane.__init__. - #This here makes the normal orthogonal to xDir by subtracting its - #projection on xDir. - #If no normal is given, the default normal is assumed. + # Test conversion between Plane and Location by converting multiple + # times between them, such that two Plane and two Location can be + # compared respectively. + + # If there are three things in plargs, ensure that xDir and normal are + # orthogonal. That should be ensured by an exception in Plane.__init__. + # This here makes the normal orthogonal to xDir by subtracting its + # projection on xDir. + # If no normal is given, the default normal is assumed. if len(plargs) == 2: plargs = (*plargs, (0, 0, 1)) if len(plargs) == 3: @@ -66,12 +67,12 @@ def test_Plane_from_Location(plargs, useproperty): normal = Vector(normal) normal -= normal.projectToLine(xDir) plargs = (origin, xDir, normal) - - #Start from random Plane with classical __init__ + + # Start from random Plane with classical __init__ originalpl = Plane(*plargs) - - #Convert back and forth, such that comparable pairs are created. - #Depending on test fixture, call constructor directly or use properties + + # Convert back and forth, such that comparable pairs are created. + # Depending on test fixture, call constructor directly or use properties if useproperty: locforth = originalpl.location plback = locforth.plane @@ -80,16 +81,16 @@ def test_Plane_from_Location(plargs, useproperty): locforth = Location(originalpl) plback = Plane(locforth) locback = Location(plback) - - #Create raw locations, which are flat tuples of raw numbers, suitable for - #assertion with pytes.approx + + # Create raw locations, which are flat tuples of raw numbers, suitable for + # assertion with pytes.approx locraws = list() for loc in (locforth, locback): loc = loc.toTuple() loc = tuple(itertools.chain(*loc)) locraws.append(loc) - - #Same for planes + + # Same for planes plraws = list() for pl in (originalpl, plback): pl = ( @@ -100,7 +101,7 @@ def test_Plane_from_Location(plargs, useproperty): ) pl = tuple(itertools.chain(*pl)) plraws.append(pl) - - #Perform assertions + + # Perform assertions assert locraws[0] == pytest.approx(locraws[1]) assert plraws[0] == pytest.approx(plraws[1]) From afc7e1f297096a44e5f1bb9ed896d328f79e6a5a Mon Sep 17 00:00:00 2001 From: Joschua-Conrad <32811308+Joschua-Conrad@users.noreply.github.com> Date: Thu, 9 Oct 2025 18:39:20 +0200 Subject: [PATCH 15/26] Ran black --- cadquery/occ_impl/geom.py | 2 +- tests/test_geom.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cadquery/occ_impl/geom.py b/cadquery/occ_impl/geom.py index 5fce1f2e6..2cd3064e0 100644 --- a/cadquery/occ_impl/geom.py +++ b/cadquery/occ_impl/geom.py @@ -611,7 +611,7 @@ def __init__( @__init__.register def __init__( self, - loc: 'Location', + loc: "Location", ): """Create a Plane from Location loc""" diff --git a/tests/test_geom.py b/tests/test_geom.py index 9d980d86b..44987e277 100644 --- a/tests/test_geom.py +++ b/tests/test_geom.py @@ -9,7 +9,7 @@ [ (False,), (True,), - ] + ], ) # Create different test cases from different initial plane. # Testing the different components is mainly useful for debugging if things From fc84aca00520f26bbbaac461f8c79f84b9919801 Mon Sep 17 00:00:00 2001 From: Joschua-Conrad <32811308+Joschua-Conrad@users.noreply.github.com> Date: Thu, 9 Oct 2025 18:51:56 +0200 Subject: [PATCH 16/26] Again supporting keyword arguments on Plane.__init__ --- cadquery/occ_impl/geom.py | 7 ++++++- tests/test_geom.py | 9 +++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/cadquery/occ_impl/geom.py b/cadquery/occ_impl/geom.py index 2cd3064e0..ed7e1947a 100644 --- a/cadquery/occ_impl/geom.py +++ b/cadquery/occ_impl/geom.py @@ -26,6 +26,8 @@ from OCP.TopLoc import TopLoc_Location from OCP.BinTools import BinTools_LocationSet +from multimethod import multidispatch + from ..types import Real from ..utils import multimethod @@ -576,7 +578,10 @@ def bottom(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)): plane._setPlaneDir(xDir) return plane - @multimethod + # Prefer multidispatch over multimethod, as that supports keyword + # arguments. These are in use, since Plane.__init__ has not always + # been a multimethod. + @multidispatch def __init__( self, origin: Union[Tuple[Real, Real, Real], Vector], diff --git a/tests/test_geom.py b/tests/test_geom.py index 44987e277..6b3525e25 100644 --- a/tests/test_geom.py +++ b/tests/test_geom.py @@ -66,10 +66,15 @@ def test_Plane_from_Location(plargs, useproperty): xDir = Vector(xDir) normal = Vector(normal) normal -= normal.projectToLine(xDir) - plargs = (origin, xDir, normal) # Start from random Plane with classical __init__ - originalpl = Plane(*plargs) + # Use keyword arguments on purpose, as they still need to work after + # having @multidispatch added to that __init__. + # Test that on cases, where plargs has three elements and was unpacked. + if len(plargs) == 3: + originalpl = Plane(origin=origin, xDir=xDir, normal=normal) + else: + originalpl = Plane(*plargs) # Convert back and forth, such that comparable pairs are created. # Depending on test fixture, call constructor directly or use properties From f53e871efc995aebfc92e2fef070327d192d8d15 Mon Sep 17 00:00:00 2001 From: Joschua-Conrad <32811308+Joschua-Conrad@users.noreply.github.com> Date: Thu, 9 Oct 2025 19:08:01 +0200 Subject: [PATCH 17/26] Tried adding multimethod support to docs, but output still looks bad --- doc/ext/sphinx_autodoc_multimethod.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/ext/sphinx_autodoc_multimethod.py b/doc/ext/sphinx_autodoc_multimethod.py index 86527d825..847fdcfd9 100644 --- a/doc/ext/sphinx_autodoc_multimethod.py +++ b/doc/ext/sphinx_autodoc_multimethod.py @@ -1,6 +1,6 @@ from types import ModuleType from typing import Any, List, Tuple, ValuesView -from multimethod import multimethod +from multimethod import multimethod, multidispatch import re from sphinx.ext.autosummary import Autosummary @@ -39,7 +39,7 @@ def process_docstring_multimethod(app, what, name, obj, options, lines): methods = [] - if what == "method" and isinstance(obj, multimethod): + if what == "method" and isinstance(obj, (multimethod, multidispatch)): # instance or static method # handle functools.singledispatch style register (multiple names) @@ -156,7 +156,7 @@ def get_items(self, names: List[str]) -> List[Tuple[str, str, str, str]]: try: sig = documenter.format_signature(show_annotation=False) # -- multimethod customization - if isinstance(obj, multimethod): + if isinstance(obj, (multimethod, multidispatch)): sig = "(...)" # -- end customization except TypeError: @@ -226,7 +226,7 @@ def format_signature(self, **kwargs: Any) -> str: documenter.objpath = [None] sigs.append(documenter.format_signature()) # -- multimethod customization - elif isinstance(meth, multimethod): + elif isinstance(meth, (multimethod, multidispatch)): if meth.pending: methods = meth.pending else: @@ -238,7 +238,7 @@ def format_signature(self, **kwargs: Any) -> str: else: methods = set(self.object.__func__.values()) sigs = self.append_signature_multiple_dispatch(methods) - elif inspect.isstaticmethod(meth) and isinstance(self.object, multimethod): + elif inspect.isstaticmethod(meth) and isinstance(self.object, (multimethod, multidispatch)): sigs = [] methods = self.object.values() for dispatchmeth in methods: From 69ed03898f45a0172e54fb675cfc397762cf6cd6 Mon Sep 17 00:00:00 2001 From: Joschua-Conrad <32811308+Joschua-Conrad@users.noreply.github.com> Date: Thu, 9 Oct 2025 19:14:31 +0200 Subject: [PATCH 18/26] Reverted changes made to support multidispatch in sphinx --- doc/ext/sphinx_autodoc_multimethod.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/ext/sphinx_autodoc_multimethod.py b/doc/ext/sphinx_autodoc_multimethod.py index 847fdcfd9..86527d825 100644 --- a/doc/ext/sphinx_autodoc_multimethod.py +++ b/doc/ext/sphinx_autodoc_multimethod.py @@ -1,6 +1,6 @@ from types import ModuleType from typing import Any, List, Tuple, ValuesView -from multimethod import multimethod, multidispatch +from multimethod import multimethod import re from sphinx.ext.autosummary import Autosummary @@ -39,7 +39,7 @@ def process_docstring_multimethod(app, what, name, obj, options, lines): methods = [] - if what == "method" and isinstance(obj, (multimethod, multidispatch)): + if what == "method" and isinstance(obj, multimethod): # instance or static method # handle functools.singledispatch style register (multiple names) @@ -156,7 +156,7 @@ def get_items(self, names: List[str]) -> List[Tuple[str, str, str, str]]: try: sig = documenter.format_signature(show_annotation=False) # -- multimethod customization - if isinstance(obj, (multimethod, multidispatch)): + if isinstance(obj, multimethod): sig = "(...)" # -- end customization except TypeError: @@ -226,7 +226,7 @@ def format_signature(self, **kwargs: Any) -> str: documenter.objpath = [None] sigs.append(documenter.format_signature()) # -- multimethod customization - elif isinstance(meth, (multimethod, multidispatch)): + elif isinstance(meth, multimethod): if meth.pending: methods = meth.pending else: @@ -238,7 +238,7 @@ def format_signature(self, **kwargs: Any) -> str: else: methods = set(self.object.__func__.values()) sigs = self.append_signature_multiple_dispatch(methods) - elif inspect.isstaticmethod(meth) and isinstance(self.object, (multimethod, multidispatch)): + elif inspect.isstaticmethod(meth) and isinstance(self.object, multimethod): sigs = [] methods = self.object.values() for dispatchmeth in methods: From b5df9cfa2d4ee9dfde39c63b27689ddc9e50e638 Mon Sep 17 00:00:00 2001 From: Joschua-Conrad <32811308+Joschua-Conrad@users.noreply.github.com> Date: Thu, 9 Oct 2025 19:25:15 +0200 Subject: [PATCH 19/26] Turned new Plane.__init__ into a class method multimethod did not work, as that disallows keyword arguments for construction of Plane, which ruins many tests multidispatch works with keyword arguments, but looks horrible in docs Construction from class method just works. --- cadquery/occ_impl/geom.py | 31 +++++++++++++------------------ tests/test_geom.py | 2 +- 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/cadquery/occ_impl/geom.py b/cadquery/occ_impl/geom.py index ed7e1947a..7e7eaf064 100644 --- a/cadquery/occ_impl/geom.py +++ b/cadquery/occ_impl/geom.py @@ -26,8 +26,6 @@ from OCP.TopLoc import TopLoc_Location from OCP.BinTools import BinTools_LocationSet -from multimethod import multidispatch - from ..types import Real from ..utils import multimethod @@ -578,10 +576,6 @@ def bottom(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)): plane._setPlaneDir(xDir) return plane - # Prefer multidispatch over multimethod, as that supports keyword - # arguments. These are in use, since Plane.__init__ has not always - # been a multimethod. - @multidispatch def __init__( self, origin: Union[Tuple[Real, Real, Real], Vector], @@ -613,11 +607,11 @@ def __init__( self._setPlaneDir(xDir) self.origin = Vector(origin) - @__init__.register - def __init__( - self, + @classmethod + def fromLocation( + cls, loc: "Location", - ): + ) -> "Plane": """Create a Plane from Location loc""" # Ask location for its information @@ -644,16 +638,17 @@ def __init__( transformation.rotateY(rotations[1] * pi / 180.0) transformation.rotateX(rotations[0] * pi / 180.0) - # Apply rotation on the cadquery global coordinate system. + # Apply rotation on vectors of the global plane # These vectors are already unit vectors and require no .normalized() - dirs = ((1, 0, 0), (0, 1, 0), (0, 0, 1)) + dirs = ((1, 0, 0), (0, 0, 1)) dirs = (Vector(*i).transform(transformation) for i in dirs) - # Unpack vectors and set attributes - self.xDir, self.yDir, self.zDir = dirs - # Set origin last, as that triggers self._calcTransforms via - # origin property, and that needs the dirs to be recent. - self.origin = origin + # Unpack vectors + xDir, normal = dirs + # Construct a plane and return it + pl = cls(origin=origin, xDir=xDir, normal=normal) + + return pl def _eq_iter(self, other): """Iterator to successively test equality""" @@ -1159,7 +1154,7 @@ def toTuple(self) -> Tuple[Tuple[float, float, float], Tuple[float, float, float @property def plane(self) -> "Plane": - return Plane(self) + return Plane.fromLocation(self) def __getstate__(self) -> BytesIO: diff --git a/tests/test_geom.py b/tests/test_geom.py index 6b3525e25..6dd323ac1 100644 --- a/tests/test_geom.py +++ b/tests/test_geom.py @@ -84,7 +84,7 @@ def test_Plane_from_Location(plargs, useproperty): locback = plback.location else: locforth = Location(originalpl) - plback = Plane(locforth) + plback = Plane.fromLocation(locforth) locback = Location(plback) # Create raw locations, which are flat tuples of raw numbers, suitable for From 607bb83d2d8e4b7e408d857e71ca2bbbe0832335 Mon Sep 17 00:00:00 2001 From: Joschua-Conrad <32811308+Joschua-Conrad@users.noreply.github.com> Date: Fri, 10 Oct 2025 08:35:10 +0200 Subject: [PATCH 20/26] More assertions on expected Location properites --- tests/test_geom.py | 76 ++++++++++++++++++++++++++++++---------------- 1 file changed, 49 insertions(+), 27 deletions(-) diff --git a/tests/test_geom.py b/tests/test_geom.py index 6dd323ac1..716f94f2b 100644 --- a/tests/test_geom.py +++ b/tests/test_geom.py @@ -14,42 +14,47 @@ # Create different test cases from different initial plane. # Testing the different components is mainly useful for debugging if things # do not work. +# Arguments to Plane.__init__ along with expected rotations in a converted +# Location object are given here. @pytest.mark.parametrize( - ["plargs",], + ["plargs", "expectedrot"], [ # Default plane - (((3, 5, 6),),), + (((3, 5, 6),), (0, 0, 0),), # Just xDir specified, but never parallel to the default normal - (((3, 5, 6), (1, 0, 0),),), - (((3, 5, 6), (0, 1, 0),),), + (((3, 5, 6), (1, 0, 0),), (0, 0, 0),), + (((3, 5, 6), (0, 1, 0),), (0, 0, 90),), # xDir and normal specified. # Omit normals, that were included as default, and once which # have no component orthogonal to xDir - (((3, 5, 6), (1, 0, 0), (0, 1, 0),),), - (((3, 5, 6), (0, 1, 0), (1, 0, 0),),), + (((3, 5, 6), (1, 0, 0), (0, 1, 0),), (-90, 0, 0),), + (((3, 5, 6), (0, 1, 0), (1, 0, 0),), (90, 0, 90),), # JUst xDir, but with multiple vector components - (((3, 5, 6), (1, 1, 0),),), - (((3, 5, 6), (1, 0, 1),),), - (((3, 5, 6), (0, 1, 1),),), + (((3, 5, 6), (1, 1, 0),), (0, 0, 45),), + (((3, 5, 6), (1, 0, 1),), (0, -45, 0),), + (((3, 5, 6), (0, 1, 1),), (0, -45, 90),), # Multiple components in xdir and normal - (((3, 5, 6), (1, 1, 0), (1, 0, 1),),), - (((3, 5, 6), (1, 1, 0), (0, 1, 1),),), - (((3, 5, 6), (1, 0, 1), (1, 1, 0),),), - (((3, 5, 6), (1, 0, 1), (0, 1, 1),),), - (((3, 5, 6), (0, 1, 1), (1, 1, 0),),), - (((3, 5, 6), (0, 1, 1), (1, 0, 1),),), + # Starting from here, there are no known golden Location rotations, + # as normal is made orthogonal to xDir and as rotational angles + # are non-trivial. + (((3, 5, 6), (1, 1, 0), (1, 0, 1),), None,), + (((3, 5, 6), (1, 1, 0), (0, 1, 1),), None,), + (((3, 5, 6), (1, 0, 1), (1, 1, 0),), None,), + (((3, 5, 6), (1, 0, 1), (0, 1, 1),), None,), + (((3, 5, 6), (0, 1, 1), (1, 1, 0),), None,), + (((3, 5, 6), (0, 1, 1), (1, 0, 1),), None,), # Same, but introduce negative directions - (((3, 5, 6), (-1, 1, 0), (-1, 0, -1),),), - (((3, 5, 6), (1, -1, 0), (0, -1, -1),),), - (((3, 5, 6), (1, 0, -1), (1, -1, 0),),), - (((3, 5, 6), (1, 0, -1), (0, -1, 1),),), - (((3, 5, 6), (0, -1, -1), (-1, 1, 0),),), - (((3, 5, 6), (0, -1, -1), (1, 0, -1),),), + (((3, 5, 6), (-1, 1, 0), (-1, 0, -1),), None,), + (((3, 5, 6), (1, -1, 0), (0, -1, -1),), None,), + (((3, 5, 6), (1, 0, -1), (1, -1, 0),), None,), + (((3, 5, 6), (1, 0, -1), (0, -1, 1),), None,), + (((3, 5, 6), (0, -1, -1), (-1, 1, 0),), None,), + (((3, 5, 6), (0, -1, -1), (1, 0, -1),), None,), # Vectors with random non-trivial directions - (((3, 5, 6), (2, 4, 7), (9, 8, 1),),), + (((3, 5, 6), (2, 4, 7), (9, 8, 1),), None,), ] ) -def test_Plane_from_Location(plargs, useproperty): +def test_Plane_from_Location(plargs, expectedrot, useproperty): # Test conversion between Plane and Location by converting multiple # times between them, such that two Plane and two Location can be # compared respectively. @@ -59,13 +64,21 @@ def test_Plane_from_Location(plargs, useproperty): # This here makes the normal orthogonal to xDir by subtracting its # projection on xDir. # If no normal is given, the default normal is assumed. - if len(plargs) == 2: - plargs = (*plargs, (0, 0, 1)) + # Packed and unpacked arguments to Plane are kept the same. + if len(plargs) == 1: + origin, = plargs + elif len(plargs) == 2: + plargs = (*plargs, (0, 0, 1),) + # If len(plargs) was 2, it is now 3, and the normal still needs to be + # made orthogonal to xDir. if len(plargs) == 3: origin, xDir, normal = plargs xDir = Vector(xDir) normal = Vector(normal) normal -= normal.projectToLine(xDir) + xDir = xDir.toTuple() + normal = normal.toTuple() + plargs = (origin, xDir, normal,) # Start from random Plane with classical __init__ # Use keyword arguments on purpose, as they still need to work after @@ -88,7 +101,7 @@ def test_Plane_from_Location(plargs, useproperty): locback = Location(plback) # Create raw locations, which are flat tuples of raw numbers, suitable for - # assertion with pytes.approx + # assertion with pytest.approx locraws = list() for loc in (locforth, locback): loc = loc.toTuple() @@ -107,6 +120,15 @@ def test_Plane_from_Location(plargs, useproperty): pl = tuple(itertools.chain(*pl)) plraws.append(pl) - # Perform assertions + # Assert the properties of the location object. + # Asserting on one Location is enough, as equality to the other one is + # asserted below. + # First, its origin shall be the same + assert locraws[0][0:3] == pytest.approx(origin) + # Then rotations are asserted from manual values + if expectedrot is not None: + assert locraws[0][3:6] == pytest.approx(expectedrot) + + # Assert that pairs of PLane or Location are equal after conversion assert locraws[0] == pytest.approx(locraws[1]) assert plraws[0] == pytest.approx(plraws[1]) From 1043dcfec98a19cef8547095b9ba6bd1078eaab8 Mon Sep 17 00:00:00 2001 From: Joschua-Conrad <32811308+Joschua-Conrad@users.noreply.github.com> Date: Fri, 10 Oct 2025 08:37:57 +0200 Subject: [PATCH 21/26] Re-ran black --- tests/test_geom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_geom.py b/tests/test_geom.py index 716f94f2b..5ba5f6a4a 100644 --- a/tests/test_geom.py +++ b/tests/test_geom.py @@ -66,7 +66,7 @@ def test_Plane_from_Location(plargs, expectedrot, useproperty): # If no normal is given, the default normal is assumed. # Packed and unpacked arguments to Plane are kept the same. if len(plargs) == 1: - origin, = plargs + (origin,) = plargs elif len(plargs) == 2: plargs = (*plargs, (0, 0, 1),) # If len(plargs) was 2, it is now 3, and the normal still needs to be From 7513047f4d33f2a58da1b510d3047b2dc16362b8 Mon Sep 17 00:00:00 2001 From: Joschua-Conrad <32811308+Joschua-Conrad@users.noreply.github.com> Date: Fri, 10 Oct 2025 09:58:32 +0200 Subject: [PATCH 22/26] Ran black, but this time with the correct CQ-custom black fork --- cadquery/occ_impl/geom.py | 5 +---- tests/test_geom.py | 19 +++++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/cadquery/occ_impl/geom.py b/cadquery/occ_impl/geom.py index 7e7eaf064..8da42d0e0 100644 --- a/cadquery/occ_impl/geom.py +++ b/cadquery/occ_impl/geom.py @@ -608,10 +608,7 @@ def __init__( self.origin = Vector(origin) @classmethod - def fromLocation( - cls, - loc: "Location", - ) -> "Plane": + def fromLocation(cls, loc: "Location",) -> "Plane": """Create a Plane from Location loc""" # Ask location for its information diff --git a/tests/test_geom.py b/tests/test_geom.py index 5ba5f6a4a..e25a77440 100644 --- a/tests/test_geom.py +++ b/tests/test_geom.py @@ -5,11 +5,7 @@ # Conversion can betriggered from explicit constructor, or from property @pytest.mark.parametrize( - ["useproperty",], - [ - (False,), - (True,), - ], + ["useproperty",], [(False,), (True,),], ) # Create different test cases from different initial plane. # Testing the different components is mainly useful for debugging if things @@ -52,7 +48,7 @@ (((3, 5, 6), (0, -1, -1), (1, 0, -1),), None,), # Vectors with random non-trivial directions (((3, 5, 6), (2, 4, 7), (9, 8, 1),), None,), - ] + ], ) def test_Plane_from_Location(plargs, expectedrot, useproperty): # Test conversion between Plane and Location by converting multiple @@ -68,7 +64,10 @@ def test_Plane_from_Location(plargs, expectedrot, useproperty): if len(plargs) == 1: (origin,) = plargs elif len(plargs) == 2: - plargs = (*plargs, (0, 0, 1),) + plargs = ( + *plargs, + (0, 0, 1), + ) # If len(plargs) was 2, it is now 3, and the normal still needs to be # made orthogonal to xDir. if len(plargs) == 3: @@ -78,7 +77,11 @@ def test_Plane_from_Location(plargs, expectedrot, useproperty): normal -= normal.projectToLine(xDir) xDir = xDir.toTuple() normal = normal.toTuple() - plargs = (origin, xDir, normal,) + plargs = ( + origin, + xDir, + normal, + ) # Start from random Plane with classical __init__ # Use keyword arguments on purpose, as they still need to work after From 198e32e98b9c869173a7f73daa9f06fc890a1b82 Mon Sep 17 00:00:00 2001 From: Joschua-Conrad <32811308+Joschua-Conrad@users.noreply.github.com> Date: Fri, 10 Oct 2025 10:34:34 +0200 Subject: [PATCH 23/26] Fixed mypy error --- cadquery/occ_impl/geom.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cadquery/occ_impl/geom.py b/cadquery/occ_impl/geom.py index 8da42d0e0..60b8e18fb 100644 --- a/cadquery/occ_impl/geom.py +++ b/cadquery/occ_impl/geom.py @@ -637,11 +637,11 @@ def fromLocation(cls, loc: "Location",) -> "Plane": # Apply rotation on vectors of the global plane # These vectors are already unit vectors and require no .normalized() - dirs = ((1, 0, 0), (0, 0, 1)) - dirs = (Vector(*i).transform(transformation) for i in dirs) + globaldirs = ((1, 0, 0), (0, 0, 1)) + localdirs = (Vector(*i).transform(transformation) for i in globaldirs) # Unpack vectors - xDir, normal = dirs + xDir, normal = localdirs # Construct a plane and return it pl = cls(origin=origin, xDir=xDir, normal=normal) From cd9961594730263d33c3eabda61090e2740e2214 Mon Sep 17 00:00:00 2001 From: Joschua-Conrad <32811308+Joschua-Conrad@users.noreply.github.com> Date: Tue, 21 Oct 2025 14:51:05 +0200 Subject: [PATCH 24/26] Changed new constructor from classmethod to multimethod --- cadquery/occ_impl/geom.py | 33 ++++++++++++++++++--------------- tests/test_geom.py | 2 +- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/cadquery/occ_impl/geom.py b/cadquery/occ_impl/geom.py index 60b8e18fb..a9e3e9dff 100644 --- a/cadquery/occ_impl/geom.py +++ b/cadquery/occ_impl/geom.py @@ -26,6 +26,8 @@ from OCP.TopLoc import TopLoc_Location from OCP.BinTools import BinTools_LocationSet +from multimethod import multidispatch + from ..types import Real from ..utils import multimethod @@ -576,20 +578,17 @@ def bottom(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)): plane._setPlaneDir(xDir) return plane + # Prefer multidispatch over multimethod, as that supports keyword + # arguments. These are in use, since Plane.__init__ has not always + # been a multimethod. + @multidispatch def __init__( self, origin: Union[Tuple[Real, Real, Real], Vector], xDir: Optional[Union[Tuple[Real, Real, Real], Vector]] = None, normal: Union[Tuple[Real, Real, Real], Vector] = (0, 0, 1), ): - """ - Create a Plane with an arbitrary orientation - - :param origin: the origin in global coordinates - :param xDir: an optional vector representing the xDirection. - :param normal: the normal direction for the plane - :raises ValueError: if the specified xDir is not orthogonal to the provided normal - """ + """Create a Plane from origin in global coordinates, vector xDir, and normal direction for the plane.""" zDir = Vector(normal) if zDir.Length == 0.0: raise ValueError("normal should be non null") @@ -607,9 +606,11 @@ def __init__( self._setPlaneDir(xDir) self.origin = Vector(origin) - @classmethod - def fromLocation(cls, loc: "Location",) -> "Plane": - """Create a Plane from Location loc""" + @__init__.register + def __init__( + self, loc: "Location", + ): + """Create a Plane from Location loc.""" # Ask location for its information origin, rotations = loc.toTuple() @@ -642,10 +643,12 @@ def fromLocation(cls, loc: "Location",) -> "Plane": # Unpack vectors xDir, normal = localdirs - # Construct a plane and return it - pl = cls(origin=origin, xDir=xDir, normal=normal) - return pl + # Apply attributes as in other constructor. + # Rememeber to set zDir before calling _setPlaneDir. + self.zDir = normal + self._setPlaneDir(xDir) + self.origin = origin def _eq_iter(self, other): """Iterator to successively test equality""" @@ -1151,7 +1154,7 @@ def toTuple(self) -> Tuple[Tuple[float, float, float], Tuple[float, float, float @property def plane(self) -> "Plane": - return Plane.fromLocation(self) + return Plane(self) def __getstate__(self) -> BytesIO: diff --git a/tests/test_geom.py b/tests/test_geom.py index e25a77440..a0252ae42 100644 --- a/tests/test_geom.py +++ b/tests/test_geom.py @@ -100,7 +100,7 @@ def test_Plane_from_Location(plargs, expectedrot, useproperty): locback = plback.location else: locforth = Location(originalpl) - plback = Plane.fromLocation(locforth) + plback = Plane(locforth) locback = Location(plback) # Create raw locations, which are flat tuples of raw numbers, suitable for From 9a1e32600465486004682dd086adcb8b0e74f64a Mon Sep 17 00:00:00 2001 From: Joschua-Conrad <32811308+Joschua-Conrad@users.noreply.github.com> Date: Wed, 22 Oct 2025 13:22:51 +0200 Subject: [PATCH 25/26] Fix typo in comment Co-authored-by: Jeremy Wright --- tests/test_geom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_geom.py b/tests/test_geom.py index a0252ae42..d21674fae 100644 --- a/tests/test_geom.py +++ b/tests/test_geom.py @@ -3,7 +3,7 @@ import itertools -# Conversion can betriggered from explicit constructor, or from property +# Conversion can be triggered from explicit constructor, or from property @pytest.mark.parametrize( ["useproperty",], [(False,), (True,),], ) From 8d8c2f5a6d890f30dc9e247af03e6375c80d873b Mon Sep 17 00:00:00 2001 From: Joschua-Conrad <32811308+Joschua-Conrad@users.noreply.github.com> Date: Wed, 22 Oct 2025 13:23:36 +0200 Subject: [PATCH 26/26] Fix typo in comment Co-authored-by: Jeremy Wright --- tests/test_geom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_geom.py b/tests/test_geom.py index d21674fae..4f69a271f 100644 --- a/tests/test_geom.py +++ b/tests/test_geom.py @@ -21,7 +21,7 @@ (((3, 5, 6), (1, 0, 0),), (0, 0, 0),), (((3, 5, 6), (0, 1, 0),), (0, 0, 90),), # xDir and normal specified. - # Omit normals, that were included as default, and once which + # Omit normals, that were included as default, and ones which # have no component orthogonal to xDir (((3, 5, 6), (1, 0, 0), (0, 1, 0),), (-90, 0, 0),), (((3, 5, 6), (0, 1, 0), (1, 0, 0),), (90, 0, 90),),