From cd66589e4e9a020d9dc9a446df100b026db0e44e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Urba=C5=84czyk?= Date: Thu, 5 Mar 2020 21:43:16 +0100 Subject: [PATCH] Implementation of BRepOffsetAPI_MakeFilling (#253) --- cadquery/cq.py | 84 ++++++++++++++++ cadquery/occ_impl/shapes.py | 183 ++++++++++++++++++++++++++++++++++ examples/Ex101_InterpPlate.py | 175 ++++++++++++++++++++++++++++++++ tests/__init__.py | 1 - tests/test_cadquery.py | 176 ++++++++++++++++++++++++++++++++ 5 files changed, 618 insertions(+), 1 deletion(-) create mode 100644 examples/Ex101_InterpPlate.py diff --git a/cadquery/cq.py b/cadquery/cq.py index 25dc1d3e..4fee00e6 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -2862,6 +2862,90 @@ class Workplane(CQ): return Compound.makeCompound(toFuse) + def interpPlate( + self, + surf_edges, + surf_pts=[], + thickness=0, + combine=False, + clean=True, + degree=3, + nbPtsOnCur=15, + nbIter=2, + anisotropy=False, + tol2d=0.00001, + tol3d=0.0001, + tolAng=0.01, + tolCurv=0.1, + maxDeg=8, + maxSegments=9, + ): + """ + Returns a plate surface that is 'thickness' thick, enclosed by 'surf_edge_pts' points, and going through 'surf_pts' points. Using pushpoints directly with interpPlate and combine=True, can be very ressources intensive depending on the complexity of the shape. In this case set combine=False. + + :param surf_edges + :type 1 surf_edges: list of [x,y,z] float ordered coordinates + :type 2 surf_edges: list of ordered or unordered CadQuery wires + :param surf_pts = [] (uses only edges if []) + :type surf_pts: list of [x,y,z] float coordinates + :param thickness = 0 (returns 2D surface if 0) + :type thickness: float (may be negative or positive depending on thicknening direction) + :param combine: should the results be combined with other solids on the stack + (and each other)? + :type combine: true to combine shapes, false otherwise. + :param boolean clean: call :py:meth:`clean` afterwards to have a clean shape + :param Degree = 3 (OCCT default) + :type Degree: Integer >= 2 + :param NbPtsOnCur = 15 (OCCT default) + :type: NbPtsOnCur Integer >= 15 + :param NbIter = 2 (OCCT default) + :type: NbIterInteger >= 2 + :param Anisotropie = False (OCCT default) + :type Anisotropie: Boolean + :param: Tol2d = 0.00001 (OCCT default) + :type Tol2d: float > 0 + :param Tol3d = 0.0001 (OCCT default) + :type Tol3dReal: float > 0 + :param TolAng = 0.01 (OCCT default) + :type TolAngReal: float > 0 + :param TolCurv = 0.1 (OCCT default) + :type TolCurvReal: float > 0 + :param MaxDeg = 8 (OCCT default) + :type MaxDegInteger: Integer >= 2 (?) + :param MaxSegments = 9 (OCCT default) + :type MaxSegments: Integer >= 2 (?) + """ + + # If thickness is 0, only a 2D surface will be returned. + if thickness == 0: + combine = False + + # Creates interpolated plate + def _makeplate(pnt): + return Solid.interpPlate( + surf_edges, + surf_pts, + thickness, + degree, + nbPtsOnCur, + nbIter, + anisotropy, + tol2d, + tol3d, + tolAng, + tolCurv, + maxDeg, + maxSegments, + ).translate(pnt) + + plates = self.eachpoint(_makeplate, True) + + # if combination is not desired, just return the created boxes + if not combine: + return plates + else: + return self.union(plates, clean=clean) + def box( self, length, diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index caa83f69..72c4e7b9 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -127,6 +127,14 @@ from OCC.Core.BRepFeat import BRepFeat_MakeDPrism from OCC.Core.BRepClass3d import BRepClass3d_SolidClassifier +from OCC.Core.GeomAbs import GeomAbs_C0 +from OCC.Extend.TopologyUtils import TopologyExplorer, WireExplorer +from OCC.Core.GeomAbs import GeomAbs_Intersection +from OCC.Core.BRepOffsetAPI import BRepOffsetAPI_MakeFilling +from OCC.Core.BRepOffset import BRepOffset_MakeOffset, BRepOffset_Skin +from OCC.Core.ShapeFix import ShapeFix_Wire + +import warnings from math import pi, sqrt from functools import reduce import warnings @@ -991,6 +999,73 @@ class Face(Shape): return [w for w in self.Wires() if not w.isSame(outer)] + @classmethod + def makeNSidedSurface( + cls, + edges, + points, + continuity=GeomAbs_C0, + degree=3, + nbPtsOnCur=15, + nbIter=2, + anisotropy=False, + tol2d=0.00001, + tol3d=0.0001, + tolAng=0.01, + tolCurv=0.1, + maxDeg=8, + maxSegments=9, + ): + """ + Returns a surface enclosed by a closed polygon defined by 'edges' and going through 'points'. + :param points + :type points: list of gp_Pnt + :param edges + :type edges: list of TopologyExplorer().edges() + :param continuity=GeomAbs_C0 + :type continuity: OCC.Core.GeomAbs continuity condition + :param Degree = 3 (OCCT default) + :type Degree: Integer >= 2 + :param NbPtsOnCur = 15 (OCCT default) + :type: NbPtsOnCur Integer >= 15 + :param NbIter = 2 (OCCT default) + :type: NbIterInteger >= 2 + :param Anisotropie = False (OCCT default) + :type Anisotropie: Boolean + :param: Tol2d = 0.00001 (OCCT default) + :type Tol2d: float > 0 + :param Tol3d = 0.0001 (OCCT default) + :type Tol3dReal: float > 0 + :param TolAng = 0.01 (OCCT default) + :type TolAngReal: float > 0 + :param TolCurv = 0.1 (OCCT default) + :type TolCurvReal: float > 0 + :param MaxDeg = 8 (OCCT default) + :type MaxDegInteger: Integer >= 2 (?) + :param MaxSegments = 9 (OCCT default) + :type MaxSegments: Integer >= 2 (?) + """ + + n_sided = BRepOffsetAPI_MakeFilling( + degree, + nbPtsOnCur, + nbIter, + anisotropy, + tol2d, + tol3d, + tolAng, + tolCurv, + maxDeg, + maxSegments, + ) + for edg in edges: + n_sided.Add(edg, continuity) + for pt in points: + n_sided.Add(pt) + n_sided.Build() + face = n_sided.Shape() + return cls.cast(face).fix() + @classmethod def makePlane(cls, length, width, basePnt=(0, 0, 0), dir=(0, 0, 1)): basePnt = Vector(basePnt) @@ -1165,6 +1240,114 @@ class Solid(Shape, Mixin3D): a single solid """ + @classmethod + def interpPlate( + cls, + surf_edges, + surf_pts, + thickness, + degree=3, + nbPtsOnCur=15, + nbIter=2, + anisotropy=False, + tol2d=0.00001, + tol3d=0.0001, + tolAng=0.01, + tolCurv=0.1, + maxDeg=8, + maxSegments=9, + ): + """ + Returns a plate surface that is 'thickness' thick, enclosed by 'surf_edge_pts' points, and going through 'surf_pts' points. + + :param surf_edges + :type 1 surf_edges: list of [x,y,z] float ordered coordinates + :type 2 surf_edges: list of ordered or unordered CadQuery wires + :param surf_pts = [] (uses only edges if []) + :type surf_pts: list of [x,y,z] float coordinates + :param thickness = 0 (returns 2D surface if 0) + :type thickness: float (may be negative or positive depending on thicknening direction) + :param Degree = 3 (OCCT default) + :type Degree: Integer >= 2 + :param NbPtsOnCur = 15 (OCCT default) + :type: NbPtsOnCur Integer >= 15 + :param NbIter = 2 (OCCT default) + :type: NbIterInteger >= 2 + :param Anisotropie = False (OCCT default) + :type Anisotropie: Boolean + :param: Tol2d = 0.00001 (OCCT default) + :type Tol2d: float > 0 + :param Tol3d = 0.0001 (OCCT default) + :type Tol3dReal: float > 0 + :param TolAng = 0.01 (OCCT default) + :type TolAngReal: float > 0 + :param TolCurv = 0.1 (OCCT default) + :type TolCurvReal: float > 0 + :param MaxDeg = 8 (OCCT default) + :type MaxDegInteger: Integer >= 2 (?) + :param MaxSegments = 9 (OCCT default) + :type MaxSegments: Integer >= 2 (?) + """ + + # POINTS CONSTRAINTS: list of (x,y,z) points, optional. + pts_array = [gp_Pnt(*pt) for pt in surf_pts] + + # EDGE CONSTRAINTS + # If a list of wires is provided, make a closed wire + if not isinstance(surf_edges, list): + surf_edges = [o.vals()[0] for o in surf_edges.all()] + surf_edges = Wire.assembleEdges(surf_edges) + w = surf_edges.wrapped + + # If a list of (x,y,z) points provided, build closed polygon + if isinstance(surf_edges, list): + e_array = [Vector(*e) for e in surf_edges] + wire_builder = BRepBuilderAPI_MakePolygon() + for e in e_array: # Create polygon from edges + wire_builder.Add(e.toPnt()) + wire_builder.Close() + w = wire_builder.Wire() + + edges = [i for i in TopologyExplorer(w).edges()] + + # MAKE SURFACE + continuity = GeomAbs_C0 # Fixed, changing to anything else crashes. + face = Face.makeNSidedSurface( + edges, + pts_array, + continuity, + degree, + nbPtsOnCur, + nbIter, + anisotropy, + tol2d, + tol3d, + tolAng, + tolCurv, + maxDeg, + maxSegments, + ) + + # THICKEN SURFACE + if ( + abs(thickness) > 0 + ): # abs() because negative values are allowed to set direction of thickening + solid = BRepOffset_MakeOffset() + solid.Initialize( + face.wrapped, + thickness, + 1.0e-5, + BRepOffset_Skin, + False, + False, + GeomAbs_Intersection, + True, + ) # The last True is important to make solid + solid.MakeOffsetShape() + return cls(solid.Shape()) + else: # Return 2D surface only + return face + @classmethod def isSolid(cls, obj): """ diff --git a/examples/Ex101_InterpPlate.py b/examples/Ex101_InterpPlate.py new file mode 100644 index 00000000..aa7db4cd --- /dev/null +++ b/examples/Ex101_InterpPlate.py @@ -0,0 +1,175 @@ +from math import sin, cos, pi, sqrt +import cadquery as cq + +# TEST_1 +# example from PythonOCC core_geometry_geomplate.py, use of thickness = 0 returns 2D surface. +thickness = 0 +edge_points = [[0.0, 0.0, 0.0], [0.0, 10.0, 0.0], [0.0, 10.0, 10.0], [0.0, 0.0, 10.0]] +surface_points = [[5.0, 5.0, 5.0]] +plate_0 = cq.Workplane("XY").interpPlate(edge_points, surface_points, thickness) +print("plate_0.val().Volume() = ", plate_0.val().Volume()) +plate_0 = plate_0.translate((0, 6 * 12, 0)) +show_object(plate_0) + +# EXAMPLE 1 +# Plate with 5 sides and 2 bumps, one side is not co-planar with the other sides +thickness = 0.1 +edge_points = [ + [-7.0, -7.0, 0.0], + [-3.0, -10.0, 3.0], + [7.0, -7.0, 0.0], + [7.0, 7.0, 0.0], + [-7.0, 7.0, 0.0], +] +edge_wire = cq.Workplane("XY").polyline( + [(-7.0, -7.0), (7.0, -7.0), (7.0, 7.0), (-7.0, 7.0)] +) +# edge_wire = edge_wire.add(cq.Workplane("YZ").workplane().transformed(offset=cq.Vector(0, 0, -7), rotate=cq.Vector(45, 0, 0)).polyline([(-7.,0.), (3,-3), (7.,0.)])) +# In CadQuery Sept-2019 it worked with rotate=cq.Vector(0, 45, 0). In CadQuery Dec-2019 rotate=cq.Vector(45, 0, 0) only closes the wire. +edge_wire = edge_wire.add( + cq.Workplane("YZ") + .workplane() + .transformed(offset=cq.Vector(0, 0, -7), rotate=cq.Vector(45, 0, 0)) + .spline([(-7.0, 0.0), (3, -3), (7.0, 0.0)]) +) +surface_points = [[-3.0, -3.0, -3.0], [3.0, 3.0, 3.0]] +plate_1 = cq.Workplane("XY").interpPlate(edge_wire, surface_points, thickness) +# plate_1 = cq.Workplane("XY").interpPlate(edge_points, surface_points, thickness) # list of (x,y,z) points instead of wires for edges +print("plate_1.val().Volume() = ", plate_1.val().Volume()) +show_object(plate_1) + +# EXAMPLE 2 +# Embossed star, need to change optional parameters to obtain nice looking result. +r1 = 3.0 +r2 = 10.0 +fn = 6 +thickness = 0.1 +edge_points = [ + [r1 * cos(i * pi / fn), r1 * sin(i * pi / fn)] + if i % 2 == 0 + else [r2 * cos(i * pi / fn), r2 * sin(i * pi / fn)] + for i in range(2 * fn + 1) +] +edge_wire = cq.Workplane("XY").polyline(edge_points) +r2 = 4.5 +surface_points = [ + [r2 * cos(i * pi / fn), r2 * sin(i * pi / fn), 1.0] for i in range(2 * fn) +] + [[0.0, 0.0, -2.0]] +plate_2 = cq.Workplane("XY").interpPlate( + edge_wire, + surface_points, + thickness, + combine=True, + clean=True, + degree=3, + nbPtsOnCur=15, + nbIter=2, + anisotropy=False, + tol2d=0.00001, + tol3d=0.0001, + tolAng=0.01, + tolCurv=0.1, + maxDeg=8, + maxSegments=49, +) +# plate_2 = cq.Workplane("XY").interpPlate(edge_points, surface_points, thickness, combine=True, clean=True, Degree=3, NbPtsOnCur=15, NbIter=2, Anisotropie=False, Tol2d=0.00001, Tol3d=0.0001, TolAng=0.01, TolCurv=0.1, MaxDeg=8, MaxSegments=49) # list of (x,y,z) points instead of wires for edges +print("plate_2.val().Volume() = ", plate_2.val().Volume()) +plate_2 = plate_2.translate((0, 2 * 12, 0)) +show_object(plate_2) + +# EXAMPLE 3 +# Points on hexagonal pattern coordinates, use of pushpoints. +r1 = 1.0 +N = 3 +ca = cos(30.0 * pi / 180.0) +sa = sin(30.0 * pi / 180.0) +# EVEN ROWS +pts = [ + (-3.0, -3.0), + (-1.267949, -3.0), + (0.464102, -3.0), + (2.196152, -3.0), + (-3.0, 0.0), + (-1.267949, 0.0), + (0.464102, 0.0), + (2.196152, 0.0), + (-2.133974, -1.5), + (-0.401923, -1.5), + (1.330127, -1.5), + (3.062178, -1.5), + (-2.133975, 1.5), + (-0.401924, 1.5), + (1.330127, 1.5), + (3.062178, 1.5), +] +# Spike surface +thickness = 0.1 +fn = 6 +edge_points = [ + [ + r1 * cos(i * 2 * pi / fn + 30 * pi / 180), + r1 * sin(i * 2 * pi / fn + 30 * pi / 180), + ] + for i in range(fn + 1) +] +surface_points = [ + [ + r1 / 4 * cos(i * 2 * pi / fn + 30 * pi / 180), + r1 / 4 * sin(i * 2 * pi / fn + 30 * pi / 180), + 0.75, + ] + for i in range(fn + 1) +] + [[0, 0, 2]] +edge_wire = cq.Workplane("XY").polyline(edge_points) +plate_3 = ( + cq.Workplane("XY") + .pushPoints(pts) + .interpPlate( + edge_wire, + surface_points, + thickness, + combine=False, + clean=False, + degree=2, + nbPtsOnCur=20, + nbIter=2, + anisotropy=False, + tol2d=0.00001, + tol3d=0.0001, + tolAng=0.01, + tolCurv=0.1, + maxDeg=8, + maxSegments=9, + ) +) +print("plate_3.val().Volume() = ", plate_3.val().Volume()) +plate_3 = plate_3.translate((0, 4 * 11, 0)) +show_object(plate_3) + +# EXAMPLE 4 +# Gyroïd, all edges are splines on different workplanes. +thickness = 0.1 +edge_points = [ + [[3.54, 3.54], [1.77, 0.0], [3.54, -3.54]], + [[-3.54, -3.54], [0.0, -1.77], [3.54, -3.54]], + [[-3.54, -3.54], [0.0, -1.77], [3.54, -3.54]], + [[-3.54, -3.54], [-1.77, 0.0], [-3.54, 3.54]], + [[3.54, 3.54], [0.0, 1.77], [-3.54, 3.54]], + [[3.54, 3.54], [0.0, 1.77], [-3.54, 3.54]], +] +plane_list = ["XZ", "XY", "YZ", "XZ", "YZ", "XY"] +offset_list = [-3.54, 3.54, 3.54, 3.54, -3.54, -3.54] +edge_wire = ( + cq.Workplane(plane_list[0]).workplane(offset=-offset_list[0]).spline(edge_points[0]) +) +for i in range(len(edge_points) - 1): + edge_wire = edge_wire.add( + cq.Workplane(plane_list[i + 1]) + .workplane(offset=-offset_list[i + 1]) + .spline(edge_points[i + 1]) + ) +surface_points = [[0, 0, 0]] +plate_4 = cq.Workplane("XY").interpPlate(edge_wire, surface_points, thickness) +print("plate_4.val().Volume() = ", plate_4.val().Volume()) +plate_4 = plate_4.translate((0, 5 * 12, 0)) +show_object(plate_4) diff --git a/tests/__init__.py b/tests/__init__.py index b36542e9..42ac9093 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -59,5 +59,4 @@ __all__ = [ "TestImporters", "TestJupyter", "TestWorkplanes", - "TestAssembleEdges", ] diff --git a/tests/test_cadquery.py b/tests/test_cadquery.py index 88a17e84..f50f8982 100644 --- a/tests/test_cadquery.py +++ b/tests/test_cadquery.py @@ -2877,3 +2877,179 @@ class TestCadQuery(BaseTest): result2 = result2.newObject([Compound.makeCompound(result2.objects)]) self.assertEqual(1, result2.compounds().size()) self.assertEqual(0, result2.compounds(tag="4 objs").size()) + + def test_interpPlate(self): + """ + Tests the interpPlate() functionnalites + Numerical values of Areas and Volumes were obtained with the Area() and Volume() functions on a Linux machine under Debian 10 with python 3.7. + """ + + # example from PythonOCC core_geometry_geomplate.py, use of thickness = 0 returns 2D surface. + thickness = 0 + edge_points = [ + [0.0, 0.0, 0.0], + [0.0, 10.0, 0.0], + [0.0, 10.0, 10.0], + [0.0, 0.0, 10.0], + ] + surface_points = [[5.0, 5.0, 5.0]] + plate_0 = Workplane("XY").interpPlate(edge_points, surface_points, thickness) + self.assertTrue(plate_0.val().isValid()) + self.assertAlmostEqual(plate_0.val().Area(), 141.218823892, 1) + + # Plate with 5 sides and 2 bumps, one side is not co-planar with the other sides + thickness = 0.1 + edge_points = [ + [-7.0, -7.0, 0.0], + [-3.0, -10.0, 3.0], + [7.0, -7.0, 0.0], + [7.0, 7.0, 0.0], + [-7.0, 7.0, 0.0], + ] + edge_wire = Workplane("XY").polyline( + [(-7.0, -7.0), (7.0, -7.0), (7.0, 7.0), (-7.0, 7.0)] + ) + # edge_wire = edge_wire.add(Workplane('YZ').workplane().transformed(offset=Vector(0, 0, -7), rotate=Vector(45, 0, 0)).polyline([(-7.,0.), (3,-3), (7.,0.)])) + # In CadQuery Sept-2019 it worked with rotate=Vector(0, 45, 0). In CadQuery Dec-2019 rotate=Vector(45, 0, 0) only closes the wire. + edge_wire = edge_wire.add( + Workplane("YZ") + .workplane() + .transformed(offset=Vector(0, 0, -7), rotate=Vector(45, 0, 0)) + .spline([(-7.0, 0.0), (3, -3), (7.0, 0.0)]) + ) + surface_points = [[-3.0, -3.0, -3.0], [3.0, 3.0, 3.0]] + plate_1 = Workplane("XY").interpPlate(edge_wire, surface_points, thickness) + self.assertTrue(plate_1.val().isValid()) + self.assertAlmostEqual(plate_1.val().Volume(), 26.124970206, 3) + + # Embossed star, need to change optional parameters to obtain nice looking result. + r1 = 3.0 + r2 = 10.0 + fn = 6 + thickness = 0.1 + edge_points = [ + [r1 * math.cos(i * math.pi / fn), r1 * math.sin(i * math.pi / fn)] + if i % 2 == 0 + else [r2 * math.cos(i * math.pi / fn), r2 * math.sin(i * math.pi / fn)] + for i in range(2 * fn + 1) + ] + edge_wire = Workplane("XY").polyline(edge_points) + r2 = 4.5 + surface_points = [ + [r2 * math.cos(i * math.pi / fn), r2 * math.sin(i * math.pi / fn), 1.0] + for i in range(2 * fn) + ] + [[0.0, 0.0, -2.0]] + plate_2 = Workplane("XY").interpPlate( + edge_wire, + surface_points, + thickness, + combine=True, + clean=True, + degree=3, + nbPtsOnCur=15, + nbIter=2, + anisotropy=False, + tol2d=0.00001, + tol3d=0.0001, + tolAng=0.01, + tolCurv=0.1, + maxDeg=8, + maxSegments=49, + ) + self.assertTrue(plate_2.val().isValid()) + self.assertAlmostEqual(plate_2.val().Volume(), 10.956054314, 0) + + # Points on hexagonal pattern coordinates, use of pushpoints. + r1 = 1.0 + N = 3 + ca = math.cos(30.0 * math.pi / 180.0) + sa = math.sin(30.0 * math.pi / 180.0) + # EVEN ROWS + pts = [ + (-3.0, -3.0), + (-1.267949, -3.0), + (0.464102, -3.0), + (2.196152, -3.0), + (-3.0, 0.0), + (-1.267949, 0.0), + (0.464102, 0.0), + (2.196152, 0.0), + (-2.133974, -1.5), + (-0.401923, -1.5), + (1.330127, -1.5), + (3.062178, -1.5), + (-2.133975, 1.5), + (-0.401924, 1.5), + (1.330127, 1.5), + (3.062178, 1.5), + ] + # Spike surface + thickness = 0.1 + fn = 6 + edge_points = [ + [ + r1 * math.cos(i * 2 * math.pi / fn + 30 * math.pi / 180), + r1 * math.sin(i * 2 * math.pi / fn + 30 * math.pi / 180), + ] + for i in range(fn + 1) + ] + surface_points = [ + [ + r1 / 4 * math.cos(i * 2 * math.pi / fn + 30 * math.pi / 180), + r1 / 4 * math.sin(i * 2 * math.pi / fn + 30 * math.pi / 180), + 0.75, + ] + for i in range(fn + 1) + ] + [[0, 0, 2]] + edge_wire = Workplane("XY").polyline(edge_points) + plate_3 = ( + Workplane("XY") + .pushPoints(pts) + .interpPlate( + edge_wire, + surface_points, + thickness, + combine=False, + clean=False, + degree=2, + nbPtsOnCur=20, + nbIter=2, + anisotropy=False, + tol2d=0.00001, + tol3d=0.0001, + tolAng=0.01, + tolCurv=0.1, + maxDeg=8, + maxSegments=9, + ) + ) + self.assertTrue(plate_3.val().isValid()) + self.assertAlmostEqual(plate_3.val().Volume(), 0.45893954685189414, 1) + + # Gyroïd, all edges are splines on different workplanes. + thickness = 0.1 + edge_points = [ + [[3.54, 3.54], [1.77, 0.0], [3.54, -3.54]], + [[-3.54, -3.54], [0.0, -1.77], [3.54, -3.54]], + [[-3.54, -3.54], [0.0, -1.77], [3.54, -3.54]], + [[-3.54, -3.54], [-1.77, 0.0], [-3.54, 3.54]], + [[3.54, 3.54], [0.0, 1.77], [-3.54, 3.54]], + [[3.54, 3.54], [0.0, 1.77], [-3.54, 3.54]], + ] + plane_list = ["XZ", "XY", "YZ", "XZ", "YZ", "XY"] + offset_list = [-3.54, 3.54, 3.54, 3.54, -3.54, -3.54] + edge_wire = ( + Workplane(plane_list[0]) + .workplane(offset=-offset_list[0]) + .spline(edge_points[0]) + ) + for i in range(len(edge_points) - 1): + edge_wire = edge_wire.add( + Workplane(plane_list[i + 1]) + .workplane(offset=-offset_list[i + 1]) + .spline(edge_points[i + 1]) + ) + surface_points = [[0, 0, 0]] + plate_4 = Workplane("XY").interpPlate(edge_wire, surface_points, thickness) + self.assertTrue(plate_4.val().isValid()) + self.assertAlmostEqual(plate_4.val().Volume(), 7.760559490, 3)