diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt
index a532b2da04..451226ae21 100644
--- a/src/Mod/Path/CMakeLists.txt
+++ b/src/Mod/Path/CMakeLists.txt
@@ -125,6 +125,8 @@ SET(PathScripts_SRCS
PathScripts/PathUtil.py
PathScripts/PathUtils.py
PathScripts/PathUtilsGui.py
+ PathScripts/PathVcarve.py
+ PathScripts/PathVcarveGui.py
PathScripts/PathWaterline.py
PathScripts/PathWaterlineGui.py
PathScripts/PostUtils.py
diff --git a/src/Mod/Path/Gui/Resources/Path.qrc b/src/Mod/Path/Gui/Resources/Path.qrc
index 1e67d1a92f..44b066cd0c 100644
--- a/src/Mod/Path/Gui/Resources/Path.qrc
+++ b/src/Mod/Path/Gui/Resources/Path.qrc
@@ -66,6 +66,7 @@
icons/Path-ToolController.svg
icons/Path-Toolpath.svg
icons/Path-ToolTable.svg
+ icons/Path-Vcarve.svg
icons/Path-Waterline.svg
icons/arrow-ccw.svg
icons/arrow-cw.svg
@@ -111,6 +112,7 @@
panels/PageOpSlotEdit.ui
panels/PageOpSurfaceEdit.ui
panels/PageOpWaterlineEdit.ui
+ panels/PageOpVcarveEdit.ui
panels/PathEdit.ui
panels/PointEdit.ui
panels/SetupGlobal.ui
diff --git a/src/Mod/Path/Gui/Resources/icons/Path-Vcarve.svg b/src/Mod/Path/Gui/Resources/icons/Path-Vcarve.svg
new file mode 100644
index 0000000000..0cf0716a94
--- /dev/null
+++ b/src/Mod/Path/Gui/Resources/icons/Path-Vcarve.svg
@@ -0,0 +1,664 @@
+
+
+
+
diff --git a/src/Mod/Path/Gui/Resources/panels/PageOpVcarveEdit.ui b/src/Mod/Path/Gui/Resources/panels/PageOpVcarveEdit.ui
new file mode 100644
index 0000000000..fd5fa66346
--- /dev/null
+++ b/src/Mod/Path/Gui/Resources/panels/PageOpVcarveEdit.ui
@@ -0,0 +1,104 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 400
+ 140
+
+
+
+ Form
+
+
+ -
+
+
+ QFrame::StyledPanel
+
+
+ QFrame::Raised
+
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ ToolController
+
+
+
+ -
+
+
+ <html><head/><body><p>The tool and its settings to be used for this operation.</p></body></html>
+
+
+
+
+
+
+ -
+
+
+
-
+
+
+ <html><head/><body><p><br/></p></body></html>
+
+
+ Discretization Deflection
+
+
+
+ -
+
+
+ <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>
+
+
+ 3
+
+
+ 0.001000000000000
+
+
+ 1.000000000000000
+
+
+ 0.001000000000000
+
+
+ 0.010000000000000
+
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+
+
+
diff --git a/src/Mod/Path/InitGui.py b/src/Mod/Path/InitGui.py
index 6b8b545bae..b630732886 100644
--- a/src/Mod/Path/InitGui.py
+++ b/src/Mod/Path/InitGui.py
@@ -113,6 +113,7 @@ class PathWorkbench (Workbench):
threedcmdgroup = threedopcmdlist
if PathPreferences.experimentalFeaturesEnabled():
projcmdlist.append("Path_Sanity")
+ engravecmdlist.append("Path_Vcarve")
prepcmdlist.append("Path_Shape")
extracmdlist.extend(["Path_Area", "Path_Area_Workplane"])
diff --git a/src/Mod/Path/PathScripts/PathGuiInit.py b/src/Mod/Path/PathScripts/PathGuiInit.py
index ad39fbea0c..060d1b3fe5 100644
--- a/src/Mod/Path/PathScripts/PathGuiInit.py
+++ b/src/Mod/Path/PathScripts/PathGuiInit.py
@@ -35,7 +35,6 @@ else:
Processed = False
-
def Startup():
global Processed # pylint: disable=global-statement
if not Processed:
@@ -82,6 +81,7 @@ def Startup():
from PathScripts import PathToolLibraryEditor
from PathScripts import PathUtilsGui
# from PathScripts import PathWaterlineGui # Added in initGui.py due to OCL dependency
+ from PathScripts import PathVcarveGui
Processed = True
else:
PathLog.debug('Skipping PathGui initialisation')
diff --git a/src/Mod/Path/PathScripts/PathSelection.py b/src/Mod/Path/PathScripts/PathSelection.py
index 95456b6759..797cb8ac00 100644
--- a/src/Mod/Path/PathScripts/PathSelection.py
+++ b/src/Mod/Path/PathScripts/PathSelection.py
@@ -47,6 +47,9 @@ class MESHGate(PathBaseGate):
def allow(self, doc, obj, sub): # pylint: disable=unused-argument
return obj.TypeId[0:4] == 'Mesh'
+class VCARVEGate:
+ def allow(self, doc, obj, sub):
+
class ENGRAVEGate(PathBaseGate):
def allow(self, doc, obj, sub): # pylint: disable=unused-argument
@@ -300,6 +303,10 @@ def surfaceselect():
FreeCADGui.Selection.addSelectionGate(gate)
FreeCAD.Console.PrintWarning("Surfacing Select Mode\n")
+def vcarveselect():
+ FreeCADGui.Selection.addSelectionGate(VCARVEGate())
+ FreeCAD.Console.PrintWarning("Vcarve Select Mode\n")
+
def probeselect():
FreeCADGui.Selection.addSelectionGate(PROBEGate())
@@ -328,6 +335,7 @@ def select(op):
opsel['Surface'] = surfaceselect
opsel['Waterline'] = surfaceselect
opsel['Adaptive'] = adaptiveselect
+ opsel['Vcarve'] = vcarveselect
opsel['Probe'] = probeselect
opsel['Custom'] = customselect
return opsel[op]
diff --git a/src/Mod/Path/PathScripts/PathVcarve.py b/src/Mod/Path/PathScripts/PathVcarve.py
new file mode 100644
index 0000000000..3fd0c8ba95
--- /dev/null
+++ b/src/Mod/Path/PathScripts/PathVcarve.py
@@ -0,0 +1,266 @@
+# -*- coding: utf-8 -*-
+
+# ***************************************************************************
+# * *
+# * Copyright (c) 2014 Yorik van Havre *
+# * *
+# * This program is free software; you can redistribute it and/or modify *
+# * it under the terms of the GNU Lesser General Public License (LGPL) *
+# * as published by the Free Software Foundation; either version 2 of *
+# * the License, or (at your option) any later version. *
+# * for detail see the LICENCE text file. *
+# * *
+# * This program is distributed in the hope that it will be useful, *
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+# * GNU Library General Public License for more details. *
+# * *
+# * You should have received a copy of the GNU Library General Public *
+# * License along with this program; if not, write to the Free Software *
+# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
+# * USA *
+# * *
+# ***************************************************************************
+
+import ArchPanel
+import FreeCAD
+import Part
+import Path
+import PathScripts.PathEngraveBase as PathEngraveBase
+import PathScripts.PathLog as PathLog
+import PathScripts.PathOp as PathOp
+import PathScripts.PathUtils as PathUtils
+import traceback
+import time
+import PathScripts.PathGeom as pg
+from PathScripts.PathOpTools import orientWire
+
+
+from PySide import QtCore
+
+__doc__ = "Class and implementation of Path Vcarve operation"
+
+if False:
+ PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
+ PathLog.trackModule(PathLog.thisModule())
+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;
+
+ 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
+ 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
+
+ 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"))
+ self.setupAdditionalProperties(obj)
+
+ def opOnDocumentRestored(self, obj):
+ # upgrade ...
+ self.setupAdditionalProperties(obj)
+
+
+ def buildPathMedial(self, obj, Faces, zDepths, unitcircle):
+ '''constructs a medial axis path using openvoronoi'''
+ 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_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_many_wires(vd, wires):
+ # print('inserting {} wires'.format(len(obj.Wires)))
+ polygon_ids =[]
+ t_before = time.time()
+ for idx, wire in enumerate(wires):
+ d = obj.Discretize
+ print('discretize: {}'.format(d))
+ d = 0.008
+ pointList = wire.discretize(Deflection=d)
+ segwire = Part.Wire([Part.makeLine(p[0],p[1]) for p in zip(pointList, pointList[1:] )])
+
+ 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 buildMedial(vd):
+ safeheight = 3.0
+ 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 = -(point[1])
+ path.append(Path.Command("G1 X{} Y{} Z{}".format(p.x, p.y, z)))
+
+ path.append(Path.Command("G0 Z{}".format(safeheight)))
+
+ return path
+
+ pathlist = []
+ bins = 120 # int bins = number of bins for grid-search (affects performance, should not affect correctness)
+ 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
+
+ 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)
+
+
+ try:
+ if 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
+ 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()
+
+ except Exception as e:
+ PathLog.error(e)
+ traceback.print_exc()
+ PathLog.error(translate('PathVcarve', 'The Job Base Object has no engraveable element. Engraving operation will produce no output.'))
+
+ def opUpdateDepths(self, obj, ignoreErrors=False):
+ '''updateDepths(obj) ... engraving is always done at the top most z-value'''
+ job = PathUtils.findParentJob(obj)
+ self.opSetDefaultValues(obj, job)
+
+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
new file mode 100644
index 0000000000..21c53066e7
--- /dev/null
+++ b/src/Mod/Path/PathScripts/PathVcarveGui.py
@@ -0,0 +1,148 @@
+# -*- coding: utf-8 -*-
+
+# ***************************************************************************
+# * *
+# * Copyright (c) 2017 sliptonic *
+# * *
+# * This program is free software; you can redistribute it and/or modify *
+# * it under the terms of the GNU Lesser General Public License (LGPL) *
+# * as published by the Free Software Foundation; either version 2 of *
+# * the License, or (at your option) any later version. *
+# * for detail see the LICENCE text file. *
+# * *
+# * This program is distributed in the hope that it will be useful, *
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+# * GNU Library General Public License for more details. *
+# * *
+# * You should have received a copy of the GNU Library General Public *
+# * License along with this program; if not, write to the Free Software *
+# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
+# * USA *
+# * *
+# ***************************************************************************
+
+import FreeCAD
+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
+
+__title__ = "Path Vcarve Operation UI"
+__author__ = "sliptonic (Brad Collette)"
+__url__ = "http://www.freecadweb.org"
+__doc__ = "Vcarve operation page controller and command implementation."
+
+if False:
+ PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
+ PathLog.trackModule(PathLog.thisModule())
+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.'''
+
+ def super(self):
+ return super(TaskPanelBaseGeometryPage, self)
+
+ def addBaseGeometry(self, selection):
+ added = False
+ shapes = self.obj.BaseShapes
+ for sel in selection:
+ 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))
+ continue
+ if base in shapes:
+ PathLog.notice((translate("Path", "Base shape %s already in the list")+"\n") % (sel.Object.Label))
+ continue
+ if base.isDerivedFrom('Part::Part2DObject'):
+ if sel.HasSubObjects:
+ # selectively add some elements of the drawing to the Base
+ for sub in sel.SubElementNames:
+ if 'Vertex' in sub:
+ PathLog.info(translate("Path", "Ignoring vertex"))
+ else:
+ 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]
+ shapes.append(base)
+ self.obj.BaseShapes = shapes
+ added = True
+ else:
+ # user wants us to engrave an edge of face of a base model
+ base = self.super().addBaseGeometry(selection)
+ added = added or base
+
+ return added
+
+ def setFields(self, obj):
+ self.super().setFields(obj)
+ self.form.baseList.blockSignals(True)
+ for shape in self.obj.BaseShapes:
+ item = QtGui.QListWidgetItem(shape.Label)
+ item.setData(self.super().DataObject, shape)
+ item.setData(self.super().DataObjectSub, None)
+ self.form.baseList.addItem(item)
+ self.form.baseList.blockSignals(False)
+
+ def updateBase(self):
+ PathLog.track()
+ shapes = []
+ for i in range(self.form.baseList.count()):
+ item = self.form.baseList.item(i)
+ obj = item.data(self.super().DataObject)
+ sub = item.data(self.super().DataObjectSub)
+ if not sub:
+ shapes.append(obj)
+ PathLog.debug("Setting new base shapes: %s -> %s" % (self.obj.BaseShapes, shapes))
+ self.obj.BaseShapes = shapes
+ return self.super().updateBase()
+
+class TaskPanelOpPage(PathOpGui.TaskPanelPage):
+ '''Page controller class for the Vcarve operation.'''
+
+ def getForm(self):
+ '''getForm() ... returns UI'''
+ return FreeCADGui.PySideUic.loadUi(":/panels/PageOpVcarveEdit.ui")
+
+ def getFields(self, obj):
+ '''getFields(obj) ... transfers values from UI to obj's proprties'''
+ # if obj.StartVertex != self.form.startVertex.value():
+ # obj.StartVertex = self.form.startVertex.value()
+ self.updateToolController(obj, self.form.toolController)
+
+ def setFields(self, obj):
+ '''setFields(obj) ... transfers obj's property values to UI'''
+ # self.form.startVertex.setValue(obj.StartVertex)
+ self.setupToolController(obj, self.form.toolController)
+
+ def getSignalsForUpdate(self, obj):
+ '''getSignalsForUpdate(obj) ... return list of signals for updating obj'''
+ signals = []
+ # signals.append(self.form.startVertex.editingFinished)
+ signals.append(self.form.toolController.currentIndexChanged)
+ return signals
+
+ def taskPanelBaseGeometryPage(self, obj, features):
+ '''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"),
+ QtCore.QT_TRANSLATE_NOOP("PathVcarve", "Creates a medial line engraving path"),
+ PathVcarve.SetupProperties)
+
+FreeCAD.Console.PrintLog("Loading PathVcarveGui... done\n")