From c141f4188798bb776d8e8f32cd3fc23087b51ea9 Mon Sep 17 00:00:00 2001 From: sliptonic Date: Tue, 11 Jun 2019 22:05:06 -0500 Subject: [PATCH] Path: add vcarve operation using openvoronoi --- src/Mod/Path/CMakeLists.txt | 2 + src/Mod/Path/Gui/Resources/Path.qrc | 2 + .../Path/Gui/Resources/icons/Path-Vcarve.svg | 664 ++++++++++++++++++ .../Gui/Resources/panels/PageOpVcarveEdit.ui | 104 +++ src/Mod/Path/InitGui.py | 1 + src/Mod/Path/PathScripts/PathGuiInit.py | 2 +- src/Mod/Path/PathScripts/PathSelection.py | 8 + src/Mod/Path/PathScripts/PathVcarve.py | 266 +++++++ src/Mod/Path/PathScripts/PathVcarveGui.py | 148 ++++ 9 files changed, 1196 insertions(+), 1 deletion(-) create mode 100644 src/Mod/Path/Gui/Resources/icons/Path-Vcarve.svg create mode 100644 src/Mod/Path/Gui/Resources/panels/PageOpVcarveEdit.ui create mode 100644 src/Mod/Path/PathScripts/PathVcarve.py create mode 100644 src/Mod/Path/PathScripts/PathVcarveGui.py 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + Path-Engrave + 2016-02-24 + http://www.freecadweb.org/wiki/index.php?title=Artwork + + + FreeCAD + + + FreeCAD/src/Mod/Path/Gui/Resources/icons/Path-Engrave.svg + + + FreeCAD LGPL2+ + + + https://www.gnu.org/copyleft/lesser.html + + + [agryson] Alexander Gryson + + + + + + + + + + + + + + + + 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")