Files
create/src/Mod/Path/PathScripts/PathSimulatorGui.py
Eric Trombly 7fe094ac02 add lazyloader support
lazy_loader is copied to Ext now, modified external imports to lazy_load

add a few more imports to be lazy loaded, think the install path is correct now

[TD]"<" symbol embedded in html

revert changes to path modules for testing

use lazyloader in PathAreaOp.py

add back in deferred loading

temp change to print error message in tests

temp change to print error message in tests

add _init__.py to lazy_loader

make install in CMakeLists.txt one line
2020-04-13 21:14:34 -05:00

585 lines
21 KiB
Python

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")