From 5ee7f17df9573792de73dbb085ab67ce75854ca8 Mon Sep 17 00:00:00 2001 From: Oleksandr Kolodkin Date: Wed, 1 May 2024 20:51:40 +0300 Subject: [PATCH 1/3] Fix random face selection on chamfer Add test for chamfer --- cadquery/cq.py | 26 +++++++++-- cadquery/occ_impl/shapes.py | 62 ++++++++++++++++++-------- tests/__init__.py | 1 + tests/test_chamfer.py | 86 +++++++++++++++++++++++++++++++++++++ 4 files changed, 152 insertions(+), 23 deletions(-) create mode 100644 tests/test_chamfer.py diff --git a/cadquery/cq.py b/cadquery/cq.py index 3ecc932f1..64c1e58ee 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -1320,6 +1320,7 @@ def chamfer(self: T, length: float, length2: Optional[float] = None) -> T: :param length2: optional parameter for asymmetrical chamfer :raises ValueError: if at least one edge is not selected :raises ValueError: if the solid containing the edge is not in the chain + :raises ValueError: if the asymmetric mode and can't find face in stack :returns: CQ object with the resulting solid selected. This example will create a unit cube, with the top edges chamfered:: @@ -1332,12 +1333,29 @@ def chamfer(self: T, length: float, length2: Optional[float] = None) -> T: """ solid = self.findSolid() - edgeList = cast(List[Edge], self.edges().vals()) - if len(edgeList) < 1: + edges_list = cast(List[Edge], self.edges().vals()) + if len(edges_list) < 1: raise ValueError("Chamfer requires that edges be selected") - s = solid.chamfer(length, length2, edgeList) - + def find_faces(x: T) -> List[Face]: + faces = [] + for o in x.objects: + # filter faces + if isinstance(o, Face): + faces.append(cast(Face, o)) + # break if reached deeper stack level + elif any(isinstance(o, t) for t in [Solid, Compound]): + return [] + + # return found faces + if len(faces) > 0: + return faces + + # if faces not fond go to the next stack level + return find_faces(x.end()) + + faces_list = find_faces(self) + s = solid.chamfer(length, length2, edges_list, cast(List[Face], faces_list)) return self.newObject([s]) def transformed( diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index ec9a760c8..2ae1a319b 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -2888,39 +2888,63 @@ def fillet(self: Any, radius: float, edgeList: Iterable[Edge]) -> Any: return self.__class__(fillet_builder.Shape()) def chamfer( - self: Any, length: float, length2: Optional[float], edgeList: Iterable[Edge] + self: Any, length: float, length2: Optional[float], edgeList: Iterable[Edge], facesList: Iterable[Face] ) -> Any: """ Chamfers the specified edges of this solid. :param length: length > 0, the length (length) of the chamfer :param length2: length2 > 0, optional parameter for asymmetrical chamfer. Should be `None` if not required. - :param edgeList: a list of Edge objects, which must belong to this solid + :param edgeList: a list of Edge objects, which must belong to this solid + :param facesList: a list of Face objects, which must belong to this solid + :raises ValueError: if the asymmetric mode and can't find face in stack :return: Chamfered solid """ - nativeEdges = [e.wrapped for e in edgeList] + native_edges = [e.wrapped for e in edgeList] + native_faces = [f.wrapped for f in facesList] + + # note: we prefer 'length' word to 'radius' as opposed to FreeCAD's API + chamfer_builder = BRepFilletAPI_MakeChamfer(self.wrapped) + + # note: finding right face is very simple if only one fase selected + if len(native_faces) == 1: + for edge in native_edges: + chamfer_builder.Add(length, length2 or length, edge, TopoDS.Face_s(native_faces[0])) + return self.__class__(chamfer_builder.Shape()) + + if (len(native_faces) == 0) and (length2 is not None): + raise ValueError("If chamber length2 not None edges must be selected on faces") # make a edge --> faces mapping edge_face_map = TopTools_IndexedDataMapOfShapeListOfShape() - TopExp.MapShapesAndAncestors_s( - self.wrapped, ta.TopAbs_EDGE, ta.TopAbs_FACE, edge_face_map - ) + TopExp.MapShapesAndAncestors_s(self.wrapped, ta.TopAbs_EDGE, ta.TopAbs_FACE, edge_face_map) - # note: we prefer 'length' word to 'radius' as opposed to FreeCAD's API - chamfer_builder = BRepFilletAPI_MakeChamfer(self.wrapped) + # note: if edges selected directly + if len(native_faces) == 0: + for edge in native_edges: + face = edge_face_map.FindFromKey(edge).First() + chamfer_builder.Add(length, length, edge, TopoDS.Face_s(face)) + return self.__class__(chamfer_builder.Shape()) - if length2: - d1 = length - d2 = length2 - else: - d1 = length - d2 = length + # note: selected multiple faces + # we need determinate face for each edge + + for edge in native_edges: + faces = edge_face_map.FindFromKey(edge) + edge_selected_faces = [] + for face in native_faces: + if any(face.IsSame(f) for f in faces): + edge_selected_faces.append(face) + + if len(edge_selected_faces) == 0: + raise ValueError("Unexpected error. Faces selected but dont contain current edge.") + + elif len(edge_selected_faces) == 1: + chamfer_builder.Add(length, length2 or length, edge, TopoDS.Face_s(edge_selected_faces[0])) + + else: + chamfer_builder.Add(length2 or length, length2 or length, edge, TopoDS.Face_s(edge_selected_faces[0])) - for e in nativeEdges: - face = edge_face_map.FindFromKey(e).First() - chamfer_builder.Add( - d1, d2, e, TopoDS.Face_s(face) - ) # NB: edge_face_map return a generic TopoDS_Shape return self.__class__(chamfer_builder.Shape()) def shell( diff --git a/tests/__init__.py b/tests/__init__.py index 81be64fb0..25451dc0f 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -62,4 +62,5 @@ def assertTupleAlmostEquals(self, expected, actual, places, msg=None): "TestImporters", "TestJupyter", "TestWorkplanes", + "TestChamfer" ] diff --git a/tests/test_chamfer.py b/tests/test_chamfer.py new file mode 100644 index 000000000..8db325876 --- /dev/null +++ b/tests/test_chamfer.py @@ -0,0 +1,86 @@ +from unittest import TestCase, main +from typing import Optional, cast +from cadquery import Workplane +from cadquery.occ_impl.shapes import Vertex +from math import atan, sqrt, fabs, degrees + + +class TestCase3D(TestCase): + + def assertAlmostEqualVertices(self, first: Workplane, second: Workplane, places: Optional[int] = None, msg: Optional[str] = None, delta: Optional[float] = None): + first = sorted([cast(Vertex, x).toTuple() for x in first.vertices().objects]) + second = sorted([cast(Vertex, x).toTuple() for x in second.vertices().objects]) + self.assertEqual(len(first), len(second)) + + # print('['+', '.join([str(x) for x in first])+']') + # print('['+', '.join([str(x) for x in second])+']') + + for f, s in zip(first, second): + distance = fabs(sqrt((f[0] - s[0]) ** 2 + (f[1] - s[1]) ** 2 + (f[2] - s[2]) ** 2)) + msg = msg or f'{f} != {s} with distance {distance}' + self.assertAlmostEqual(distance, 0.0, places=places, msg=msg, delta=delta) + + +class TestChamfer(TestCase3D): + + def test_symmetric_chamfer_all(self): + obj1 = Workplane().box(10, 10, 10).chamfer(1) + obj2 = obj1.rotate((0, 0, 0), (1, 0, 0), 90) + self.assertAlmostEqualVertices(obj1, obj2) + + def test_asymmetric_chamfer_x_1(self): + obj1 = Workplane().box(10, 10, 10).faces("X").chamfer(1, 2) + obj2 = Workplane().box(8, 10, 10).faces(">X").workplane().rect(10, 10).extrude(2, taper=degrees(atan(0.5))).translate((-1, 0, 0)) + self.assertAlmostEqualVertices(obj1, obj2) + + def test_asymmetric_chamfer_y_1(self): + obj1 = Workplane().box(10, 10, 10).faces("Y").chamfer(1, 2) + obj2 = Workplane().box(10, 8, 10).faces(">Y").workplane().rect(10, 10).extrude(2, taper=degrees(atan(0.5))).translate((0, -1, 0)) + self.assertAlmostEqualVertices(obj1, obj2) + + def test_asymmetric_chamfer_z_1(self): + obj1 = Workplane().box(10, 10, 10).faces("Z").chamfer(1, 2) + obj2 = Workplane().box(10, 10, 8).faces(">Z").workplane().rect(10, 10).extrude(2, taper=degrees(atan(0.5))).translate((0, 0, -1)) + self.assertAlmostEqualVertices(obj1, obj2) + + def test_asymmetric_chamfer_xy_1(self): + obj1 = Workplane().box(10, 10, 10).faces(" Date: Wed, 1 May 2024 20:51:40 +0300 Subject: [PATCH 2/3] Fix random face selection on chamfer Add test for chamfer --- cadquery/cq.py | 26 ++++++++-- cadquery/occ_impl/shapes.py | 62 +++++++++++++++------- tests/__init__.py | 1 + tests/test_chamfer.py | 101 ++++++++++++++++++++++++++++++++++++ 4 files changed, 167 insertions(+), 23 deletions(-) create mode 100644 tests/test_chamfer.py diff --git a/cadquery/cq.py b/cadquery/cq.py index 3ecc932f1..64c1e58ee 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -1320,6 +1320,7 @@ def chamfer(self: T, length: float, length2: Optional[float] = None) -> T: :param length2: optional parameter for asymmetrical chamfer :raises ValueError: if at least one edge is not selected :raises ValueError: if the solid containing the edge is not in the chain + :raises ValueError: if the asymmetric mode and can't find face in stack :returns: CQ object with the resulting solid selected. This example will create a unit cube, with the top edges chamfered:: @@ -1332,12 +1333,29 @@ def chamfer(self: T, length: float, length2: Optional[float] = None) -> T: """ solid = self.findSolid() - edgeList = cast(List[Edge], self.edges().vals()) - if len(edgeList) < 1: + edges_list = cast(List[Edge], self.edges().vals()) + if len(edges_list) < 1: raise ValueError("Chamfer requires that edges be selected") - s = solid.chamfer(length, length2, edgeList) - + def find_faces(x: T) -> List[Face]: + faces = [] + for o in x.objects: + # filter faces + if isinstance(o, Face): + faces.append(cast(Face, o)) + # break if reached deeper stack level + elif any(isinstance(o, t) for t in [Solid, Compound]): + return [] + + # return found faces + if len(faces) > 0: + return faces + + # if faces not fond go to the next stack level + return find_faces(x.end()) + + faces_list = find_faces(self) + s = solid.chamfer(length, length2, edges_list, cast(List[Face], faces_list)) return self.newObject([s]) def transformed( diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index ec9a760c8..2ae1a319b 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -2888,39 +2888,63 @@ def fillet(self: Any, radius: float, edgeList: Iterable[Edge]) -> Any: return self.__class__(fillet_builder.Shape()) def chamfer( - self: Any, length: float, length2: Optional[float], edgeList: Iterable[Edge] + self: Any, length: float, length2: Optional[float], edgeList: Iterable[Edge], facesList: Iterable[Face] ) -> Any: """ Chamfers the specified edges of this solid. :param length: length > 0, the length (length) of the chamfer :param length2: length2 > 0, optional parameter for asymmetrical chamfer. Should be `None` if not required. - :param edgeList: a list of Edge objects, which must belong to this solid + :param edgeList: a list of Edge objects, which must belong to this solid + :param facesList: a list of Face objects, which must belong to this solid + :raises ValueError: if the asymmetric mode and can't find face in stack :return: Chamfered solid """ - nativeEdges = [e.wrapped for e in edgeList] + native_edges = [e.wrapped for e in edgeList] + native_faces = [f.wrapped for f in facesList] + + # note: we prefer 'length' word to 'radius' as opposed to FreeCAD's API + chamfer_builder = BRepFilletAPI_MakeChamfer(self.wrapped) + + # note: finding right face is very simple if only one fase selected + if len(native_faces) == 1: + for edge in native_edges: + chamfer_builder.Add(length, length2 or length, edge, TopoDS.Face_s(native_faces[0])) + return self.__class__(chamfer_builder.Shape()) + + if (len(native_faces) == 0) and (length2 is not None): + raise ValueError("If chamber length2 not None edges must be selected on faces") # make a edge --> faces mapping edge_face_map = TopTools_IndexedDataMapOfShapeListOfShape() - TopExp.MapShapesAndAncestors_s( - self.wrapped, ta.TopAbs_EDGE, ta.TopAbs_FACE, edge_face_map - ) + TopExp.MapShapesAndAncestors_s(self.wrapped, ta.TopAbs_EDGE, ta.TopAbs_FACE, edge_face_map) - # note: we prefer 'length' word to 'radius' as opposed to FreeCAD's API - chamfer_builder = BRepFilletAPI_MakeChamfer(self.wrapped) + # note: if edges selected directly + if len(native_faces) == 0: + for edge in native_edges: + face = edge_face_map.FindFromKey(edge).First() + chamfer_builder.Add(length, length, edge, TopoDS.Face_s(face)) + return self.__class__(chamfer_builder.Shape()) - if length2: - d1 = length - d2 = length2 - else: - d1 = length - d2 = length + # note: selected multiple faces + # we need determinate face for each edge + + for edge in native_edges: + faces = edge_face_map.FindFromKey(edge) + edge_selected_faces = [] + for face in native_faces: + if any(face.IsSame(f) for f in faces): + edge_selected_faces.append(face) + + if len(edge_selected_faces) == 0: + raise ValueError("Unexpected error. Faces selected but dont contain current edge.") + + elif len(edge_selected_faces) == 1: + chamfer_builder.Add(length, length2 or length, edge, TopoDS.Face_s(edge_selected_faces[0])) + + else: + chamfer_builder.Add(length2 or length, length2 or length, edge, TopoDS.Face_s(edge_selected_faces[0])) - for e in nativeEdges: - face = edge_face_map.FindFromKey(e).First() - chamfer_builder.Add( - d1, d2, e, TopoDS.Face_s(face) - ) # NB: edge_face_map return a generic TopoDS_Shape return self.__class__(chamfer_builder.Shape()) def shell( diff --git a/tests/__init__.py b/tests/__init__.py index 81be64fb0..25451dc0f 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -62,4 +62,5 @@ def assertTupleAlmostEquals(self, expected, actual, places, msg=None): "TestImporters", "TestJupyter", "TestWorkplanes", + "TestChamfer" ] diff --git a/tests/test_chamfer.py b/tests/test_chamfer.py new file mode 100644 index 000000000..e488ff2f5 --- /dev/null +++ b/tests/test_chamfer.py @@ -0,0 +1,101 @@ +from unittest import TestCase, main +from typing import Optional, Union, List, Tuple, cast +from cadquery import Workplane +from cadquery.occ_impl.shapes import Vertex +from math import atan, sqrt, fabs, degrees + + +class TestCase3D(TestCase): + + def assertAlmostEqualVertices( + self, + first: Workplane, + second: Union[Workplane, List[Tuple[float, float, float]]], + places: Optional[int] = None, + msg: Optional[str] = None, + delta: Optional[float] = None + ): + first = sorted([cast(Vertex, x).toTuple() for x in first.vertices().objects]) + if isinstance(second, Workplane): + second = sorted([cast(Vertex, x).toTuple() for x in second.vertices().objects]) + else: + second = sorted(second) + + # print('['+', '.join([str(x) for x in first])+']') + # print('['+', '.join([str(x) for x in second])+']') + + self.assertEqual(len(first), len(second)) + + for f, s in zip(first, second): + distance: float = fabs(sqrt((f[0] - s[0]) ** 2 + (f[1] - s[1]) ** 2 + (f[2] - s[2]) ** 2)) + message = msg or f'{f} != {s} with distance {distance}' + self.assertAlmostEqual(distance, 0.0, places=places, msg=message, delta=delta) + + +class TestChamfer(TestCase3D): + + def test_symmetric_chamfer_all(self): + obj1 = Workplane().box(10, 10, 10).chamfer(1) + obj2 = obj1.rotate((0, 0, 0), (1, 0, 0), 90) + self.assertAlmostEqualVertices(obj1, obj2) + + def test_asymmetric_chamfer_x_1(self): + obj1 = Workplane().box(10, 10, 10).faces("X").chamfer(1, 2) + obj2 = Workplane().box(8, 10, 10).faces(">X").workplane().rect(10, 10).extrude(2, taper=degrees(atan(0.5))).translate((-1, 0, 0)) + self.assertAlmostEqualVertices(obj1, obj2) + + def test_asymmetric_chamfer_y_1(self): + obj1 = Workplane().box(10, 10, 10).faces("Y").chamfer(1, 2) + obj2 = Workplane().box(10, 8, 10).faces(">Y").workplane().rect(10, 10).extrude(2, taper=degrees(atan(0.5))).translate((0, -1, 0)) + self.assertAlmostEqualVertices(obj1, obj2) + + def test_asymmetric_chamfer_z_1(self): + obj1 = Workplane().box(10, 10, 10).faces("Z").chamfer(1, 2) + obj2 = Workplane().box(10, 10, 8).faces(">Z").workplane().rect(10, 10).extrude(2, taper=degrees(atan(0.5))).translate((0, 0, -1)) + self.assertAlmostEqualVertices(obj1, obj2) + + def test_asymmetric_chamfer_xy_1(self): + obj1 = Workplane().box(10, 10, 10).faces(" Date: Thu, 2 May 2024 18:24:27 +0300 Subject: [PATCH 3/3] Format code with black --- cadquery/occ_impl/shapes.py | 36 ++++++-- tests/__init__.py | 2 +- tests/test_chamfer.py | 160 +++++++++++++++++++++++++++++------- 3 files changed, 159 insertions(+), 39 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 2ae1a319b..68da3122b 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -2888,7 +2888,11 @@ def fillet(self: Any, radius: float, edgeList: Iterable[Edge]) -> Any: return self.__class__(fillet_builder.Shape()) def chamfer( - self: Any, length: float, length2: Optional[float], edgeList: Iterable[Edge], facesList: Iterable[Face] + self: Any, + length: float, + length2: Optional[float], + edgeList: Iterable[Edge], + facesList: Iterable[Face], ) -> Any: """ Chamfers the specified edges of this solid. @@ -2909,15 +2913,21 @@ def chamfer( # note: finding right face is very simple if only one fase selected if len(native_faces) == 1: for edge in native_edges: - chamfer_builder.Add(length, length2 or length, edge, TopoDS.Face_s(native_faces[0])) + chamfer_builder.Add( + length, length2 or length, edge, TopoDS.Face_s(native_faces[0]) + ) return self.__class__(chamfer_builder.Shape()) if (len(native_faces) == 0) and (length2 is not None): - raise ValueError("If chamber length2 not None edges must be selected on faces") + raise ValueError( + "If chamber length2 not None edges must be selected on faces" + ) # make a edge --> faces mapping edge_face_map = TopTools_IndexedDataMapOfShapeListOfShape() - TopExp.MapShapesAndAncestors_s(self.wrapped, ta.TopAbs_EDGE, ta.TopAbs_FACE, edge_face_map) + TopExp.MapShapesAndAncestors_s( + self.wrapped, ta.TopAbs_EDGE, ta.TopAbs_FACE, edge_face_map + ) # note: if edges selected directly if len(native_faces) == 0: @@ -2937,13 +2947,25 @@ def chamfer( edge_selected_faces.append(face) if len(edge_selected_faces) == 0: - raise ValueError("Unexpected error. Faces selected but dont contain current edge.") + raise ValueError( + "Unexpected error. Faces selected but dont contain current edge." + ) elif len(edge_selected_faces) == 1: - chamfer_builder.Add(length, length2 or length, edge, TopoDS.Face_s(edge_selected_faces[0])) + chamfer_builder.Add( + length, + length2 or length, + edge, + TopoDS.Face_s(edge_selected_faces[0]), + ) else: - chamfer_builder.Add(length2 or length, length2 or length, edge, TopoDS.Face_s(edge_selected_faces[0])) + chamfer_builder.Add( + length2 or length, + length2 or length, + edge, + TopoDS.Face_s(edge_selected_faces[0]), + ) return self.__class__(chamfer_builder.Shape()) diff --git a/tests/__init__.py b/tests/__init__.py index 25451dc0f..346ff6db8 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -62,5 +62,5 @@ def assertTupleAlmostEquals(self, expected, actual, places, msg=None): "TestImporters", "TestJupyter", "TestWorkplanes", - "TestChamfer" + "TestChamfer", ] diff --git a/tests/test_chamfer.py b/tests/test_chamfer.py index e488ff2f5..50211fd76 100644 --- a/tests/test_chamfer.py +++ b/tests/test_chamfer.py @@ -4,20 +4,23 @@ from cadquery.occ_impl.shapes import Vertex from math import atan, sqrt, fabs, degrees +from cadquery.vis import show -class TestCase3D(TestCase): +class TestCase3D(TestCase): def assertAlmostEqualVertices( - self, - first: Workplane, - second: Union[Workplane, List[Tuple[float, float, float]]], - places: Optional[int] = None, - msg: Optional[str] = None, - delta: Optional[float] = None + self, + first: Workplane, + second: Union[Workplane, List[Tuple[float, float, float]]], + places: Optional[int] = None, + msg: Optional[str] = None, + delta: Optional[float] = None, ): first = sorted([cast(Vertex, x).toTuple() for x in first.vertices().objects]) if isinstance(second, Workplane): - second = sorted([cast(Vertex, x).toTuple() for x in second.vertices().objects]) + second = sorted( + [cast(Vertex, x).toTuple() for x in second.vertices().objects] + ) else: second = sorted(second) @@ -27,13 +30,16 @@ def assertAlmostEqualVertices( self.assertEqual(len(first), len(second)) for f, s in zip(first, second): - distance: float = fabs(sqrt((f[0] - s[0]) ** 2 + (f[1] - s[1]) ** 2 + (f[2] - s[2]) ** 2)) - message = msg or f'{f} != {s} with distance {distance}' - self.assertAlmostEqual(distance, 0.0, places=places, msg=message, delta=delta) + distance = fabs( + sqrt((f[0] - s[0]) ** 2 + (f[1] - s[1]) ** 2 + (f[2] - s[2]) ** 2) + ) + message = msg or f"{f} != {s} with distance {distance}" + self.assertAlmostEqual( + distance, 0.0, places=places, msg=message, delta=delta + ) class TestChamfer(TestCase3D): - def test_symmetric_chamfer_all(self): obj1 = Workplane().box(10, 10, 10).chamfer(1) obj2 = obj1.rotate((0, 0, 0), (1, 0, 0), 90) @@ -41,60 +47,152 @@ def test_symmetric_chamfer_all(self): def test_asymmetric_chamfer_x_1(self): obj1 = Workplane().box(10, 10, 10).faces("X").chamfer(1, 2) - obj2 = Workplane().box(8, 10, 10).faces(">X").workplane().rect(10, 10).extrude(2, taper=degrees(atan(0.5))).translate((-1, 0, 0)) + obj2 = ( + Workplane() + .box(8, 10, 10) + .faces(">X") + .workplane() + .rect(10, 10) + .extrude(2, taper=degrees(atan(0.5))) + .translate((-1, 0, 0)) + ) self.assertAlmostEqualVertices(obj1, obj2) def test_asymmetric_chamfer_y_1(self): obj1 = Workplane().box(10, 10, 10).faces("Y").chamfer(1, 2) - obj2 = Workplane().box(10, 8, 10).faces(">Y").workplane().rect(10, 10).extrude(2, taper=degrees(atan(0.5))).translate((0, -1, 0)) + obj2 = ( + Workplane() + .box(10, 8, 10) + .faces(">Y") + .workplane() + .rect(10, 10) + .extrude(2, taper=degrees(atan(0.5))) + .translate((0, -1, 0)) + ) self.assertAlmostEqualVertices(obj1, obj2) def test_asymmetric_chamfer_z_1(self): obj1 = Workplane().box(10, 10, 10).faces("Z").chamfer(1, 2) - obj2 = Workplane().box(10, 10, 8).faces(">Z").workplane().rect(10, 10).extrude(2, taper=degrees(atan(0.5))).translate((0, 0, -1)) + obj2 = ( + Workplane() + .box(10, 10, 8) + .faces(">Z") + .workplane() + .rect(10, 10) + .extrude(2, taper=degrees(atan(0.5))) + .translate((0, 0, -1)) + ) self.assertAlmostEqualVertices(obj1, obj2) def test_asymmetric_chamfer_xy_1(self): obj1 = Workplane().box(10, 10, 10).faces("