import FreeCAD import Path import PathScripts.PathDressup as PathDressup import PathScripts.PathGeom as PathGeom import PathScripts.PathLog as PathLog import PathSimulator import math import os from FreeCAD import Vector, Base _filePath = os.path.dirname(os.path.abspath(__file__)) # lazily loaded modules from lazy_loader.lazy_loader import LazyLoader Mesh = LazyLoader('Mesh', globals(), 'Mesh') Part = LazyLoader('Part', globals(), 'Part') if FreeCAD.GuiUp: import FreeCADGui from PySide import QtGui, QtCore # compiled with pyrcc4 -py3 Resources\CAM_Sim.qrc -o CAM_Sim_rc.py class CAMSimTaskUi: def __init__(self, parent): # this will create a Qt widget from our ui file self.form = FreeCADGui.PySideUic.loadUi(":/panels/TaskPathSimulator.ui") self.parent = parent def accept(self): self.parent.accept() FreeCADGui.Control.closeDialog() def reject(self): self.parent.cancel() FreeCADGui.Control.closeDialog() 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.iprogress = 0 self.numCommands = 0 self.simperiod = 20 self.accuracy = 0.1 self.resetSimulation = False def Connect(self, but, sig): QtCore.QObject.connect(but, QtCore.SIGNAL("clicked()"), sig) def UpdateProgress(self): if self.numCommands > 0: self.taskForm.form.progressBar.setValue(self.iprogress * 100 / self.numCommands) def Activate(self): self.initdone = False 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.sliderSpeed.valueChanged.connect(self.onSpeedBarChange) self.onSpeedBarChange() form.sliderAccuracy.valueChanged.connect(self.onAccuracyBarChange) self.onAccuracyBarChange() form.comboJobs.currentIndexChanged.connect(self.onJobChange) 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.firstDrill = True self.voxSim = PathSimulator.PathSim() self.SimulateMill() self.initdone = True def SetupSimulation(self): form = self.taskForm.form self.activeOps = [] self.numCommands = 0 self.ioperation = 0 for i in range(form.listOperations.count()): if form.listOperations.item(i).checkState() == QtCore.Qt.CheckState.Checked: self.firstDrill = True self.activeOps.append(self.operations[i]) self.numCommands += len(self.operations[i].Path.Commands) self.stock = self.job.Stock.Shape if (self.isVoxel): maxlen = self.stock.BoundBox.XLength if (maxlen < self.stock.BoundBox.YLength): maxlen = self.stock.BoundBox.YLength self.voxSim.BeginSimulation(self.stock, 0.01 * self.accuracy * maxlen) (self.cutMaterial.Mesh, self.cutMaterialIn.Mesh) = self.voxSim.GetResultMesh() else: self.cutMaterial.Shape = self.stock self.busy = False self.tool = None for i in range(len(self.activeOps)): self.SetupOperation(0) if (self.tool is not None): break self.iprogress = 0 self.UpdateProgress() def SetupOperation(self, itool): self.operation = self.activeOps[itool] try: self.tool = PathDressup.toolController(self.operation).Tool except Exception: self.tool = None # if hasattr(self.operation, "ToolController"): # self.tool = self.operation.ToolController.Tool if (self.tool is not None): toolProf = self.CreateToolProfile(self.tool, Vector(0, 1, 0), Vector(0, 0, 0), float(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 self.opCommands = self.operation.Path.Commands def SimulateMill(self): self.job = self.jobs[self.taskForm.form.comboJobs.currentIndex()] self.busy = False # self.timer.start(100) 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") self.cutMaterialIn = FreeCAD.ActiveDocument.addObject("Mesh::FeaturePython", "CutMaterialIn") self.cutMaterialIn.ViewObject.Proxy = 0 self.cutMaterialIn.ViewObject.show() self.cutMaterialIn.ViewObject.ShapeColor = (1.0, 0.85, 0.45, 0.0) 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() self.cutMaterial.ViewObject.ShapeColor = (0.5, 0.25, 0.25, 0.0) # 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() self.SetupSimulation() self.resetSimulation = True FreeCAD.ActiveDocument.recompute() # 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) if cmd.Name in ['G81', 'G82', 'G83']: if self.firstDrill: extendcommand = Path.Command('G0', {"X": 0.0, "Y": 0.0, "Z": cmd.r}) self.curpos = self.RapidMove(extendcommand, self.curpos) self.firstDrill = False extendcommand = Path.Command('G0', {"X": cmd.x, "Y": cmd.y, "Z": cmd.r}) self.curpos = self.RapidMove(extendcommand, self.curpos) extendcommand = Path.Command('G1', {"X": cmd.x, "Y": cmd.y, "Z": cmd.z}) self.curpos = self.RapidMove(extendcommand, self.curpos) extendcommand = Path.Command('G1', {"X": cmd.x, "Y": cmd.y, "Z": cmd.r}) self.curpos = self.RapidMove(extendcommand, 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 Exception: if self.debug: print("invalid cut at cmd #{}".format(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.opCommands[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.cutMaterialIn.Mesh) = self.voxSim.GetResultMesh() if cmd.Name in ['G81', 'G82', 'G83']: extendcommands = [] if self.firstDrill: extendcommands.append(Path.Command('G0', {"X": 0.0, "Y": 0.0, "Z": cmd.r})) self.firstDrill = False extendcommands.append(Path.Command('G0', {"X": cmd.x, "Y": cmd.y, "Z": cmd.r})) extendcommands.append(Path.Command('G1', {"X": cmd.x, "Y": cmd.y, "Z": cmd.z})) extendcommands.append(Path.Command('G1', {"X": cmd.x, "Y": cmd.y, "Z": cmd.r})) for ecmd in extendcommands: self.curpos = self.voxSim.ApplyCommand(self.curpos, ecmd) if not self.disableAnim: self.cutTool.Placement = self.curpos # FreeCAD.Placement(self.curpos, self.stdrot) (self.cutMaterial.Mesh, self.cutMaterialIn.Mesh) = self.voxSim.GetResultMesh() self.icmd += 1 self.iprogress += 1 self.UpdateProgress() if self.icmd >= len(self.opCommands): # 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 = float(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 Exception: return (None, endPos) # height = self.height # hack to overcome occ bugs rad = float(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 Exception: 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 = float(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 if h <= 0.0: # set default if user fails to avoid freeze h = 1.0 PathLog.error("SET Tool Length") # 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()] self.job = j 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) if self.initdone: self.SetupSimulation() def onSpeedBarChange(self): form = self.taskForm.form self.simperiod = 1000 / form.sliderSpeed.value() form.labelGPerSec.setText(str(form.sliderSpeed.value()) + " G/s") # if (self.timer.isActive()): self.timer.setInterval(self.simperiod) def onAccuracyBarChange(self): form = self.taskForm.form self.accuracy = 1.1 - 0.1 * form.sliderAccuracy.value() form.labelAccuracy.setText(str(round(self.accuracy, 1)) + "%") 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 InvalidOperation(self): if len(self.activeOps) == 0: return True if (self.tool is None): TSError("No tool assigned for the operation") return True return False def SimFF(self): if self.InvalidOperation(): return self.GuiBusy(True) self.timer.start(1) self.disableAnim = True def SimStep(self): if self.InvalidOperation(): return self.disableAnim = False self.PerformCut() def SimPlay(self): if self.InvalidOperation(): return self.disableAnim = False self.GuiBusy(True) self.timer.start(self.simperiod) def ViewShape(self): if self.isVoxel: (self.cutMaterial.Mesh, self.cutMaterialIn.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 RemoveInnerMaterial(self): if self.cutMaterialIn is not None: if self.isVoxel and self.cutMaterial is not None: mesh = Mesh.Mesh() mesh.addMesh(self.cutMaterial.Mesh) mesh.addMesh(self.cutMaterialIn.Mesh) self.cutMaterial.Mesh = mesh FreeCAD.ActiveDocument.removeObject(self.cutMaterialIn.Name) self.cutMaterialIn = None def RemoveMaterial(self): if self.cutMaterial is not None: FreeCAD.ActiveDocument.removeObject(self.cutMaterial.Name) self.cutMaterial = None self.RemoveInnerMaterial() def accept(self): self.EndSimulation() self.RemoveInnerMaterial() 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, M", '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_Simulator', CommandPathSimulate()) FreeCAD.Console.PrintLog("Loading PathSimulator Gui... done\n")