From f63d0fadcb7e0b19528e2829aa122c5aa2c23f5c Mon Sep 17 00:00:00 2001 From: sliptonic Date: Tue, 11 Jun 2019 21:59:54 -0500 Subject: [PATCH] Path: Vcarve - Added threshold property to remove unwanted segments code cleanup & debug --- .../Gui/Resources/panels/PageOpVcarveEdit.ui | 40 +- src/Mod/Path/PathScripts/PathSelection.py | 17 + src/Mod/Path/PathScripts/PathVcarve.py | 373 +++++++++++------- src/Mod/Path/PathScripts/PathVcarveGui.py | 22 +- 4 files changed, 291 insertions(+), 161 deletions(-) diff --git a/src/Mod/Path/Gui/Resources/panels/PageOpVcarveEdit.ui b/src/Mod/Path/Gui/Resources/panels/PageOpVcarveEdit.ui index c514bad1d8..b10575eea4 100644 --- a/src/Mod/Path/Gui/Resources/panels/PageOpVcarveEdit.ui +++ b/src/Mod/Path/Gui/Resources/panels/PageOpVcarveEdit.ui @@ -48,7 +48,7 @@ - + @@ -65,22 +65,54 @@ <html><head/><body><p>This value is used in discretizing arcs into segments. Smaller values will result in larger gcode. Larger values may cause unwanted segments in the medial line path.</p></body></html> - 4 + 3 - 0.000100000000000 + 0.001000000000000 1.000000000000000 - 0.000100000000000 + 0.010000000000000 0.010000000000000 + + + + <html><head/><body><p>Threshold is used by the medial axis filter to remove unwanted segments. If the resulting path contains unwanted segments, decrease this value. </p><p>Valid values are 0.0 - 1.0</p><p>Default = 0.8</p><p>1.0 will remove nothing.</p></body></html> + + + 2 + + + 0.000000000000000 + + + 1.000000000000000 + + + 0.100000000000000 + + + 0.800000000000000 + + + + + + + <html><head/><body><p><br/></p></body></html> + + + Threshold + + + diff --git a/src/Mod/Path/PathScripts/PathSelection.py b/src/Mod/Path/PathScripts/PathSelection.py index 797cb8ac00..657f4519ce 100644 --- a/src/Mod/Path/PathScripts/PathSelection.py +++ b/src/Mod/Path/PathScripts/PathSelection.py @@ -49,6 +49,23 @@ class MESHGate(PathBaseGate): class VCARVEGate: def allow(self, doc, obj, sub): + try: + shape = obj.Shape + except Exception: # pylint: disable=broad-except + return False + + if math.fabs(shape.Volume) < 1e-9 and len(shape.Wires) > 0: + return True + + if shape.ShapeType == 'Edge': + return True + + if sub: + subShape = shape.getElement(sub) + if subShape.ShapeType == 'Edge': + return True + + return False class ENGRAVEGate(PathBaseGate): diff --git a/src/Mod/Path/PathScripts/PathVcarve.py b/src/Mod/Path/PathScripts/PathVcarve.py index 6209f90377..55ca61d6c9 100644 --- a/src/Mod/Path/PathScripts/PathVcarve.py +++ b/src/Mod/Path/PathScripts/PathVcarve.py @@ -22,7 +22,6 @@ # * * # *************************************************************************** -import ArchPanel import FreeCAD import Part import Path @@ -30,9 +29,9 @@ import PathScripts.PathEngraveBase as PathEngraveBase import PathScripts.PathLog as PathLog import PathScripts.PathOp as PathOp import PathScripts.PathUtils as PathUtils +import PathScripts.PathGeom as PathGeom import traceback import time -import PathScripts.PathGeom as pg from PathScripts.PathOpTools import orientWire import math @@ -46,148 +45,264 @@ if False: else: PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) + # Qt tanslation handling def translate(context, text, disambig=None): return QtCore.QCoreApplication.translate(context, text, disambig) + class ObjectVcarve(PathEngraveBase.ObjectOp): '''Proxy class for Vcarve operation.''' def opFeatures(self, obj): '''opFeatures(obj) ... return all standard features and edges based geomtries''' - return PathOp.FeatureTool | PathOp.FeatureDepths | PathOp.FeatureHeights | PathOp.FeatureStepDown | PathOp.FeatureBaseFaces; + return PathOp.FeatureTool | PathOp.FeatureHeights | PathOp.FeatureBaseFaces def setupAdditionalProperties(self, obj): if not hasattr(obj, 'BaseShapes'): obj.addProperty("App::PropertyLinkList", "BaseShapes", "Path", QtCore.QT_TRANSLATE_NOOP("PathVcarve", "Additional base objects to be engraved")) - obj.setEditorMode('BaseShapes', 2) # hide + obj.setEditorMode('BaseShapes', 2) # hide if not hasattr(obj, 'BaseObject'): obj.addProperty("App::PropertyLink", "BaseObject", "Path", QtCore.QT_TRANSLATE_NOOP("PathVcarve", "Additional base objects to be engraved")) - obj.setEditorMode('BaseObject', 2) # hide + obj.setEditorMode('BaseObject', 2) # hide def initOperation(self, obj): '''initOperation(obj) ... create vcarve specific properties.''' obj.addProperty("App::PropertyFloat", "Discretize", "Path", QtCore.QT_TRANSLATE_NOOP("PathVcarve", "The deflection value for discretizing arcs")) + obj.addProperty("App::PropertyFloat", "Threshold", "Path", QtCore.QT_TRANSLATE_NOOP("PathVcarve", "cutoff threshold for removing extraneous segments (0-1.0). default=0.8. Larger numbers remove less.")) + obj.Threshold = 0.8 + obj.Discretize = 0.01 self.setupAdditionalProperties(obj) def opOnDocumentRestored(self, obj): # upgrade ... self.setupAdditionalProperties(obj) - - def buildPathMedial(self, obj, Faces, zDepths, unitcircle): + def buildPathMedial(self, obj, Faces): '''constructs a medial axis path using openvoronoi''' - import openvoronoi as ovd + #import openvoronoi as ovd - def insert_wire_points(vd, wire): - pts=[] - for p in wire.Vertexes: - pts.append( ovd.Point( p.X, p.Y ) ) - print('p1 = FreeCAD.Vector(X:{} Y:{}'.format(p.X, p.Y)) - id_list = [] - print("inserting ",len(pts)," point-sites:") - for p in pts: - id_list.append( vd.addVertexSite( p ) ) - return id_list + # def insert_wire_points(vd, wire): + # pts = [] + # for p in wire.Vertexes: + # pts.append(ovd.Point(p.X, p.Y)) + # PathLog.debug('ovd.Point( {} ,{} )'.format(p.X, p.Y)) + # id_list = [] + # PathLog.debug("inserting {} openvoronoi point-sites".format(len(pts))) + # for p in pts: + # id_list.append(vd.addVertexSite(p)) + # return id_list - def insert_wire_segments(vd,id_list): - print('insert_polygon-segments') - print('inserting {} segments'.format(len(id_list))) - for n in range(len(id_list)): - n_nxt = n+1 - if n==(len(id_list)-1): - n_nxt=0 - vd.addLineSite( id_list[n], id_list[n_nxt]) + # def insert_wire_segments(vd, id_list): + # PathLog.debug('inserting {} segments into the voronoi diagram'.format(len(id_list))) + # for n in range(len(id_list)): + # n_nxt = n + 1 + # if n == (len(id_list) - 1): + # n_nxt = 0 + # vd.addLineSite(id_list[n], id_list[n_nxt]) def insert_many_wires(vd, wires): - # print('inserting {} wires'.format(len(obj.Wires))) - polygon_ids =[] - t_before = time.time() - for idx, wire in enumerate(wires): - print('discretize: {}'.format(obj.Discretize)) - pointList = wire.discretize(Deflection=obj.Discretize) - segwire = Part.Wire([Part.makeLine(p[0],p[1]) for p in zip(pointList, pointList[1:] )]) + #polygon_ids = [] + #t_before = time.time() + for wire in wires: + PathLog.debug('discretize value: {}'.format(obj.Discretize)) + pts = wire.discretize(QuasiDeflection=obj.Discretize) + ptv = [FreeCAD.Vector(p[0], p[1]) for p in pts] + ptv.append(ptv[0]) - if idx == 0: - segwire = orientWire(segwire, forward=False) - else: - segwire = orientWire(segwire, forward=True) + for i in range(len(pts)): + vd.addSegment(ptv[i], ptv[i+1]) - poly_id = insert_wire_points(vd,segwire) - polygon_ids.append(poly_id) - t_after = time.time() - pt_time = t_after-t_before + # segwire = Part.Wire([Part.makeLine(p[0], p[1]) for p in zip(pointList, pointList[1:])]) - t_before = time.time() - for ids in polygon_ids: - insert_wire_segments(vd,ids) - t_after = time.time() - seg_time = t_after-t_before - return [pt_time, seg_time] + # if idx == 0: + # segwire = orientWire(segwire, forward=False) + # else: + # segwire = orientWire(segwire, forward=True) + + # poly_id = insert_wire_points(vd, segwire) + # polygon_ids.append(poly_id) + # t_after = time.time() + # pt_time = t_after - t_before + + # t_before = time.time() + # for ids in polygon_ids: + # insert_wire_segments(vd, ids) + # t_after = time.time() + # seg_time = t_after - t_before + # return [pt_time, seg_time] def calculate_depth(MIC): # given a maximum inscribed circle (MIC) and tool angle, # return depth of cut. + maxdepth = obj.ToolController.Tool.CuttingEdgeHeight toolangle = obj.ToolController.Tool.CuttingEdgeAngle - return MIC / math.tan(math.radians(toolangle/2)) + d = MIC / math.tan(math.radians(toolangle / 2)) + return d if d <= maxdepth else maxdepth - def buildMedial(vd): - safeheight = obj.SafeHeight.Value + # def buildMedial(vd): + # safeheight = obj.SafeHeight.Value + # path = [] + # maw = ovd.MedialAxisWalk(vd.getGraph()) + # toolpath = maw.walk() + # for chain in toolpath: + + # path.append(Path.Command("G0 Z{}".format(safeheight))) + # p = chain[0][0][0] + # z = -(chain[0][0][1]) + + # path.append(Path.Command("G0 X{} Y{} Z{}".format(p.x, p.y, safeheight))) + + # for step in chain: + # for point in step: + # p = point[0] + # z = calculate_depth(-(point[1])) + # path.append(Path.Command("G1 X{} Y{} Z{} F{}".format(p.x, p.y, z, obj.ToolController.HorizFeed.Value))) +# path.append(Path.Command("G0 Z{}".format(safeheight))) return path pathlist = [] + def getEdges(vd, color=[0]): + if type(color) == int: + color = [color] + geomList = [] + for e in vd.Edges: + if e.Color not in color: + continue + # geom = e.toGeom(8) + if e.toGeom(8) is None: + continue + p1 = e.Vertices[0].toGeom(calculate_depth(0-e.getDistances()[0])) + p2 = e.Vertices[-1].toGeom(calculate_depth(0-e.getDistances()[-1])) + geomList.append(Part.LineSegment(p1, p2)) + # if individualEdges: + # name = "e%04d" % i + # Part.show(Part.Edge(geom), name) + #geomList.append(Part.Edge(geom)) + if geomList: + return geomList + + def areConnected(seg1, seg2): + ''' + Checks if two segments share an endpoint. + returns a new linesegment if connected or original seg1 if not + ''' + l1 = [seg1.StartPoint, seg1.EndPoint] + l2 = [seg2.StartPoint, seg2.EndPoint] + l3 = [v1 for v1 in l1 for v2 in l2 if PathGeom.pointsCoincide (v1, v2, error=0.01)] + # for v1 in l1: + # for v2 in l2: + # if PathGeom.pointsCoincide(v1, v2): + # l3.append(v1) + #l3 = [value for value in l1 if value in l2] + print('l1: {} l2: {} l3: {}'.format(l1,l2,l3)) + if len(l3) == 0: # no connection + print('no connection') + return seg1 + elif len(l3) == 1: # extend chain + print('one vert') + p1 = l1[0] if l1[0] == l3[0] else l1[1] + p2 = l2[0] if l2[0] == l3[0] else l2[1] + return Part.LineSegment(p1, p2) + else: # loop + print('loop') + return None + + def chains(seglist): + ''' + iterates through segements and builds a list of chains + ''' + + chains = [] + while len(seglist) > 0: + cur_seg = seglist.pop(0) + cur_chain = [cur_seg] + remaining = [] + tempseg = cur_seg # tempseg is a linesegment from first vertex to last in curchain + for i, seg in enumerate(seglist): + + ac = areConnected(tempseg, seg) + if ac != tempseg: + cur_chain.append(seg) + if ac is None: + remaining.extend(seglist[i+1:]) + break + else: + tempseg = ac + + #print("c: {}".format(cur_chain)) + + chains.append(cur_chain) + seglist = remaining + + return chains + + def cutWire(w): path = [] - maw = ovd.MedialAxisWalk( vd.getGraph() ) - toolpath = maw.walk() - for chain in toolpath: - path.append(Path.Command("G0 Z{}".format(safeheight))) - p = chain[0][0][0] - z = -(chain[0][0][1]) - - path.append(Path.Command("G0 X{} Y{} Z{}".format(p.x, p.y, safeheight))) - - for step in chain: - for point in step: - p = point[0] - z = calculate_depth(-(point[1])) - path.append(Path.Command("G1 X{} Y{} Z{} F{}".format(p.x, p.y, z, obj.ToolController.HorizFeed.Value))) - - path.append(Path.Command("G0 Z{}".format(safeheight))) + p = w.Vertexes[0] + path.append(Path.Command("G0 Z{}".format(obj.SafeHeight.Value))) + path.append(Path.Command("G0 X{} Y{} Z{}".format(p.X, p.Y, obj.SafeHeight.Value))) + # print('\/ \/ \/') + # print(p.Point) + c = Path.Command("G1 X{} Y{} Z{} F{}".format(p.X, p.Y, p.Z, obj.ToolController.HorizFeed.Value)) + # print(c) + # print('/\ /\ /\ ') + path.append(c) + #path.append(Path.Command("G1 X{} Y{} Z{} F{}".format(p.X, p.Y, p.Z, obj.ToolController.HorizFeed.Value))) + for vert in w.Vertexes[1:]: + path.append(Path.Command("G1 X{} Y{} Z{} F{}".format(vert.X, vert.Y, vert.Z, obj.ToolController.HorizFeed.Value))) + path.append(Path.Command("G0 Z{}".format(obj.SafeHeight.Value))) return path pathlist = [] - bins = 120 # int bins = number of bins for grid-search (affects performance, should not affect correctness) + pathlist.append(Path.Command("(starting)")) for f in Faces: - #unitcircle = f.BoundBox.DiagonalLength/2 - print('unitcircle: {}'.format(unitcircle)) - vd = ovd.VoronoiDiagram(200, bins) - vd.set_silent(True) # suppress Warnings! - wires = f.Wires - insert_many_wires(vd, wires) - pi = ovd.PolygonInterior( True ) - vd.filter_graph(pi) - ma = ovd.MedialAxis() - vd.filter_graph(ma) - pathlist.extend(buildMedial( vd )) # the actual cutting g-code + vd = Path.Voronoi() + insert_many_wires(vd, f.Wires) + + vd.construct() + # vd.colorExterior(1) + # vd.colorTwins(2) + + for e in vd.Edges: + e.Color = 0 if e.isPrimary() else 5 + vd.colorExterior(1) + vd.colorExterior(4, lambda v: not f.isInside(v.toGeom(), 0.01, True)) # should derive tolerance from geometry + vd.colorColinear(3) + vd.colorTwins(2) + + edgelist = getEdges(vd) + # for e in edgelist: + # Part.show(e.toShape()) + + # for e in [e_ for e_ in vd.Edges if e_.Color == 2]: + # print(e.getDistances()) + # p1 = e.Vertices[0].toGeom(calculate_depth(0-e.getDistances()[0])) + # p2 = e.Vertices[-1].toGeom(calculate_depth(0-e.getDistances()[-1])) + # edgelist.append(Part.makeLine(p1, p2)) + + # vlist = [] + # for v, r in zip(e.Vertices, e.getDistances()): + # p = v.toGeom() + # p.z = calculate_depth(r) + # vlist.append(p) + # l = Part.makeLine(vlist[0], vlist[-1]) + # edgelist.append(l) + + # for s in Part.sortEdges(edgelist): + # pathlist.extend(cutWire(Part.Wire(s))) + + for chain in chains(edgelist): + print('chain length {}'.format(len(chain))) + print(chain) + Part.show(Part.Wire([e.toShape() for e in chain])) + + #pathlist.extend(sortedWires) # the actual cutting g-code self.commandlist = pathlist - - def opExecute(self, obj): '''opExecute(obj) ... process engraving operation''' PathLog.track() # Openvoronoi must be installed - try: - import openvoronoi as ovd - except: - FreeCAD.Console.PrintError( - translate("Path_Vcarve", "This operation requires OpenVoronoi to be installed.") + "\n") - return - - - job = PathUtils.findParentJob(obj) - - jobshapes = [] - zValues = self.getZValues(obj) if obj.ToolController.Tool.ToolType != 'Engraver': FreeCAD.Console.PrintError( @@ -199,62 +314,23 @@ class ObjectVcarve(PathEngraveBase.ObjectOp): translate("Path_Vcarve", "Engraver Cutting Edge Angle must be < 180 degrees.") + "\n") return try: - if len(self.model) == 1 and self.model[0].isDerivedFrom('Sketcher::SketchObject') or \ + if obj.Base: + PathLog.track() + for base in obj.Base: + faces = [] + for sub in base[1]: + shape = getattr(base[0].Shape, sub) + if isinstance(shape, Part.Face): + faces.append(shape) + + modelshape = Part.makeCompound(faces) + + elif len(self.model) == 1 and self.model[0].isDerivedFrom('Sketcher::SketchObject') or \ self.model[0].isDerivedFrom('Part::Part2DObject'): PathLog.track() - # self.commandlist.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid})) - - # we only consider the outer wire if this is a Face modelshape = self.model[0].Shape - modelshape.tessellate(0.01) - self.buildPathMedial(obj, modelshape.Faces, zValues, modelshape.BoundBox.DiagonalLength/2) - # self.wires = wires - - # elif obj.Base: - # PathLog.track() - # wires = [] - # for base, subs in obj.Base: - # edges = [] - # basewires = [] - # for feature in subs: - # sub = base.Shape.getElement(feature) - # if type(sub) == Part.Edge: - # edges.append(sub) - # elif sub.Wires: - # basewires.extend(sub.Wires) - # else: - # basewires.append(Part.Wire(sub.Edges)) - - # for edgelist in Part.sortEdges(edges): - # basewires.append(Part.Wire(edgelist)) - - # wires.extend(basewires) - # self.buildpathocc(obj, wires, zValues) - # self.wires = wires - # elif not obj.BaseShapes: - # PathLog.track() - # if not obj.Base and not obj.BaseShapes: - # for base in self.model: - # PathLog.track(base.Label) - # if base.isDerivedFrom('Part::Part2DObject'): - # jobshapes.append(base) - - # if not jobshapes: - # raise ValueError(translate('PathVcarve', "Unknown baseobject type for engraving (%s)") % (obj.Base)) - - # if obj.BaseShapes or jobshapes: - # PathLog.track() - # wires = [] - # for shape in obj.BaseShapes + jobshapes: - # PathLog.track(shape.Label) - # shapeWires = shape.Shape.Wires - # self.buildpathocc(obj, shapeWires, zValues) - # wires.extend(shapeWires) - # self.wires = wires - # # the last command is a move to clearance, which is automatically added by PathOp - # if self.commandlist: - # self.commandlist.pop() + self.buildPathMedial(obj, modelshape.Faces) except Exception as e: PathLog.error(e) @@ -266,13 +342,14 @@ class ObjectVcarve(PathEngraveBase.ObjectOp): job = PathUtils.findParentJob(obj) self.opSetDefaultValues(obj, job) -def SetupProperties(): - return [ "Discretize" ] -def Create(name, obj = None): +def SetupProperties(): + return ["Discretize"] + + +def Create(name, obj=None): '''Create(name) ... Creates and returns a Vcarve operation.''' if obj is None: obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name) proxy = ObjectVcarve(obj, name) return obj - diff --git a/src/Mod/Path/PathScripts/PathVcarveGui.py b/src/Mod/Path/PathScripts/PathVcarveGui.py index 0ece507d45..34e2073002 100644 --- a/src/Mod/Path/PathScripts/PathVcarveGui.py +++ b/src/Mod/Path/PathScripts/PathVcarveGui.py @@ -27,7 +27,6 @@ import FreeCADGui import PathScripts.PathVcarve as PathVcarve import PathScripts.PathLog as PathLog import PathScripts.PathOpGui as PathOpGui -import PathScripts.PathSelection as PathSelection import PathScripts.PathUtils as PathUtils from PySide import QtCore, QtGui @@ -43,9 +42,11 @@ if False: else: PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) + def translate(context, text, disambig=None): return QtCore.QCoreApplication.translate(context, text, disambig) + class TaskPanelBaseGeometryPage(PathOpGui.TaskPanelBaseGeometryPage): '''Enhanced base geometry page to also allow special base objects.''' @@ -59,10 +60,10 @@ class TaskPanelBaseGeometryPage(PathOpGui.TaskPanelBaseGeometryPage): job = PathUtils.findParentJob(self.obj) base = job.Proxy.resourceClone(job, sel.Object) if not base: - PathLog.notice((translate("Path", "%s is not a Base Model object of the job %s")+"\n") % (sel.Object.Label, job.Label)) + PathLog.notice((translate("Path", "%s is not a Base Model object of the job %s") + "\n") % (sel.Object.Label, job.Label)) continue if base in shapes: - PathLog.notice((translate("Path", "Base shape %s already in the list")+"\n") % (sel.Object.Label)) + PathLog.notice((translate("Path", "Base shape %s already in the list") + "\n") % (sel.Object.Label)) continue if base.isDerivedFrom('Part::Part2DObject'): if sel.HasSubObjects: @@ -74,7 +75,7 @@ class TaskPanelBaseGeometryPage(PathOpGui.TaskPanelBaseGeometryPage): self.obj.Proxy.addBase(self.obj, base, sub) else: # when adding an entire shape to BaseShapes we can take its sub shapes out of Base - self.obj.Base = [(p,el) for p,el in self.obj.Base if p != base] + self.obj.Base = [(p, el) for p, el in self.obj.Base if p != base] shapes.append(base) self.obj.BaseShapes = shapes added = True @@ -108,6 +109,7 @@ class TaskPanelBaseGeometryPage(PathOpGui.TaskPanelBaseGeometryPage): self.obj.BaseShapes = shapes return self.super().updateBase() + class TaskPanelOpPage(PathOpGui.TaskPanelPage): '''Page controller class for the Vcarve operation.''' @@ -119,17 +121,21 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): '''getFields(obj) ... transfers values from UI to obj's proprties''' if obj.Discretize != self.form.discretize.value(): obj.Discretize = self.form.discretize.value() + if obj.Threshold != self.form.threshold.value(): + obj.Threshold = self.form.threshold.value() self.updateToolController(obj, self.form.toolController) def setFields(self, obj): '''setFields(obj) ... transfers obj's property values to UI''' self.form.discretize.setValue(obj.Discretize) + self.form.threshold.setValue(obj.Threshold) self.setupToolController(obj, self.form.toolController) def getSignalsForUpdate(self, obj): '''getSignalsForUpdate(obj) ... return list of signals for updating obj''' signals = [] signals.append(self.form.discretize.editingFinished) + signals.append(self.form.threshold.editingFinished) signals.append(self.form.toolController.currentIndexChanged) return signals @@ -137,11 +143,9 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): '''taskPanelBaseGeometryPage(obj, features) ... return page for adding base geometries.''' return TaskPanelBaseGeometryPage(obj, features) -Command = PathOpGui.SetupOperation('Vcarve', - PathVcarve.Create, - TaskPanelOpPage, - 'Path-Vcarve', - QtCore.QT_TRANSLATE_NOOP("PathVcarve", "Vcarve"), + +Command = PathOpGui.SetupOperation('Vcarve', PathVcarve.Create, TaskPanelOpPage, + 'Path-Vcarve', QtCore.QT_TRANSLATE_NOOP("PathVcarve", "Vcarve"), QtCore.QT_TRANSLATE_NOOP("PathVcarve", "Creates a medial line engraving path"), PathVcarve.SetupProperties)