diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt index ddfcaa519b..3735fc0419 100644 --- a/src/Mod/Path/CMakeLists.txt +++ b/src/Mod/Path/CMakeLists.txt @@ -33,6 +33,7 @@ SET(PathScripts_SRCS PathScripts/PathCircularHoleBaseGui.py PathScripts/PathComment.py PathScripts/PathCopy.py + PathScripts/PathCamoticsGui.py PathScripts/PathCustom.py PathScripts/PathCustomGui.py PathScripts/PathDeburr.py diff --git a/src/Mod/Path/Gui/Resources/Path.qrc b/src/Mod/Path/Gui/Resources/Path.qrc index 309906a1b1..cc3d6340fb 100644 --- a/src/Mod/Path/Gui/Resources/Path.qrc +++ b/src/Mod/Path/Gui/Resources/Path.qrc @@ -14,6 +14,7 @@ icons/Path_BStep.svg icons/Path_BStop.svg icons/Path_BaseGeometry.svg + icons/Path_Camotics.svg icons/Path_Comment.svg icons/Path_Compound.svg icons/Path_Contour.svg @@ -130,6 +131,7 @@ panels/ToolBitSelector.ui panels/ToolEditor.ui panels/ToolLibraryEditor.ui + panels/TaskPathCamoticsSim.ui panels/TaskPathSimulator.ui panels/ZCorrectEdit.ui preferences/Advanced.ui diff --git a/src/Mod/Path/Gui/Resources/icons/Path_Camotics.svg b/src/Mod/Path/Gui/Resources/icons/Path_Camotics.svg new file mode 100644 index 0000000000..e9d4b73037 --- /dev/null +++ b/src/Mod/Path/Gui/Resources/icons/Path_Camotics.svg @@ -0,0 +1,644 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + Path_Drilling + 2015-07-04 + http://www.freecadweb.org/wiki/index.php?title=Artwork + + + FreeCAD + + + FreeCAD/src/Mod/Path/Gui/Resources/icons/Path_Drilling.svg + + + FreeCAD LGPL2+ + + + https://www.gnu.org/copyleft/lesser.html + + + [agryson] Alexander Gryson + + + + + + + + + C + + + + diff --git a/src/Mod/Path/Gui/Resources/icons/camotics-logo.png b/src/Mod/Path/Gui/Resources/icons/camotics-logo.png new file mode 100644 index 0000000000..652cd11b14 Binary files /dev/null and b/src/Mod/Path/Gui/Resources/icons/camotics-logo.png differ diff --git a/src/Mod/Path/Gui/Resources/panels/TaskPathCamoticsSim.ui b/src/Mod/Path/Gui/Resources/panels/TaskPathCamoticsSim.ui new file mode 100644 index 0000000000..e73a4f179e --- /dev/null +++ b/src/Mod/Path/Gui/Resources/panels/TaskPathCamoticsSim.ui @@ -0,0 +1,103 @@ + + + TaskPathSimulator + + + + 0 + 0 + 404 + 364 + + + + Path Simulator + + + + + + QAbstractItemView::ContiguousSelection + + + QAbstractItemView::SelectRows + + + + + + + + + Estimated time to run to the selected point in the job + + + Estimated run time: + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + true + + + + + + + + + + + 23 + + + + + + + Stop + + + + + + + + + Launch Camotics + + + + :/icons/Path_Camotics.svg:/icons/Path_Camotics.svg + + + + + + + + + + + SimStop() + SimPlay() + SimPause() + SimStep() + SimFF() + + diff --git a/src/Mod/Path/InitGui.py b/src/Mod/Path/InitGui.py index d4011741ab..d02be4923e 100644 --- a/src/Mod/Path/InitGui.py +++ b/src/Mod/Path/InitGui.py @@ -78,6 +78,7 @@ class PathWorkbench (Workbench): from PathScripts import PathToolBitLibraryCmd import PathCommands + PathGuiInit.Startup() # build commands list @@ -96,8 +97,6 @@ class PathWorkbench (Workbench): "Path_DressupLeadInOut", "Path_DressupRampEntry", "Path_DressupTag", "Path_DressupZCorrect"] extracmdlist = [] - # modcmdmore = ["Path_Hop",] - # remotecmdlist = ["Path_Remote"] specialcmdlist = [] @@ -122,6 +121,12 @@ class PathWorkbench (Workbench): twodopcmdlist.append("Path_Slot") if PathPreferences.advancedOCLFeaturesEnabled(): + try: + import camotics + toolcmdlist.append("Path_Camotics") + except ImportError: + pass + try: import ocl # pylint: disable=unused-variable from PathScripts import PathSurfaceGui diff --git a/src/Mod/Path/PathScripts/PathCamoticsGui.py b/src/Mod/Path/PathScripts/PathCamoticsGui.py new file mode 100644 index 0000000000..125ba47bfb --- /dev/null +++ b/src/Mod/Path/PathScripts/PathCamoticsGui.py @@ -0,0 +1,400 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2020 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.PathLog as PathLog +# from pivy import coin +# from itertools import cycle +import json +import Mesh +import time +import camotics +import PathScripts.PathPost as PathPost +import io +import PathScripts +import queue +from threading import Thread, Lock +import subprocess +import PySide + +from PySide import QtCore, QtGui + +__title__ = "Camotics Simulator" +__author__ = "sliptonic (Brad Collette)" +__url__ = "https://www.freecadweb.org" +__doc__ = "Task panel for Camotics Simulation" + +PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) +PathLog.trackModule(PathLog.thisModule()) + +# Qt translation handling +def translate(context, text, disambig=None): + return QtCore.QCoreApplication.translate(context, text, disambig) + + +class CAMoticsUI: + def __init__(self, simulation): + # this will create a Qt widget from our ui file + self.form = FreeCADGui.PySideUic.loadUi(":/panels/TaskPathCamoticsSim.ui") + self.simulation = simulation + self.opModel = PySide.QtGui.QStandardItemModel(0, 0)#len(self.columnNames())) + self.initializeUI() + self.lock = False + + def columnNames(self): + return ['Operation'] + + def loadData(self): + self.opModel.clear() + self.opModel.setHorizontalHeaderLabels(self.columnNames()) + ops = self.simulation.finalpostlist + for i, op in enumerate(ops): + libItem = PySide.QtGui.QStandardItem(op.Label) + libItem.setToolTip('op') + libItem.setData(op) + #libItem.setIcon(PySide.QtGui.QPixmap(':/icons/Path_ToolTable.svg')) + self.opModel.appendRow(libItem) + + + def initializeUI(self): + self.form.progressBar.reset() + self.updateEstimate("00:00:00") + self.form.btnRun.setText("Run") + self.form.btnLaunchCamotics.clicked.connect(self.launchCamotics) + self.simulation.progressUpdate.connect(self.calculating) + self.simulation.estimateChanged.connect(self.updateEstimate) + self.form.btnRun.clicked.connect(self.runButton) + self.form.lstPathObjects.setModel(self.opModel) + self.form.lstPathObjects.selectionModel().selectionChanged.connect(self.reSelect) + self.loadData() + self.selectAll() + + def selectAll(self): + selmodel = self.form.lstPathObjects.selectionModel() + index0 = self.opModel.index(0, 0) + index1 = self.opModel.index(self.opModel.rowCount()-1,0) + itemSelection = QtCore.QItemSelection(index0, index1) + selmodel.blockSignals(True) + selmodel.select(itemSelection, QtCore.QItemSelectionModel.Rows | QtCore.QItemSelectionModel.Select) + selmodel.blockSignals(False) + + + def reSelect(self): + selmodel = self.form.lstPathObjects.selectionModel() + item = selmodel.selection().indexes()[0] + + index0 = self.opModel.index(0, 0) + itemSelection = QtCore.QItemSelection(index0, item) + + selmodel.blockSignals(True) + selmodel.select(itemSelection, + QtCore.QItemSelectionModel.Rows | QtCore.QItemSelectionModel.Select) + selmodel.blockSignals(False) + + selectedObjs = [self.opModel.itemFromIndex(i).data() for i in selmodel.selection().indexes()] + + self.simulation.setSimulationPath(selectedObjs) + + def runButton(self): + if self.form.btnRun.text() == 'Run': + self.simulation.run() + else: + self.simulation.stopSim() + + + def updateEstimate(self, timestring): + self.form.txtRunEstimate.setText(timestring) + + def launchCamotics(self): + projfile = self.simulation.buildproject() + subprocess.Popen(["camotics", projfile]) + + def accept(self): + self.simulation.accept() + FreeCADGui.Control.closeDialog() + + def reject(self): + self.simulation.cancel() + FreeCADGui.Control.closeDialog() + + def calculating(self, progress=0.0): + if progress < 1.0: + self.form.btnRun.setText('Stop') + else: + self.form.btnRun.setText('Run') + self.form.progressBar.setValue(int(progress*100)) + + +class CamoticsSimulation(QtCore.QObject): + + SIM = camotics.Simulation() + q = queue.Queue() + progressUpdate = QtCore.Signal(object) + estimateChanged = QtCore.Signal(str) + + SHAPEMAP = {'ballend': 'Ballnose', + 'endmill': 'Cylindrical', + 'v-bit' : 'Conical', + 'chamfer': 'Snubnose'} + + cutMaterial = None + + def worker(self, lock): + while True: + item = self.q.get() + with lock: + if item['TYPE'] == 'STATUS': + if item['VALUE'] == 'DONE': + self.SIM.wait() + surface = self.SIM.get_surface('binary') + #surface = self.SIM.get_surface('python') + self.SIM.wait() + self.addMesh(surface) + #self.makeCoinMesh(surface) + elif item['TYPE'] == 'PROGRESS': + #self.taskForm.calculating(item['VALUE']) + msg = item['VALUE'] + self.progressUpdate.emit(msg) + self.q.task_done() + + + + def __init__(self): + super().__init__() # needed for QT signals + lock = Lock() + Thread(target=self.worker, daemon=True, args=(lock,)).start() + + def callback(self, status, progress): + self.q.put({'TYPE': 'PROGRESS', 'VALUE': progress}) + self.q.put({'TYPE': 'STATUS' , 'VALUE': status }) + + def isDone(self, success): + self.q.put({'TYPE': 'STATUS' , 'VALUE': 'DONE'}) + + def stopSim(self): + if self.SIM.is_running(): + self.SIM.interrupt() + self.SIM.wait() + return True + + + def addMesh(self, surface): + '''takes a binary stl and adds a Mesh to the current docuemnt''' + + if self.cutMaterial is None: + self.cutMaterial = FreeCAD.ActiveDocument.addObject("Mesh::Feature", "SimulationOutput") + buffer=io.BytesIO() + buffer.write(surface) + buffer.seek(0) + mesh=Mesh.Mesh() + mesh.read(buffer, "STL") + self.cutMaterial.Mesh = mesh + # Mesh.show(mesh) + + def setSimulationPath(self, postlist): + gcode, fname = PathPost.CommandPathPost().getGcodeSilently(postlist, self.job) + self.SIM.compute_path(gcode) + self.SIM.wait() + + tot = sum([step['time'] for step in self.SIM.get_path()]) + self.estimateChanged.emit(time.strftime("%H:%M:%S", time.gmtime(tot))) + + def Activate(self): + self.job = FreeCADGui.Selection.getSelectionEx()[0].Object + postlist = PathPost.buildPostList(self.job) + self.finalpostlist = [item for slist in postlist for item in slist] + + self.taskForm = CAMoticsUI(self) + FreeCADGui.Control.showDialog(self.taskForm) + + self.SIM.set_metric() + self.SIM.set_resolution('high') + + bb = self.job.Stock.Shape.BoundBox + self.SIM.set_workpiece(min = (bb.XMin, bb.YMin, bb.ZMin), max = (bb.XMax, bb.YMax, bb.ZMax)) + + for t in self.job.Tools.Group: + self.SIM.set_tool(t.ToolNumber, + metric = True, + shape = self.SHAPEMAP.get(t.Tool.ShapeName, 'Cylindrical'), + length = t.Tool.Length.Value, + diameter = t.Tool.Diameter.Value) + + + self.setSimulationPath(self.finalpostlist) + + def run(self): + self.SIM.start(self.callback, done=self.isDone) + + #def makeCoinMesh(self, surface): + # # this doesn't work yet + # #sg = FreeCADGui.ActiveDocument.ActiveView.getSceneGraph(); + # color = coin.SoBaseColor() + # color.rgb = (1, 0, 1) + # coords = coin.SoTransform() + # node = coin.SoSeparator() + # node.addChild(color) + # node.addChild(coords) + + # end = [-1] + # vertices = list(zip(*[iter(surface['vertices'])] * 3)) + # #polygons = list(zip(*[iter(vertices)] * 3, cycle(end))) + # polygons = list(zip(*[iter(range(len(vertices)))] * 3, cycle(end))) + + # print('verts {}'.format(len(vertices))) + # print('polygons {}'.format(len(polygons))) + + # data=coin.SoCoordinate3() + # face=coin.SoIndexedFaceSet() + # node.addChild(data) + # node.addChild(face) + + # # i = 0 + # # for i, v in enumerate(vertices): + # # print('i: {} v: {}'.format(i, v)) + # #data.point.set1Value(i, v[0], v[1], v[2]) + # # i += 1 + # # i = 0 + # # for p in polygons: + # # try: + # # face.coordIndex.set1Value(i, p) + # # i += 1 + # # except Exception as e: + # # print(e) + # # print(i) + # # print(p) + + # # sg.addChild(node) + + + def RemoveMaterial(self): + if self.cutMaterial is not None: + FreeCAD.ActiveDocument.removeObject(self.cutMaterial.Name) + self.cutMaterial = None + + def accept(self): + pass + + def cancel(self): + self.RemoveMaterial() + + def buildproject(self): + + job = self.job + gcode, fname = PathPost.CommandPathPost().getGcodeSilently(self.finalpostlist, self.job, temp=False) + + tooltemplate = { + "units": "metric", + "shape": "cylindrical", + "length": 10, + "diameter": 3.125, + "description": "" + } + + workpiecetemplate = { + "automatic": False, + "margin": 0, + "bounds": { + "min": [0, 0, 0], + "max": [0, 0, 0]} + } + + camoticstemplate = { + "units": "metric", + "resolution-mode": "high", + "resolution": 1, + "tools": {}, + "workpiece": {}, + "files": [] + } + + unitstring = "imperial" if FreeCAD.Units.getSchema() in [2,3,5,7] else "metric" + + camoticstemplate["units"] = unitstring + camoticstemplate["resolution-mode"] = "medium" + camoticstemplate["resolution"] = 1 + + toollist = {} + for t in job.Tools.Group: + tooltemplate["units"] = unitstring + if hasattr(t.Tool, 'Camotics'): + tooltemplate["shape"] = t.Tool.Camotics + else: + tooltemplate["shape"] = self.SHAPEMAP.get(t.Tool.ShapeName, 'Cylindrical') + + tooltemplate["length"] = t.Tool.Length.Value + tooltemplate["diameter"] = t.Tool.Diameter.Value + tooltemplate["description"] = t.Label + toollist[t.ToolNumber] = tooltemplate + + camoticstemplate['tools'] = toollist + + bb = job.Stock.Shape.BoundBox + + workpiecetemplate['bounds']['min'] = [bb.XMin, bb.YMin, bb.ZMin] + workpiecetemplate['bounds']['max'] = [bb.XMax, bb.YMax, bb.ZMax] + camoticstemplate['workpiece'] = workpiecetemplate + + camoticstemplate['files'] = [fname] + + foo = QtGui.QFileDialog.getSaveFileName(QtGui.QApplication.activeWindow(), "Camotics Project File") + if foo: + tfile = foo[0] + else: + return None + + with open(tfile, 'w') as t: + proj=json.dumps(camoticstemplate, indent=2) + t.write(proj) + + return tfile #json.dumps(camoticstemplate, indent=2) + + +class CommandCamoticsSimulate: + def GetResources(self): + return {'Pixmap': 'Path_Camotics', + 'MenuText': QtCore.QT_TRANSLATE_NOOP("Path_Camotics", "Camotics"), + 'Accel': "P, C", + 'ToolTip': QtCore.QT_TRANSLATE_NOOP("Path_Camotics", "Simulate using Camotics"), + 'CmdType': "ForEdit"} + + def IsActive(self): + if bool(FreeCADGui.Selection.getSelection()) is False: + return False + try: + job = FreeCADGui.Selection.getSelectionEx()[0].Object + return isinstance(job.Proxy, PathScripts.PathJob.ObjectJob) + except: + return False + + def Activated(self): + pathSimulation.Activate() + + +pathSimulation = CamoticsSimulation() + +if FreeCAD.GuiUp: + FreeCADGui.addCommand('Path_Camotics', CommandCamoticsSimulate()) + + +FreeCAD.Console.PrintLog("Loading PathCamoticsSimulateGui ... done\n") diff --git a/src/Mod/Path/PathScripts/PathGuiInit.py b/src/Mod/Path/PathScripts/PathGuiInit.py index 405dcb4fcc..bf13af4b9a 100644 --- a/src/Mod/Path/PathScripts/PathGuiInit.py +++ b/src/Mod/Path/PathScripts/PathGuiInit.py @@ -71,6 +71,7 @@ def Startup(): from PathScripts import PathSetupSheetGui from PathScripts import PathSimpleCopy from PathScripts import PathSimulatorGui + from PathScripts import PathCamoticsGui from PathScripts import PathSlotGui from PathScripts import PathStop # from PathScripts import PathSurfaceGui # Added in initGui.py due to OCL dependency diff --git a/src/Mod/Path/PathScripts/PathPost.py b/src/Mod/Path/PathScripts/PathPost.py index fec6ddd3a5..530151b8d7 100644 --- a/src/Mod/Path/PathScripts/PathPost.py +++ b/src/Mod/Path/PathScripts/PathPost.py @@ -33,6 +33,7 @@ import PathScripts.PathPreferences as PathPreferences import PathScripts.PathUtil as PathUtil import PathScripts.PathUtils as PathUtils import os +import tempfile from PathScripts.PathPostProcessor import PostProcessor from PySide import QtCore, QtGui @@ -56,6 +57,187 @@ class _TempObject: Label = "Fixture" +def resolveFileName(job): + + path = PathPreferences.defaultOutputFile() + if job.PostProcessorOutputFile: + path = job.PostProcessorOutputFile + filename = path + + if '%D' in filename: + D = FreeCAD.ActiveDocument.FileName + if D: + D = os.path.dirname(D) + # in case the document is in the current working directory + if not D: + D = '.' + else: + FreeCAD.Console.PrintError("Please save document in order to resolve output path!\n") + return None + filename = filename.replace('%D', D) + + if '%d' in filename: + d = FreeCAD.ActiveDocument.Label + filename = filename.replace('%d', d) + + if '%j' in filename: + j = job.Label + filename = filename.replace('%j', j) + + if '%M' in filename: + pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Macro") + M = pref.GetString("MacroPath", FreeCAD.getUserAppDataDir()) + filename = filename.replace('%M', M) + + + policy = PathPreferences.defaultOutputPolicy() + + openDialog = policy == 'Open File Dialog' + if os.path.isdir(filename) or not os.path.isdir(os.path.dirname(filename)): + # Either the entire filename resolves into a directory or the parent directory doesn't exist. + # Either way I don't know what to do - ask for help + openDialog = True + + if os.path.isfile(filename) and not openDialog: + if policy == 'Open File Dialog on conflict': + openDialog = True + elif policy == 'Append Unique ID on conflict': + fn, ext = os.path.splitext(filename) + nr = fn[-3:] + n = 1 + if nr.isdigit(): + n = int(nr) + while os.path.isfile("%s%03d%s" % (fn, n, ext)): + n = n + 1 + filename = "%s%03d%s" % (fn, n, ext) + + if openDialog: + foo = QtGui.QFileDialog.getSaveFileName(QtGui.QApplication.activeWindow(), "Output File", filename) + if foo: + filename = foo[0] + else: + filename = None + + return filename + +def buildPostList(job): + ''' Takes the job and determines the specific objects and order to + postprocess Returns a list of objects which can be passed to + exportObjectsWith() for final posting''' + wcslist = job.Fixtures + orderby = job.OrderOutputBy + + postlist = [] + + if orderby == 'Fixture': + PathLog.debug("Ordering by Fixture") + # Order by fixture means all operations and tool changes will be completed in one + # fixture before moving to the next. + + currTool = None + for index, f in enumerate(wcslist): + # create an object to serve as the fixture path + fobj = _TempObject() + c1 = Path.Command(f) + fobj.Path = Path.Path([c1]) + if index != 0: + c2 = Path.Command("G0 Z" + str(job.Stock.Shape.BoundBox.ZMax + job.SetupSheet.ClearanceHeightOffset.Value)) + fobj.Path.addCommands(c2) + fobj.InList.append(job) + sublist = [fobj] + + # Now generate the gcode + for obj in job.Operations.Group: + tc = PathUtil.toolControllerForOp(obj) + if tc is not None and PathUtil.opProperty(obj, 'Active'): + if tc.ToolNumber != currTool: + sublist.append(tc) + PathLog.debug("Appending TC: {}".format(tc.Name)) + currTool = tc.ToolNumber + sublist.append(obj) + postlist.append(sublist) + + elif orderby == 'Tool': + PathLog.debug("Ordering by Tool") + # Order by tool means tool changes are minimized. + # all operations with the current tool are processed in the current + # fixture before moving to the next fixture. + + currTool = None + fixturelist = [] + for f in wcslist: + # create an object to serve as the fixture path + fobj = _TempObject() + c1 = Path.Command(f) + c2 = Path.Command("G0 Z" + str(job.Stock.Shape.BoundBox.ZMax + job.SetupSheet.ClearanceHeightOffset.Value)) + fobj.Path = Path.Path([c1, c2]) + fobj.InList.append(job) + fixturelist.append(fobj) + + # Now generate the gcode + curlist = [] # list of ops for tool, will repeat for each fixture + sublist = [] # list of ops for output splitting + + for idx, obj in enumerate(job.Operations.Group): + + # check if the operation is active + active = PathUtil.opProperty(obj, 'Active') + + tc = PathUtil.toolControllerForOp(obj) + if tc is None or tc.ToolNumber == currTool and active: + curlist.append(obj) + elif tc.ToolNumber != currTool and currTool is None and active: # first TC + sublist.append(tc) + curlist.append(obj) + currTool = tc.ToolNumber + elif tc.ToolNumber != currTool and currTool is not None and active: # TC + for fixture in fixturelist: + sublist.append(fixture) + sublist.extend(curlist) + postlist.append(sublist) + sublist = [tc] + curlist = [obj] + currTool = tc.ToolNumber + + if idx == len(job.Operations.Group) - 1: # Last operation. + for fixture in fixturelist: + sublist.append(fixture) + sublist.extend(curlist) + postlist.append(sublist) + + elif orderby == 'Operation': + PathLog.debug("Ordering by Operation") + # Order by operation means ops are done in each fixture in + # sequence. + currTool = None + firstFixture = True + + # Now generate the gcode + for obj in job.Operations.Group: + if PathUtil.opProperty(obj, 'Active'): + sublist = [] + PathLog.debug("obj: {}".format(obj.Name)) + for f in wcslist: + fobj = _TempObject() + c1 = Path.Command(f) + fobj.Path = Path.Path([c1]) + if not firstFixture: + c2 = Path.Command("G0 Z" + str(job.Stock.Shape.BoundBox.ZMax + job.SetupSheet.ClearanceHeightOffset.Value)) + fobj.Path.addCommands(c2) + fobj.InList.append(job) + sublist.append(fobj) + firstFixture = False + tc = PathUtil.toolControllerForOp(obj) + if tc is not None: + if job.SplitOutput or (tc.ToolNumber != currTool): + sublist.append(tc) + currTool = tc.ToolNumber + sublist.append(obj) + postlist.append(sublist) + + return postlist + + class DlgSelectPostProcessor: def __init__(self, parent=None): @@ -97,73 +279,6 @@ class CommandPathPost: # pylint: disable=no-init subpart = 1 - def resolveFileName(self, job): - path = PathPreferences.defaultOutputFile() - if job.PostProcessorOutputFile: - path = job.PostProcessorOutputFile - filename = path - - if '%D' in filename: - D = FreeCAD.ActiveDocument.FileName - if D: - D = os.path.dirname(D) - # in case the document is in the current working directory - if not D: - D = '.' - else: - FreeCAD.Console.PrintError("Please save document in order to resolve output path!\n") - return None - filename = filename.replace('%D', D) - - if '%d' in filename: - d = FreeCAD.ActiveDocument.Label - filename = filename.replace('%d', d) - - if '%j' in filename: - j = job.Label - filename = filename.replace('%j', j) - - if '%M' in filename: - pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Macro") - M = pref.GetString("MacroPath", FreeCAD.getUserAppDataDir()) - filename = filename.replace('%M', M) - - if '%s' in filename: - if job.SplitOutput: - filename = filename.replace('%s', '_'+str(self.subpart)) - self.subpart += 1 - else: - filename = filename.replace('%s', '') - - policy = PathPreferences.defaultOutputPolicy() - - openDialog = policy == 'Open File Dialog' - if os.path.isdir(filename) or not os.path.isdir(os.path.dirname(filename)): - # Either the entire filename resolves into a directory or the parent directory doesn't exist. - # Either way I don't know what to do - ask for help - openDialog = True - - if os.path.isfile(filename) and not openDialog: - if policy == 'Open File Dialog on conflict': - openDialog = True - elif policy == 'Append Unique ID on conflict': - fn, ext = os.path.splitext(filename) - nr = fn[-3:] - n = 1 - if nr.isdigit(): - n = int(nr) - while os.path.isfile("%s%03d%s" % (fn, n, ext)): - n = n + 1 - filename = "%s%03d%s" % (fn, n, ext) - - if openDialog: - foo = QtGui.QFileDialog.getSaveFileName(QtGui.QApplication.activeWindow(), "Output File", filename) - if foo: - filename = foo[0] - else: - filename = None - - return filename def resolvePostProcessor(self, job): if hasattr(job, "PostProcessor"): @@ -190,6 +305,37 @@ class CommandPathPost: return False + def getGcodeSilently(self, objs, job, temp=True): + '''This method will postprocess the given objects without opening dialogs + or prompting the user for any input. gcode is returned as a list of lists + suitable for splitting''' + PathLog.track('objs: {}'.format(objs)) + + postArgs = PathPreferences.defaultPostProcessorArgs() + if hasattr(job, "PostProcessorArgs") and job.PostProcessorArgs: + postArgs = job.PostProcessorArgs + elif hasattr(job, "PostProcessor") and job.PostProcessor: + postArgs = '' + + if not '--no-show-editor' in postArgs: + postArgs += ' --no-show-editor' + + PathLog.track('postArgs: {}'.format(postArgs)) + + if temp: + tfile = tempfile.NamedTemporaryFile() + filename = tfile.name + else: + filename = resolveFileName(job) + tfile = open(filename, "w") + + with tfile: + postname = self.resolvePostProcessor(job) + processor = PostProcessor.load(postname) + gcode = processor.export(objs, filename, postArgs) + return gcode, filename + + def exportObjectsWith(self, objs, job, needFilename=True): PathLog.track() # check if the user has a project and has set the default post and @@ -203,7 +349,7 @@ class CommandPathPost: postname = self.resolvePostProcessor(job) filename = '-' if postname and needFilename: - filename = self.resolveFileName(job) + filename = resolveFileName(job) if postname and filename: print("post: %s(%s, %s)" % (postname, filename, postArgs)) @@ -261,117 +407,9 @@ class CommandPathPost: PathLog.debug("about to postprocess job: {}".format(job.Name)) - wcslist = job.Fixtures - orderby = job.OrderOutputBy - split = job.SplitOutput + postlist = buildPostList(job) + filename = resolveFileName(job) - postlist = [] - - if orderby == 'Fixture': - PathLog.debug("Ordering by Fixture") - # Order by fixture means all operations and tool changes will be completed in one - # fixture before moving to the next. - - currTool = None - for index, f in enumerate(wcslist): - # create an object to serve as the fixture path - fobj = _TempObject() - c1 = Path.Command(f) - fobj.Path = Path.Path([c1]) - if index != 0: - c2 = Path.Command("G0 Z" + str(job.Stock.Shape.BoundBox.ZMax + job.SetupSheet.ClearanceHeightOffset.Value)) - fobj.Path.addCommands(c2) - fobj.InList.append(job) - sublist = [fobj] - - # Now generate the gcode - for obj in job.Operations.Group: - tc = PathUtil.toolControllerForOp(obj) - if tc is not None and PathUtil.opProperty(obj, 'Active'): - if tc.ToolNumber != currTool: - sublist.append(tc) - PathLog.debug("Appending TC: {}".format(tc.Name)) - currTool = tc.ToolNumber - sublist.append(obj) - postlist.append(sublist) - - elif orderby == 'Tool': - PathLog.debug("Ordering by Tool") - # Order by tool means tool changes are minimized. - # all operations with the current tool are processed in the current - # fixture before moving to the next fixture. - - currTool = None - fixturelist = [] - for f in wcslist: - # create an object to serve as the fixture path - fobj = _TempObject() - c1 = Path.Command(f) - c2 = Path.Command("G0 Z" + str(job.Stock.Shape.BoundBox.ZMax + job.SetupSheet.ClearanceHeightOffset.Value)) - fobj.Path = Path.Path([c1, c2]) - fobj.InList.append(job) - fixturelist.append(fobj) - - # Now generate the gcode - curlist = [] # list of ops for tool, will repeat for each fixture - sublist = [] # list of ops for output splitting - - for idx, obj in enumerate(job.Operations.Group): - - # check if the operation is active - active = PathUtil.opProperty(obj, 'Active') - - tc = PathUtil.toolControllerForOp(obj) - if tc is None or tc.ToolNumber == currTool and active: - curlist.append(obj) - elif tc.ToolNumber != currTool and currTool is None and active: # first TC - sublist.append(tc) - curlist.append(obj) - currTool = tc.ToolNumber - elif tc.ToolNumber != currTool and currTool is not None and active: # TC - for fixture in fixturelist: - sublist.append(fixture) - sublist.extend(curlist) - postlist.append(sublist) - sublist = [tc] - curlist = [obj] - currTool = tc.ToolNumber - - if idx == len(job.Operations.Group) - 1: # Last operation. - for fixture in fixturelist: - sublist.append(fixture) - sublist.extend(curlist) - postlist.append(sublist) - - elif orderby == 'Operation': - PathLog.debug("Ordering by Operation") - # Order by operation means ops are done in each fixture in - # sequence. - currTool = None - firstFixture = True - - # Now generate the gcode - for obj in job.Operations.Group: - if PathUtil.opProperty(obj, 'Active'): - sublist = [] - PathLog.debug("obj: {}".format(obj.Name)) - for f in wcslist: - fobj = _TempObject() - c1 = Path.Command(f) - fobj.Path = Path.Path([c1]) - if not firstFixture: - c2 = Path.Command("G0 Z" + str(job.Stock.Shape.BoundBox.ZMax + job.SetupSheet.ClearanceHeightOffset.Value)) - fobj.Path.addCommands(c2) - fobj.InList.append(job) - sublist.append(fobj) - firstFixture = False - tc = PathUtil.toolControllerForOp(obj) - if tc is not None: - if job.SplitOutput or (tc.ToolNumber != currTool): - sublist.append(tc) - currTool = tc.ToolNumber - sublist.append(obj) - postlist.append(sublist) fail = True rc = ''