From 6e766deb32ad87e90c8ef24fd760b73a03051943 Mon Sep 17 00:00:00 2001 From: Shai Seger Date: Tue, 24 Oct 2017 16:27:19 +0300 Subject: [PATCH] integrate simulator into path workbench --- src/Mod/Path/CMakeLists.txt | 2 + src/Mod/Path/Gui/Resources/Path.qrc | 1 + .../Gui/Resources/icons/Path-Simulator.svg | 1459 +++++++++++++++++ src/Mod/Path/PathScripts/PathSimulator.py | 479 ++++++ src/Mod/Path/PathScripts/TaskPathSimulator.ui | 219 +++ src/Mod/Path/PathSimulator/CMakeLists.txt | 7 +- 6 files changed, 2163 insertions(+), 4 deletions(-) create mode 100644 src/Mod/Path/Gui/Resources/icons/Path-Simulator.svg create mode 100644 src/Mod/Path/PathScripts/PathSimulator.py create mode 100644 src/Mod/Path/PathScripts/TaskPathSimulator.ui diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt index 67164ba86a..0c332580c6 100644 --- a/src/Mod/Path/CMakeLists.txt +++ b/src/Mod/Path/CMakeLists.txt @@ -87,7 +87,9 @@ SET(PathScripts_SRCS PathScripts/PathToolLibraryManager.py PathScripts/PathUtil.py PathScripts/PathUtils.py + PathScripts/PathSimulator.py PathScripts/PostUtils.py + PathScripts/TaskPathSimulator.ui PathScripts/__init__.py ) diff --git a/src/Mod/Path/Gui/Resources/Path.qrc b/src/Mod/Path/Gui/Resources/Path.qrc index 19875f199a..23cd4b8f89 100644 --- a/src/Mod/Path/Gui/Resources/Path.qrc +++ b/src/Mod/Path/Gui/Resources/Path.qrc @@ -52,6 +52,7 @@ icons/Path-Area.svg icons/Path-Area-View.svg icons/Path-Area-Workplane.svg + icons/Path-Simulator.svg icons/preferences-path.svg panels/DlgJobChooser.ui panels/DlgJobCreate.ui diff --git a/src/Mod/Path/Gui/Resources/icons/Path-Simulator.svg b/src/Mod/Path/Gui/Resources/icons/Path-Simulator.svg new file mode 100644 index 0000000000..ed0b48db6f --- /dev/null +++ b/src/Mod/Path/Gui/Resources/icons/Path-Simulator.svg @@ -0,0 +1,1459 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + Path-Machine + 2015-07-04 + http://www.freecadweb.org/wiki/index.php?title=Artwork + + + FreeCAD + + + FreeCAD/src/Mod/Path/Gui/Resources/icons/Path-Machine.svg + + + FreeCAD LGPL2+ + + + https://www.gnu.org/copyleft/lesser.html + + + [agryson] Alexander Gryson + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Mod/Path/PathScripts/PathSimulator.py b/src/Mod/Path/PathScripts/PathSimulator.py new file mode 100644 index 0000000000..45cbdf1f4c --- /dev/null +++ b/src/Mod/Path/PathScripts/PathSimulator.py @@ -0,0 +1,479 @@ +import os +_filePath = os.path.dirname(os.path.abspath(__file__)) + +import Path +import PathSimulator +import math +from FreeCAD import Vector, Base +from PathScripts.PathGeom import PathGeom +from PySide import QtGui, QtCore + +#compiled with pyrcc4 -py3 Resources\CAM_Sim.qrc -o CAM_Sim_rc.py +import CAM_Sim_rc + +class CAMSimTaskUi: + def __init__(self, parent): + # this will create a Qt widget from our ui file + self.form = FreeCADGui.PySideUic.loadUi(_filePath + "/TaskPathSimulator.ui") + self.parent = parent + + def accept(self): + self.parent.accept() + FreeCADGui.Control.closeDialog() + + def reject(self): + self.parent.cancel() + FreeCADGui.Control.closeDialog() + +#for cmd in obj.Path.Commands: +# if cmd.Name[0] == 'G': +# e1 = +# print cmd.Name + +def TSError(msg): + QtGui.QMessageBox.information(None,"Path Simulation",msg) + +class PathSimulation: + def __init__(self): + self.debug = False + self.timer = QtCore.QTimer() + QtCore.QObject.connect(self.timer, QtCore.SIGNAL("timeout()"), self.PerformCut) + self.stdrot = FreeCAD.Rotation(Vector(0,0,1),0) + + self.taskForm = CAMSimTaskUi(self) + form = self.taskForm.form + self.Connect(form.toolButtonStop, self.SimStop) + self.Connect(form.toolButtonPlay, self.SimPlay) + self.Connect(form.toolButtonPause, self.SimPause) + self.Connect(form.toolButtonStep, self.SimStep) + self.Connect(form.toolButtonFF, self.SimFF) + form.comboJobs.currentIndexChanged.connect(self.onJobChange) + + def Connect(self, but, sig): + QtCore.QObject.connect(but, QtCore.SIGNAL("clicked()"), sig) + + def UpdateProgress(self): + self.taskForm.form.progressBar.setValue(self.iprogress * 100 / self.numCommands) + + def Activate(self): + form = self.taskForm.form + jobList = FreeCAD.ActiveDocument.findObjects("Path::FeaturePython", "Job.*") + form.comboJobs.clear() + self.jobs = [] + for j in jobList: + self.jobs.append(j) + form.comboJobs.addItem(j.ViewObject.Icon, j.Label) + FreeCADGui.Control.showDialog(self.taskForm) + self.disableAnim = False + self.isVoxel = True + self.voxSim = PathSimulator.PathSim() + self.SimulateMill() + + def SetupSimulation(self): + form = self.taskForm.form + self.activeOps = [] + self.numCommands = 0 + for i in range(form.listOperations.count()): + if form.listOperations.item(i).checkState() == QtCore.Qt.CheckState.Checked: + self.activeOps.append(self.operations[i]) + self.numCommands += len(self.operations[i].Path.Commands) + if len(self.activeOps) == 0: + return 0 + self.stock = self.job.Stock.Shape + if (self.isVoxel): + self.voxSim.BeginSimulation(self.stock,0.5) + self.cutMaterial.Mesh = self.voxSim.GetResultMesh() + else: + self.cutMaterial.Shape = self.stock + self.busy = False + self.SetupOperation(0) + self.iprogress = 0 + self.UpdateProgress() + + def SetupOperation(self, itool): + self.operation = self.activeOps[itool] + self.tool = self.operation.ToolController.Tool + toolProf = self.CreateToolProfile(self.tool, Vector(0,1,0), Vector(0,0,0), self.tool.Diameter / 2.0) + self.cutTool.Shape = Part.makeSolid(toolProf.revolve(Vector(0,0,0), Vector(0,0,1))) + self.cutTool.ViewObject.show() + self.voxSim.SetCurrentTool(self.tool) + self.icmd = 0 + self.curpos = FreeCAD.Placement(self.initialPos, self.stdrot) + #self.cutTool.Placement = FreeCAD.Placement(self.curpos, self.stdrot) + self.cutTool.Placement = self.curpos + + + def SimulateMill(self): + self.job = self.jobs[self.taskForm.form.comboJobs.currentIndex()] + self.busy = False + #self.timer.start(100) + self.ioperation = 0 + self.height = 10 + self.skipStep = False + self.initialPos = Vector(0, 0, self.job.Stock.Shape.BoundBox.ZMax) + # Add cut tool + self.cutTool = FreeCAD.ActiveDocument.addObject("Part::FeaturePython","CutTool") + self.cutTool.ViewObject.Proxy = 0 + self.cutTool.ViewObject.hide() + + # Add cut material + if self.isVoxel: + self.cutMaterial = FreeCAD.ActiveDocument.addObject("Mesh::FeaturePython","CutMaterial") + else: + self.cutMaterial = FreeCAD.ActiveDocument.addObject("Part::FeaturePython","CutMaterial") + self.cutMaterial.Shape = self.job.Stock.Shape + self.cutMaterial.ViewObject.Proxy = 0 + self.cutMaterial.ViewObject.show() + + # Add cut path solid for debug + if self.debug: + self.cutSolid = FreeCAD.ActiveDocument.addObject("Part::FeaturePython","CutDebug") + self.cutSolid.ViewObject.Proxy = 0 + self.cutSolid.ViewObject.hide() + + FreeCAD.ActiveDocument.recompute() + self.resetSimulation = True + #self.dialog.show() + + def SkipStep(self): + self.skipStep = True + self.PerformCut() + + + def PerformCutBoolean(self): + if self.resetSimulation: + self.resetSimulation = False + self.SetupSimulation() + + if self.busy: + return + self.busy = True + + cmd = self.operation.Path.Commands[self.icmd] + #for cmd in job.Path.Commands: + pathSolid = None + if cmd.Name in ['G0']: + self.curpos = self.RapidMove(cmd, self.curpos) + if cmd.Name in ['G1', 'G2', 'G3']: + if self.skipStep: + self.curpos = self.RapidMove(cmd, self.curpos) + else: + (pathSolid, self.curpos) = self.GetPathSolid(self.tool, cmd, self.curpos) + self.skipStep = False + if pathSolid is not None: + if self.debug: + self.cutSolid.Shape = pathSolid + newStock = self.stock.cut([pathSolid], 1e-3) + try: + if newStock.isValid(): + self.stock = newStock.removeSplitter() + except: + if self.debug: + print "invalid cut at cmd #" + str(self.icmd) + if not self.disableAnim: + self.cutTool.Placement = FreeCAD.Placement(self.curpos, self.stdrot) + self.icmd += 1 + self.iprogress += 1 + self.UpdateProgress() + if self.icmd >= len(self.operation.Path.Commands): + #self.cutMaterial.Shape = self.stock.removeSplitter() + self.ioperation += 1 + if self.ioperation >= len(self.activeOps): + self.EndSimulation() + return + else: + self.SetupOperation(self.ioperation) + if not self.disableAnim: + self.cutMaterial.Shape = self.stock + self.busy = False + + def PerformCutVoxel(self): + if self.resetSimulation: + self.resetSimulation = False + self.SetupSimulation() + + if self.busy: + return + self.busy = True + + cmd = self.operation.Path.Commands[self.icmd] + #for cmd in job.Path.Commands: + if cmd.Name in ['G0', 'G1', 'G2', 'G3']: + self.curpos = self.voxSim.ApplyCommand(self.curpos, cmd) + if not self.disableAnim: + self.cutTool.Placement = self.curpos #FreeCAD.Placement(self.curpos, self.stdrot) + self.cutMaterial.Mesh = self.voxSim.GetResultMesh() + self.icmd += 1 + self.iprogress += 1 + self.UpdateProgress() + if self.icmd >= len(self.operation.Path.Commands): + #self.cutMaterial.Shape = self.stock.removeSplitter() + self.ioperation += 1 + if self.ioperation >= len(self.activeOps): + self.EndSimulation() + return + else: + self.SetupOperation(self.ioperation) + self.busy = False + + def PerformCut(self): + if (self.isVoxel): + self.PerformCutVoxel() + else: + self.PerformCutBoolean() + + def RapidMove(self, cmd, curpos): + path = PathGeom.edgeForCmd(cmd, curpos) # hack to overcome occ bug + if path is None: + return curpos + return path.valueAt(path.LastParameter) + + def GetPathSolidOld(self, tool, cmd, curpos): + e1 = PathGeom.edgeForCmd(cmd, curpos) + #curpos = e1.valueAt(e1.LastParameter) + n1 = e1.tangentAt(0) + n1[2] = 0.0 + try: + n1.normalize() + except: + return (None, e1.valueAt(e1.LastParameter)) + height = self.height + rad = tool.Diameter / 2.0 - 0.001 * curpos[2] # hack to overcome occ bug + if type(e1.Curve) is Part.Circle and e1.Curve.Radius <= rad: # hack to overcome occ bug + rad = e1.Curve.Radius - 0.001 + #return (None, e1.valueAt(e1.LastParameter)) + xf = n1[0] * rad + yf = n1[1] * rad + xp = curpos[0] + yp = curpos[1] + zp = curpos[2] + v1 = Vector(yf + xp, -xf + yp, zp) + v2 = Vector(yf + xp, -xf + yp, zp + height) + v3 = Vector(-yf + xp, xf + yp, zp + height) + v4 = Vector(-yf + xp, xf + yp, zp) + vc1 = Vector(xf + xp, yf + yp, zp) + vc2 = Vector(xf + xp, yf + yp, zp + height) + l1 = Part.makeLine(v1, v2) + l2 = Part.makeLine(v2, v3) + #l2 = Part.Edge(Part.Arc(v2, vc2, v3)) + l3 = Part.makeLine(v3, v4) + l4 = Part.makeLine(v4, v1) + #l4 = Part.Edge(Part.Arc(v4, vc1, v1)) + w1 = Part.Wire([l1, l2, l3, l4]) + w2 = Part.Wire(e1) + try: + ex1 = w2.makePipeShell([w1],True, True) + except: + #Part.show(w1) + #Part.show(w2) + return (None, e1.valueAt(e1.LastParameter)) + cyl1 = Part.makeCylinder(rad, height, curpos) + curpos = e1.valueAt(e1.LastParameter) + cyl2 = Part.makeCylinder(rad, height, curpos) + ex1s = Part.Solid(ex1) + f1 = ex1s.fuse([cyl1,cyl2]).removeSplitter() + return (f1, curpos) + + # get a solid representation of a tool going along path + def GetPathSolid(self, tool, cmd, pos): + toolPath = PathGeom.edgeForCmd(cmd, pos) + #curpos = e1.valueAt(e1.LastParameter) + startDir = toolPath.tangentAt(0) + startDir[2] = 0.0 + endPos = toolPath.valueAt(toolPath.LastParameter) + endDir = toolPath.tangentAt(toolPath.LastParameter) + try: + startDir.normalize() + endDir.normalize() + except: + return (None, endPos) + height = self.height + + # hack to overcome occ bugs + rad = tool.Diameter / 2.0 - 0.001 * pos[2] + #rad = rad + 0.001 * self.icmd + if type(toolPath.Curve) is Part.Circle and toolPath.Curve.Radius <= rad: + rad = toolPath.Curve.Radius - 0.01 * (pos[2] + 1) + return (None, endPos) + + # create the path shell + toolProf = self.CreateToolProfile(tool, startDir, pos, rad) + rotmat = Base.Matrix() + rotmat.move(pos.negative()) + rotmat.rotateZ(math.pi) + rotmat.move(pos) + mirroredProf = toolProf.transformGeometry(rotmat) + fullProf = Part.Wire([toolProf, mirroredProf]) + pathWire = Part.Wire(toolPath) + try: + pathShell = pathWire.makePipeShell([fullProf],False, True) + except: + if self.debug: + Part.show(pathWire) + Part.show(fullProf) + return (None, endPos) + + # create the start cup + startCup = toolProf.revolve(pos, Vector(0,0,1), -180) + + #create the end cup + endProf = self.CreateToolProfile(tool, endDir, endPos, rad) + endCup = endProf.revolve(endPos, Vector(0,0,1), 180) + + fullShell = Part.makeShell(startCup.Faces + pathShell.Faces + endCup.Faces) + return (Part.makeSolid(fullShell).removeSplitter(), endPos) + + # create radial profile of the tool (90 degrees to the direction of the path) + def CreateToolProfile(self, tool, dir, pos, rad): + type = tool.ToolType + #rad = tool.Diameter / 2.0 - 0.001 * pos[2] # hack to overcome occ bug + xf = dir[0] * rad + yf = dir[1] * rad + xp = pos[0] + yp = pos[1] + zp = pos[2] + h = tool.CuttingEdgeHeight + # common to all tools + vTR = Vector(xp + yf, yp - xf, zp + h) + vTC = Vector(xp, yp, zp + h) + vBC = Vector(xp, yp, zp) + lT = Part.makeLine(vTR, vTC) + res = None + if type == "ChamferMill": + ang = 90 - tool.CuttingEdgeAngle / 2.0 + if ang > 80: + ang = 80 + if ang < 0: + ang = 0 + h1 = math.tan(ang * math.pi / 180) * rad + if h1 > (h - 0.1): + h1 = h - 0.1 + vBR = Vector(xp + yf, yp - xf, zp + h1) + lR = Part.makeLine(vBR, vTR) + lB = Part.makeLine(vBC, vBR) + res = Part.Wire([lB, lR, lT]) + + elif type == "BallEndMill": + h1 = rad + if h1 >= h: + h1 = h - 0.1 + vBR = Vector(xp + yf, yp - xf, zp + h1) + r2 = h1 / 2.0 + h2 = rad - math.sqrt(rad * rad - r2 * r2) + vBCR = Vector(xp + yf / 2.0, yp - xf / 2.0, zp + h2) + cB = Part.Edge(Part.Arc(vBC, vBCR, vBR)) + lR = Part.makeLine(vBR, vTR) + res = Part.Wire([cB, lR, lT]) + + else: # default: assume type == "EndMill" + vBR = Vector(xp + yf, yp - xf, zp) + lR = Part.makeLine(vBR, vTR) + lB = Part.makeLine(vBC, vBR) + res = Part.Wire([lB, lR, lT]) + + return res + + def onJobChange(self): + form = self.taskForm.form + j = self.jobs[form.comboJobs.currentIndex()] + form.listOperations.clear() + self.operations = [] + for op in j.Operations.OutList: + listItem = QtGui.QListWidgetItem(op.ViewObject.Icon, op.Label) + listItem.setFlags(listItem.flags() | QtCore.Qt.ItemIsUserCheckable) + listItem.setCheckState(QtCore.Qt.CheckState.Checked) + self.operations.append(op) + form.listOperations.addItem(listItem) + + def GuiBusy(self, isBusy): + form = self.taskForm.form + #form.toolButtonStop.setEnabled() + form.toolButtonPlay.setEnabled(not isBusy) + form.toolButtonPause.setEnabled(isBusy) + form.toolButtonStep.setEnabled(not isBusy) + form.toolButtonFF.setEnabled(not isBusy) + + def EndSimulation(self): + self.UpdateProgress() + self.timer.stop() + self.GuiBusy(False) + self.ViewShape() + self.resetSimulation = True + + def SimStop(self): + self.cutTool.ViewObject.hide() + self.iprogress = 0 + self.EndSimulation() + + def SimFF(self): + self.GuiBusy(True) + self.timer.start(10) + self.disableAnim = True + + def SimStep(self): + self.disableAnim = False + self.PerformCut() + + def SimPlay(self): + self.disableAnim = False + self.GuiBusy(True) + self.timer.start(20) + + def ViewShape(self): + if self.isVoxel: + self.cutMaterial.Mesh = self.voxSim.GetResultMesh() + else: + self.cutMaterial.Shape = self.stock + + def SimPause(self): + if self.disableAnim: + self.ViewShape() + self.GuiBusy(False) + self.timer.stop() + + def RemoveTool(self): + if self.cutTool is None: + return + FreeCAD.ActiveDocument.removeObject(self.cutTool.Name) + self.cutTool = None + + def RemoveMaterial(self): + if self.cutMaterial is None: + return + FreeCAD.ActiveDocument.removeObject(self.cutMaterial.Name) + self.cutMaterial = None + + + def accept(self): + self.EndSimulation() + self.RemoveTool() + + def cancel(self): + self.EndSimulation() + self.RemoveTool() + self.RemoveMaterial() + + +class CommandPathSimulate: + + def GetResources(self): + return {'Pixmap': 'Path-Simulator', + 'MenuText': QtCore.QT_TRANSLATE_NOOP("Path_Simulator", "CAM Simulator"), + 'Accel': "P, I", + 'ToolTip': QtCore.QT_TRANSLATE_NOOP("Path_Simulator", "Simulate Path G-Code on stock")} + + def IsActive(self): + if FreeCAD.ActiveDocument is not None: + for o in FreeCAD.ActiveDocument.Objects: + if o.Name[:3] == "Job": + return True + return False + + def Activated(self): + pathSimulation.Activate() + + +pathSimulation = PathSimulation() +if FreeCAD.GuiUp: + # register the FreeCAD command + FreeCADGui.addCommand('Path_Sanity',CommandPathSanity()) diff --git a/src/Mod/Path/PathScripts/TaskPathSimulator.ui b/src/Mod/Path/PathScripts/TaskPathSimulator.ui new file mode 100644 index 0000000000..3a60df4f0c --- /dev/null +++ b/src/Mod/Path/PathScripts/TaskPathSimulator.ui @@ -0,0 +1,219 @@ + + + TaskPathSimulator + + + + 0 + 0 + 266 + 335 + + + + Dialog + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Stop running simulation + + + Stop + + + + :/Icons/BStop.svg:/Icons/BStop.svg + + + + 32 + 32 + + + + + + + + Activate / resume simulation + + + Play + + + + :/Icons/BPlay.svg:/Icons/BPlay.svg + + + + 32 + 32 + + + + + + + + Pause simulation + + + Pause + + + + :/Icons/BPause.svg:/Icons/BPause.svg + + + + 32 + 32 + + + + + + + + Single step simulation + + + Step + + + + :/Icons/BStep.svg:/Icons/BStep.svg + + + + 32 + 32 + + + + + + + + Run simulation till end without animation + + + Fast Forward + + + + :/Icons/BFastForward.svg:/Icons/BFastForward.svg + + + + 32 + 32 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + 0 + 0 + + + + + 16777215 + 5 + + + + false + + + + + + + + + + 50 + 16777215 + + + + Job: + + + + + + + + + + + + QAbstractItemView::NoSelection + + + + + + + QLabel { color: rgb(250, 100, 0) } + + + * Note: Due to inherent CAD engine limitations, inaccuracies may occur + + + true + + + + + + + + + + + SimStop() + SimPlay() + SimPause() + SimStep() + SimFF() + + diff --git a/src/Mod/Path/PathSimulator/CMakeLists.txt b/src/Mod/Path/PathSimulator/CMakeLists.txt index b0fed48a19..de72a72c4a 100644 --- a/src/Mod/Path/PathSimulator/CMakeLists.txt +++ b/src/Mod/Path/PathSimulator/CMakeLists.txt @@ -1,13 +1,12 @@ add_subdirectory(App) # if(BUILD_GUI) - # add_subdirectory(Gui) + # add_subdirectory(Gui) # endif(BUILD_GUI) # install( # FILES - # Init.py - # InitGui.py + # Gui/PathSimulator.py # DESTINATION - # Mod/PathSimulator + # Mod/Path # )