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 @@
+
+
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 = ''