diff --git a/freecad/LaserCladdingWorkbench/commands.py b/freecad/LaserCladdingWorkbench/commands.py index 8dcf895..0917c70 100644 --- a/freecad/LaserCladdingWorkbench/commands.py +++ b/freecad/LaserCladdingWorkbench/commands.py @@ -53,11 +53,11 @@ class LCSelectBaseReference(): class LCCreatePad(): - def _create_laserpad(self, ref_to_face): - laserprogram = App.ActiveDocument.getObject('LaserProgram') - if laserprogram is None: - App.Console.PrintMessage('Create a LaserProgram first') - return + def _create_laserpad(self, laserprogram, ref_to_face): + #laserprogram = App.ActiveDocument.getObject('LaserProgram') + #if laserprogram is None: + # App.Console.PrintMessage('Create a LaserProgram first') + # return pad_obj = laserprogram.newObject("App::FeaturePython","LaserPad") LaserPad(pad_obj, ref_to_face) ViewProviderLaserPad(pad_obj.ViewObject) @@ -72,8 +72,29 @@ class LCCreatePad(): App.Console.PrintMessage('Select a Face') return # check length - ref_face = (Gui.Selection.getSelection()[0], - Gui.Selection.getSelectionEx()[0].SubElementNames[0]) + selection = Gui.Selection.getSelectionEx() + if len(selection) != 2: + App.Console.PrintMessage('Select a Face and a LaserProgram') + return + + # find the Program + prog = None + face = None + if selection[0].Object.Name.startswith('LaserProgram'): + prog = selection[0].Object + ref_face = (selection[1].Object, Gui.Selection.getSelectionEx()[1].SubElementNames[0]) + elif selection[1].Object.Name.startswith('LaserProgram'): + prog = selection[1].Object + ref_face = (selection[0].Object, Gui.Selection.getSelectionEx()[0].SubElementNames[0]) + else: + App.Console.PrintMessage('Please select a LaserProgram') + return + + #if program is None: + # App.Console.PrintMessage('Please select a LaserProgram') + # return + #ref_face = (Gui.Selection.getSelection()[0], + # Gui.Selection.getSelectionEx()[0].SubElementNames[0]) #selection = Gui.Selection.getSelectionEx() # find first vertex #for s in selection: @@ -81,7 +102,7 @@ class LCCreatePad(): # for obj in s.SubObjects: # if isinstance(obj, Part.Face): # face = obj.copy() - self._create_laserpad(ref_face) + self._create_laserpad(prog, ref_face) App.ActiveDocument.recompute() @@ -118,7 +139,16 @@ class LCSaveProg(): def Activated(self): # Here your write what your ScriptCmd does... App.Console.PrintMessage('Saving to KRL') - c = App.ActiveDocument.getObject("LaserProgram") + if not Gui.Selection.hasSelection(): + App.Console.PrintMessage('Select a Face') + return + selection = Gui.Selection.getSelectionEx() + c = None + if selection[0].Object.Name.startswith('LaserProgram'): + c = selection[0].Object + if c is None: + App.Console.PrintMessage('Selection is not a LaserProgram') + #c = App.ActiveDocument.getObject("LaserProgram") pads = c.Group prog = Kuka_Prog() prog.set_baseorigin(c.base_reference) @@ -128,7 +158,7 @@ class LCSaveProg(): prog.set_laser_power(c.laser_power) prog.set_laser_out(c.laser_real_out) prog.set_simulation(c.simulation) - + prog.set_label(c.Label) for pad in pads: # one pad with contours and hatchlines prog.create_layer() @@ -151,18 +181,24 @@ class LCSaveProg(): prog.append_contour(poses, pathtype) elif re.match('Hatch*', progpart.Name): face = pad.ref_body.getSubObject(pad.ref_surface) - edges = progpart.Shape.Edges - for edge in edges: - p0 = edge.Vertexes[0].Point - p1 = edge.Vertexes[1].Point + wires = progpart.Shape.Wires + print("Number Hatchlines: ", len(wires)) + counter = 0 + for wire in wires: line = [] - for p in [p0, p1]: - uv = face.Surface.parameter(p) - normal = face.normalAt(uv[0], uv[1]) - pose = Kuka_Pose.from_point_and_normal(p, normal) - line.append(pose) + for edge in wire.Edges: + p0 = edge.Vertexes[0].Point + p1 = edge.Vertexes[1].Point + for p in [p0, p1]: + uv = face.Surface.parameter(p) + normal = face.normalAt(uv[0], uv[1]) + pose = Kuka_Pose.from_point_and_normal(p, normal) + line.append(pose) + print("append line: ", counter) + counter += 1 prog.append_hatchline(line, progpart.pathtype) + #prog.save_prog(c.article, c.progpath) if c.templatename is None: templatename = "lasercladding" diff --git a/freecad/LaserCladdingWorkbench/kuka.py b/freecad/LaserCladdingWorkbench/kuka.py index 2026183..19f8ac0 100644 --- a/freecad/LaserCladdingWorkbench/kuka.py +++ b/freecad/LaserCladdingWorkbench/kuka.py @@ -41,6 +41,7 @@ class Kuka_Prog: self.inert_gas_out = 9 self.powder_out = 7 self.simulation = True + self.label = "REPLACEME" def set_baseorigin(self, vec): self.baseorigin = (vec.x, vec.y, vec.z) @@ -71,6 +72,9 @@ class Kuka_Prog: else: self.use_laser_out = self.laser_pilot_out + def set_label(self, label): + self.label = label + def create_layer(self): self.layers.append(Kuka_Layer()) self.current_layer += 1 @@ -84,6 +88,7 @@ class Kuka_Prog: layer = self.layers[self.current_layer] if not len(layer.hatchlines): layer.hatchlines.append((line, segmenttype)) + return # poses are sorted # but maybe we need to reverse # get the point distance from first and last pose @@ -110,12 +115,14 @@ class Kuka_Prog: return [FreeCAD.Base.Vector(p.X, p.Y, p.Z) for p in poses for poses in self.contour_path_list ] def save_with_template(self, article, path, templatename): + if article[0].isdigit(): + article = "_"+article if self.simulation: - filename_src = "kvt_{}_sim.src".format(article) - filename_dat = "kvt_{}_sim.dat".format(article) + filename_src = "{}_sim.src".format(article) + filename_dat = "{}_sim.dat".format(article) else: - filename_src = "kvt_{}.src".format(article) - filename_dat = "kvt_{}.dat".format(article) + filename_src = "{}.src".format(article) + filename_dat = "{}.dat".format(article) user_dir = FreeCAD.getUserAppDataDir() template_dir = os.path.join(user_dir, "Mod", "fc_lasercladding_wb", "freecad", "LaserCladdingWorkbench", "templates") @@ -152,26 +159,40 @@ class Kuka_Prog: # end of subroutine f.write(";- =============================\n") - f.write(";- Hatchlines\n") - f.write("$VEL.CP = TRAVELSPEED ; m/s ; m/s \n") - f.write("LIN_REL {Z 90.0} C_VEL; just move up \n") - (line, seg_type) = layer.hatchlines[0] - f.write("LIN refpose:{}:{} C_VEL; move to first hatch point but with z_up\n".format(line[0].translate_with(self.baseorigin).to_string(), z_up_pose)) - for (line, seg_type) in layer.hatchlines: - # start laser code - f.write(";- Hatchline\n") - if seg_type == 'LIN': - f.write("$VEL.CP = TRAVELSPEED ;m/s \n") - f.write("LIN refpose:{} C_VEL; GENERATED\n".format(line[0].translate_with(self.baseorigin).to_string())) - f.write("TRIGGER WHEN DISTANCE=0 DELAY=0 DO $OUT[%d]=TRUE; Turn on Laser at point \n" % self.use_laser_out) - f.write("$VEL.CP = WELDSPEED ; m/s \n") - f.write("LIN refpose:{} C_VEL; GENERATED\n".format(line[1].translate_with(self.baseorigin).to_string())) + if len(layer.hatchlines): + print("Number Hatchlines: ", len(layer.hatchlines)) + f.write(";- Hatchlines\n") + f.write("$VEL.CP = %f ; m/s ; m/s \n" % self.vmax) + f.write("LIN_REL {Z 90.0} C_VEL; just move up \n") + for (line, seg_type) in layer.hatchlines: + # a line has many segments + # start laser at first segment + # stop with last + f.write(";- Hatchline\n") + segment = line[0] + f.write("$VEL.CP = %f ; m/s ; m/s \n" % self.vmax) + f.write("LIN {}:{} C_VEL; move to first hatch point but with z_up\n".format(segment.translate_with(self.baseorigin).to_string(), z_up_pose)) + # One Hatchline + f.write(";- First Point in line \n") + f.write("LIN {} C_VEL; GENERATED\n".format(segment.translate_with(self.baseorigin).to_string())) + f.write("TRIGGER WHEN DISTANCE=0 DELAY=0 DO $OUT[%d]=True; Turn on Laser at point \n" % self.use_laser_out) + # each segment + for segment in line[1:-1]: + f.write("$VEL.CP = %f ; m/s ; m/s \n" % self.vproc) + f.write("LIN {} C_VEL; GENERATED\n".format(segment.translate_with(self.baseorigin).to_string())) + segment = line[-1] + f.write(";- Last Point in line \n") + f.write("LIN {} C_VEL; GENERATED\n".format(segment.translate_with(self.baseorigin).to_string())) f.write("TRIGGER WHEN DISTANCE=0 DELAY=0 DO $OUT[%d]=FALSE; Turn off Laser at point\n" % self.use_laser_out) + f.write(";- End line \n") + + f.write(";- ========= END LAYER =========\n") f.write(";- =============================\n") - # end of subroutine + # end of subroutine content = template_src.render( + simulation=self.simulation, artikel=article, tool=self.tool, base=self.base, @@ -181,12 +202,14 @@ class Kuka_Prog: laserpower=self.laser_power, vproc=self.vproc, vmax=self.vmax, - paths=f.getvalue() + paths=f.getvalue(), + label=self.label ) # dat template template_dat = environment.get_template(templatename+".dat") datcontent = template_dat.render( + simulation=self.simulation, artikel=article ) @@ -295,18 +318,25 @@ class Kuka_Prog: srcfile.write(";- Hatchlines\n") srcfile.write("$VEL.CP = %f ; m/s ; m/s \n" % self.vmax) srcfile.write("LIN_REL {Z 90.0} C_VEL; just move up \n") - (line, seg_type) = layer.hatchlines[0] - srcfile.write("LIN {}:{} C_VEL; move to first hatch point but with z_up\n".format(line[0].translate_with(self.baseorigin).to_string(), z_up_pose)) for (line, seg_type) in layer.hatchlines: - # start laser code + # a line has many segments + # start laser at first segment + # stop with last + segment = line[0] + srcfile.write("$VEL.CP = %f ; m/s ; m/s \n" % self.vmax) + srcfile.write("LIN {}:{} C_VEL; move to first hatch point but with z_up\n".format(segment.translate_with(self.baseorigin).to_string(), z_up_pose)) + # One Hatchline srcfile.write(";- Hatchline\n") - if seg_type == 'LIN': - srcfile.write("$VEL.CP = %f ; m/s ; m/s \n" % self.vmax) - srcfile.write("LIN {} C_VEL; GENERATED\n".format(line[0].translate_with(self.baseorigin).to_string())) - srcfile.write("TRIGGER WHEN DISTANCE=0 DELAY=0 DO $OUT[%d]=True; Turn on Laser at point \n" % self.use_laser_out) + srcfile.write("LIN {} C_VEL; GENERATED\n".format(segment.translate_with(self.baseorigin).to_string())) + srcfile.write("TRIGGER WHEN DISTANCE=0 DELAY=0 DO $OUT[%d]=True; Turn on Laser at point \n" % self.use_laser_out) + # each segment + for segment in line[1:-1]: srcfile.write("$VEL.CP = %f ; m/s ; m/s \n" % self.vproc) - srcfile.write("LIN {} C_VEL; GENERATED\n".format(line[1].translate_with(self.baseorigin).to_string())) - srcfile.write("TRIGGER WHEN DISTANCE=0 DELAY=0 DO $OUT[%d]=FALSE; Turn off Laser at point\n" % self.use_laser_out) + srcfile.write("LIN {} C_VEL; GENERATED\n".format(segment.translate_with(self.baseorigin).to_string())) + segment = line[-1] + srcfile.write("LIN {} C_VEL; GENERATED\n".format(segment.translate_with(self.baseorigin).to_string())) + srcfile.write("TRIGGER WHEN DISTANCE=0 DELAY=0 DO $OUT[%d]=FALSE; Turn off Laser at point\n" % self.use_laser_out) + srcfile.write(";- ========= END LAYER =========\n") srcfile.write(";- =============================\n") # end of subroutine diff --git a/freecad/LaserCladdingWorkbench/pad.py b/freecad/LaserCladdingWorkbench/pad.py index 111e724..b27865f 100644 --- a/freecad/LaserCladdingWorkbench/pad.py +++ b/freecad/LaserCladdingWorkbench/pad.py @@ -37,6 +37,9 @@ class LaserPad: obj.addProperty("App::PropertyFloat", "hatch_contour_offset", "Parameters", "Contour Offset") obj.hatch_contour_offset = 2.3 + obj.addProperty("App::PropertyBool", "hatching_enabled", "Parameters", "Hatch inner Region") + obj.hatching_enabled = True + obj.addProperty("App::PropertyFloat", "z_offset", "Parameters", "Height (Z) Offset") obj.z_offset = 0 @@ -46,6 +49,17 @@ class LaserPad: obj.addProperty("App::PropertyString","ref_surface","Reference","face") obj.ref_surface = face[1] + + obj.addProperty("App::PropertyBool","debug_polygons","Debugging","Show Polygons from shapely") + obj.debug_polygons = False + obj.addProperty("App::PropertyBool","debug_rdp","Debugging","Simplify polygons") + obj.debug_rdp = False + obj.addProperty("App::PropertyFloat","debug_rdp_epsilon","Debugging","Epsilon") + obj.debug_rdp_epsilon = 0.2 + + obj.addProperty("App::PropertyFloat","debug_discretization","Debugging","Discretization") + obj.debug_discretization = 2.3 + obj.addExtension("App::GroupExtensionPython") obj.Proxy = self self._create_path(obj) @@ -57,6 +71,7 @@ class LaserPad: # obj.removeObjectsFromDocument() face = obj.ref_body.getSubObject(obj.ref_surface) myHatcher = hatching.Hatcher() + myHatcher.hatchingEnabled = obj.hatching_enabled myHatcher.hatchDistance = obj.tool_width myHatcher.hatchAngle = obj.hatch_angle myHatcher.volumeOffsetHatch = obj.hatch_volume_offset @@ -66,17 +81,18 @@ class LaserPad: myHatcher.contourOffset = obj.hatch_contour_offset myHatcher.hatchSortMethod = hatching.AlternateSort() - polygon_coords = get_coords_from_shape(face) + polygon_coords = get_coords_from_shape(face, obj.debug_rdp_epsilon, obj.debug_discretization) print("===== coords from shape ======") print("{}", polygon_coords) print("===== ======") ## debug - # for poly in polygon_coords: - # c = Part.makeCompound([]) - # vertex_list = [App.Vector(x,y,0) for x,y in poly] - # c.add(Part.makePolygon(vertex_list)) - # Part.show(c) + if obj.debug_polygons: + for poly in polygon_coords: + c = Part.makeCompound([]) + vertex_list = [App.Vector(x,y,0) for x,y in poly] + c.add(Part.makePolygon(vertex_list)) + Part.show(c) layer = myHatcher.hatch(polygon_coords) @@ -102,8 +118,12 @@ class LaserPad: print("Creating Hatch Part") hatches_comp = obj.newObject("Part::Feature", "Hatchlines") p = Part.makeCompound([]) - for pc in proj_hatchlines: - p.add(Part.makeCompound(pc)) + for hatchline in proj_hatchlines: + # discretize by length + for line in hatchline: + wire_sections = line.Wires[0].discretize(Distance=3.0) + wire = Part.makePolygon(wire_sections) + p.add(wire) LaserPath(hatches_comp) hatches_comp.Shape = p diff --git a/freecad/LaserCladdingWorkbench/program.py b/freecad/LaserCladdingWorkbench/program.py index 03e230a..1f09c8c 100644 --- a/freecad/LaserCladdingWorkbench/program.py +++ b/freecad/LaserCladdingWorkbench/program.py @@ -23,10 +23,10 @@ class LaserProgram: obj.laser_real_out = 3 obj.addProperty("App::PropertyInteger", "laser_gas_out", "Laser Parameter", "Laser inert gas") - obj.laser_gas_out = 9 + obj.laser_gas_out = 7 obj.addProperty("App::PropertyInteger", "laser_powder_out", "Laser Parameter", "Laser Powder Bucket") - obj.laser_powder_out = 7 + obj.laser_powder_out = 8 obj.addProperty("App::PropertyFloat", "laser_feedrate", "Laser Parameter", "Process Velocity (Feedrate m/s)") obj.laser_feedrate = 0.022500 diff --git a/freecad/LaserCladdingWorkbench/templates/lasercladding.dat b/freecad/LaserCladdingWorkbench/templates/lasercladding.dat index 4e29181..846403c 100644 --- a/freecad/LaserCladdingWorkbench/templates/lasercladding.dat +++ b/freecad/LaserCladdingWorkbench/templates/lasercladding.dat @@ -2,7 +2,12 @@ &REL 47 &PARAM TEMPLATE = C:\KRC\Roboter\Template\ExpertVorgabe &PARAM EDITMASK = * -DEFDAT kvt_{{artikel}} PUBLIC +{%- if simulation %} +DEFDAT k{{artikel}}_sim PUBLIC +{%- else %} + DEFDAT k{{artikel}} PUBLIC +{%- endif %} + ;FOLD EXTERNAL DECLARATIONS;%{PE}%MKUKATPBASIS,%CEXT,%VCOMMON,%P ;FOLD BASISTECH EXT;%{PE}%MKUKATPBASIS,%CEXT,%VEXT,%P DECL INT SUCCESS diff --git a/freecad/LaserCladdingWorkbench/templates/lasercladding.src b/freecad/LaserCladdingWorkbench/templates/lasercladding.src index 14b9b78..1cfb2fa 100644 --- a/freecad/LaserCladdingWorkbench/templates/lasercladding.src +++ b/freecad/LaserCladdingWorkbench/templates/lasercladding.src @@ -2,9 +2,15 @@ &REL 1 &PARAM TEMPLATE = C:\KRC\Roboter\Template\ExpertVorgabe &PARAM EDITMASK = * -DEF kvt_{{artikel}}( ) - +{%- if simulation %} +DEF k{{artikel}}_sim( ) +{%- else %} +DEF k{{artikel}}( ) +{%- endif %} ;- Kuka src file, generated by FreeCAD LaserCladding WorkBench (by KVT) +;- Artikelnummer: {{artikel}} +;- Programm: {{label}} +;- Anweisungen .. E6POS refpose E6POS pulverstart @@ -12,6 +18,9 @@ REAL WELDSPEED REAL TRAVELSPEED REAL LASERPOWER INT POWDEROUT +REAL OFFSET_X +REAL OFFSET_Y +REAL OFFSET_Z ;------------- definitions ------------ EXT BAS (BAS_COMMAND :IN,REAL :IN ) ;set base to World @@ -40,6 +49,12 @@ WELDSPEED = {{vproc}}; m/s LASERPOWER = {{laserpower}} ; Set laser power POWDEROUT = {{powder_out}} +;- Offset for tweaking (in mm) +;- Only experts! +OFFSET_X = 0.0 ; mm +OFFSET_Y = 0.0 ; mm +OFFSET_Z = 0.0 ; mm + ;- Movement parameters $VEL.CP = TRAVELSPEED ; m/s ; m/s $APO.CDIS = 2.300000 ; mm @@ -49,32 +64,47 @@ $APO.CVEL = 95.000000 ; percent $ANOUT[1] = LASERPOWER ; Set laser power $OUT[{{laser_out}}] = FALSE ; Set Laser off $OUT[2] = TRUE ; Set Laser activation on -$OUT[POWDER_OUT] = FALSE ; Set powder on -$OUT[{{inert_gas_out}}] = FALSE ; Set inert gas on +$OUT[POWDEROUT] = FALSE ; Set powder off +$OUT[{{inert_gas_out}}] = FALSE ; Set inert gas off ;- Ab hier nicht mehr aendern! ;- Starting point refpose=$POS_ACT -pulverstart = {X -110.0, Y 0.0, Z 0.0, A 0.0000, B 0.0000, C 0.0000, E1 0.0000, E2 0.0000} +; zero out rotations +; because the base is already rectangular +refpose.A=0 +refpose.B=0 +refpose.C=0 +; Offset draufrechnen (fummelfaktor) +refpose.X=refpose.X+OFFSET_X +refpose.Y=refpose.Y+OFFSET_Y +refpose.Z=refpose.Z+OFFSET_Z + + +pulverstart = {X 130.0, Y 0.0, Z 0.0, A 0.0000, B 0.0000, C 0.0000, E1 0.0000, E2 0.0000} pulverstart.S = refpose.S pulverstart.T = refpose.T LIN refpose:pulverstart C_VEL; GENERATED -WAIT SEC 7.0 + $OUT[2] = TRUE ; Set Laser activation on +{% if simulation %} +$OUT[POWDEROUT] = FALSE ; OFF: SIMULATION Mode +$OUT[{{inert_gas_out}}] = FALSE ; OFF: SIMULATION Mode +{% else %} $OUT[POWDEROUT] = TRUE ; Set powder on $OUT[{{inert_gas_out}}] = TRUE ; Set inert gas on +{% endif %} +WAIT SEC 7.0 ;- ============================= ;- == generated poses == {{paths}} - - ;- ============================= $OUT[{{laser_out}}] = FALSE ; Set Laser off $OUT[2] = FALSE ; Set Laser activation on $OUT[POWDEROUT] = FALSE ; Set powder on $OUT[{{inert_gas_out}}] = FALSE ; Set inert gas on -$VEL.CP = 0.100000 ; m/s ; m/s +$VEL.CP = TRAVELSPEED ; m/s ; m/s ;- Move to HOME position PTP {A1 -33.31, A2 -104.71, A3 114.60, A4 282.66, A5 -39.21, A6 -104.87, E1 -90, E2 1.0} diff --git a/freecad/LaserCladdingWorkbench/utils.py b/freecad/LaserCladdingWorkbench/utils.py index 415aed7..805674d 100644 --- a/freecad/LaserCladdingWorkbench/utils.py +++ b/freecad/LaserCladdingWorkbench/utils.py @@ -4,8 +4,10 @@ import FreeCADGui as Gui import numpy as np import math import Part +import Draft import shapely from shapely.geometry import Polygon, Point +from shapely.validation import make_valid, explain_validity from pyslm import hatching as hatching from pyslm.geometry.geometry import LayerGeometryType from typing import List @@ -129,23 +131,28 @@ def get_polygon_from_subshape(subshape): return polygon -def get_polygon_from_wire(wire): +def get_polygon_from_wire(wire, discretization_factor): polygon = [] - n = math.floor(wire.Length/2.3) - for v in wire.discretize(Number=n): + n = math.floor(wire.Length/discretization_factor) + for v in wire.discretize(Number=n)[:-1]: polygon.append((v.x, v.y)) return polygon -def get_coords_from_shape(face): +def get_coords_from_shape(face, rdp_epsilon, discretization_factor): #outerpoly = get_polygon_from_subshape(face.OuterWire) - outerpoly = get_polygon_from_wire(face.OuterWire) + #sv0 = Draft.make_shape2dview(face.OuterWire, FreeCAD.Vector(0.0, 0.0, 1.0)) + + outerpoly = get_polygon_from_wire(face.OuterWire, discretization_factor) inner_polys = [] - for inner_wire in face.SubShapes[1:]: - tmp = get_polygon_from_wire(inner_wire) + subshapes = [x for x in face.SubShapes if not x.isEqual(face.OuterWire)] + for inner_wire in subshapes: + tmp = get_polygon_from_wire(inner_wire, discretization_factor) inner_polys.append(tmp) poly = Polygon(shell=outerpoly, holes=inner_polys) print("Polygon: ", poly) + valid_poly = make_valid(poly) + print("Validated Polygon:", explain_validity(valid_poly)) return path2DToPathList([poly]) @@ -163,12 +170,12 @@ def create_hatch_lines(geoms): return hatchlines -def create_contour_lines(geoms): +def create_contour_lines(geoms, epsilon=0.2): contours = [] for geom in geoms: if geom.type() == LayerGeometryType.Polygon: # print("Contour with {} coords".format(len(geom.coords))) - coords = rdp.rdp(geom.coords, epsilon=0.2, algo="iter", return_mask=False) + coords = rdp.rdp(geom.coords, epsilon=epsilon, algo="iter", return_mask=False) #print("Simplfied Poly:", len(coords)) pp = Part.makePolygon([App.Vector(x,y,0) for (x,y) in coords]) contours.append(pp)