| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | """ |
| | Command and task window handler for the OpenGL based CAM simulator |
| | """ |
| |
|
| |
|
| | import math |
| | import os |
| | import FreeCAD |
| | import Path.Base.Util as PathUtil |
| | import Path.Dressup.Utils as PathDressup |
| | import Path.Main.Job as PathJob |
| | from PathScripts import PathUtils |
| | import CAMSimulator |
| |
|
| | from FreeCAD import Vector, Placement, Rotation |
| |
|
| |
|
| | |
| | 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 |
| | from PySide.QtGui import QDialogButtonBox |
| |
|
| | _filePath = os.path.dirname(os.path.abspath(__file__)) |
| |
|
| |
|
| | def IsSame(x, y): |
| | """Check if two floats are the same within an epsilon""" |
| | return abs(x - y) < 0.0001 |
| |
|
| |
|
| | def RadiusAt(edge, p): |
| | """Find the tool radius within a point on its circumference""" |
| | x = edge.valueAt(p).x |
| | y = edge.valueAt(p).y |
| | return math.sqrt(x * x + y * y) |
| |
|
| |
|
| | class CAMSimTaskUi: |
| | """Handles the simulator task panel""" |
| |
|
| | def __init__(self, parent): |
| | |
| | self.form = FreeCADGui.PySideUic.loadUi(":/panels/TaskCAMSimulator.ui") |
| | self.parent = parent |
| |
|
| | def getStandardButtons(self, *_args): |
| | """Task panel needs only Close button""" |
| | return QDialogButtonBox.Close |
| |
|
| | def reject(self): |
| | """User Pressed the Close button""" |
| | self.parent.cancel() |
| | FreeCADGui.Control.closeDialog() |
| |
|
| |
|
| | def TSError(msg): |
| | """Display error message""" |
| | QtGui.QMessageBox.information(None, "Path Simulation", msg) |
| |
|
| |
|
| | class CAMSimulation: |
| | """Handles and prepares CAM jobs for simulation""" |
| |
|
| | def __init__(self): |
| | self.debug = False |
| | self.stdrot = FreeCAD.Rotation(Vector(0, 0, 1), 0) |
| | self.iprogress = 0 |
| | self.numCommands = 0 |
| | self.simperiod = 20 |
| | self.quality = 10 |
| | self.resetSimulation = False |
| | self.jobs = [] |
| | self.initdone = False |
| | self.taskForm = None |
| | self.disableAnim = False |
| | self.firstDrill = True |
| | self.millSim = None |
| | self.job = None |
| | self.activeOps = [] |
| | self.ioperation = 0 |
| | self.stock = None |
| | self.busy = False |
| | self.operations = [] |
| | self.baseShape = None |
| |
|
| | def Connect(self, but, sig): |
| | """Connect task panel buttons""" |
| | QtCore.QObject.connect(but, QtCore.SIGNAL("clicked()"), sig) |
| |
|
| | def FindClosestEdge(self, edges, px, pz): |
| | """Convert tool shape to tool profile needed by GL simulator""" |
| | for edge in edges: |
| | p1 = edge.FirstParameter |
| | p2 = edge.LastParameter |
| | rad1 = RadiusAt(edge, p1) |
| | z1 = edge.valueAt(p1).z |
| | if IsSame(px, rad1) and IsSame(pz, z1): |
| | return edge, p1, p2 |
| | rad2 = RadiusAt(edge, p2) |
| | z2 = edge.valueAt(p2).z |
| | if IsSame(px, rad2) and IsSame(pz, z2): |
| | return edge, p2, p1 |
| | |
| | |
| | if IsSame(pz, z1): |
| | return edge, p1, p2 |
| | if IsSame(pz, z2): |
| | return edge, p2, p1 |
| | return None, 0.0, 0.0 |
| |
|
| | def FindTopMostEdge(self, edges): |
| | """Examine tool solid edges and find the top most one""" |
| | maxz = -99999999.0 |
| | topedge = None |
| | top_p1 = 0.0 |
| | top_p2 = 0.0 |
| | for edge in edges: |
| | p1 = edge.FirstParameter |
| | p2 = edge.LastParameter |
| | z = edge.valueAt(p1).z |
| | if z > maxz: |
| | topedge = edge |
| | top_p1 = p1 |
| | top_p2 = p2 |
| | maxz = z |
| | z = edge.valueAt(p2).z |
| | if z > maxz: |
| | topedge = edge |
| | top_p1 = p2 |
| | top_p2 = p1 |
| | maxz = z |
| | return topedge, top_p1, top_p2 |
| |
|
| | def GetToolProfile(self, tool, resolution): |
| | """Get the edge profile of a tool solid. Basically locating the |
| | side edge that OCC creates on any revolved object |
| | """ |
| | originalPlacement = tool.Placement |
| | tool.Placement = Placement(Vector(0, 0, 0), Rotation(Vector(0, 0, 1), 0), Vector(0, 0, 0)) |
| | shape = tool.Shape |
| | tool.Placement = originalPlacement |
| | sideEdgeList = [] |
| | for _i, edge in enumerate(shape.Edges): |
| | if not edge.isClosed(): |
| | |
| | |
| | |
| | sideEdgeList.append(edge) |
| |
|
| | |
| |
|
| | |
| | edge, p1, p2 = self.FindTopMostEdge(sideEdgeList) |
| | profile = [RadiusAt(edge, p1), edge.valueAt(p1).z] |
| | endrad = 0.0 |
| | |
| | while edge is not None: |
| | sideEdgeList.remove(edge) |
| | if isinstance(edge.Curve, Part.Circle): |
| | |
| | nsegments = int(edge.Length / resolution) + 1 |
| | step = (p2 - p1) / nsegments |
| | location = p1 + step |
| | while nsegments > 0: |
| | endrad = RadiusAt(edge, location) |
| | endz = edge.valueAt(location).z |
| | profile.append(endrad) |
| | profile.append(endz) |
| | location += step |
| | nsegments -= 1 |
| | else: |
| | endrad = RadiusAt(edge, p2) |
| | endz = edge.valueAt(p2).z |
| | profile.append(endrad) |
| | profile.append(endz) |
| | edge, p1, p2 = self.FindClosestEdge(sideEdgeList, endrad, endz) |
| | if edge is None: |
| | break |
| | startrad = RadiusAt(edge, p1) |
| | if not IsSame(startrad, endrad): |
| | profile.append(startrad) |
| | startz = edge.valueAt(p1).z |
| | profile.append(startz) |
| |
|
| | return profile |
| |
|
| | def Activate(self): |
| | """Invoke the simulator task panel""" |
| | self.initdone = False |
| | self.taskForm = CAMSimTaskUi(self) |
| | form = self.taskForm.form |
| | self.Connect(form.toolButtonPlay, self.SimPlay) |
| | form.sliderAccuracy.valueChanged.connect(self.onAccuracyBarChange) |
| | self.onAccuracyBarChange() |
| | self._populateJobSelection(form) |
| | form.comboJobs.currentIndexChanged.connect(self.onJobChange) |
| | self.onJobChange() |
| | form.listOperations.itemChanged.connect(self.onOperationItemChange) |
| | FreeCADGui.Control.showDialog(self.taskForm) |
| | self.disableAnim = False |
| | self.firstDrill = True |
| | self.millSim = CAMSimulator.PathSim() |
| | self.initdone = True |
| | self.job = self.jobs[self.taskForm.form.comboJobs.currentIndex()] |
| | |
| |
|
| | def _populateJobSelection(self, form): |
| | """Make Job selection combobox""" |
| | setJobIdx = 0 |
| | jobName = "" |
| | jIdx = 0 |
| | |
| | jobList = FreeCAD.ActiveDocument.findObjects("Path::FeaturePython", "Job.*") |
| | jCnt = len(jobList) |
| |
|
| | |
| | guiSelection = FreeCADGui.Selection.getSelectionEx() |
| | if guiSelection: |
| | sel = guiSelection[0] |
| | if hasattr(sel.Object, "Proxy") and isinstance(sel.Object.Proxy, PathJob.ObjectJob): |
| | jobName = sel.Object.Name |
| | FreeCADGui.Selection.clearSelection() |
| |
|
| | |
| | form.comboJobs.blockSignals(True) |
| | form.comboJobs.clear() |
| | form.comboJobs.blockSignals(False) |
| | for j in jobList: |
| | form.comboJobs.addItem(j.ViewObject.Icon, j.Label) |
| | self.jobs.append(j) |
| | if j.Name == jobName or jCnt == 1: |
| | setJobIdx = jIdx |
| | jIdx += 1 |
| |
|
| | |
| | if jobName or jCnt == 1: |
| | form.comboJobs.setCurrentIndex(setJobIdx) |
| | else: |
| | form.comboJobs.setCurrentIndex(0) |
| |
|
| | def SetupSimulation(self): |
| | """Prepare all selected job operations for simulation""" |
| | 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 |
| | self.busy = False |
| |
|
| | def onJobChange(self): |
| | """When a new job is selected from the drop-down, update job operation list""" |
| | form = self.taskForm.form |
| | j = self.jobs[form.comboJobs.currentIndex()] |
| | self.job = j |
| | form.listOperations.clear() |
| | self.operations = [] |
| | for op in j.Operations.OutList: |
| | if PathUtil.opProperty(op, "Active"): |
| | 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 len(j.Model.OutList) > 0: |
| | self.baseShape = j.Model.OutList[0].Shape |
| | else: |
| | self.baseShape = None |
| |
|
| | def onAccuracyBarChange(self): |
| | """Update simulation quality""" |
| | form = self.taskForm.form |
| | self.quality = form.sliderAccuracy.value() |
| | qualText = QtCore.QT_TRANSLATE_NOOP("CAM_Simulator", "High") |
| | if self.quality < 4: |
| | qualText = QtCore.QT_TRANSLATE_NOOP("CAM_Simulator", "Low") |
| | elif self.quality < 9: |
| | qualText = QtCore.QT_TRANSLATE_NOOP("CAM_Simulator", "Medium") |
| | form.labelAccuracy.setText(qualText) |
| |
|
| | def onOperationItemChange(self, _item): |
| | """Check if at least one operation is selected to enable the Play button""" |
| | playvalid = False |
| | form = self.taskForm.form |
| | for i in range(form.listOperations.count()): |
| | if form.listOperations.item(i).checkState() == QtCore.Qt.CheckState.Checked: |
| | playvalid = True |
| | break |
| | form.toolButtonPlay.setEnabled(playvalid) |
| |
|
| | def SimPlay(self): |
| | """Activate the simulation""" |
| | self.SetupSimulation() |
| | self.millSim.ResetSimulation() |
| | for op in self.activeOps: |
| | tool = PathDressup.toolController(op).Tool |
| | toolNumber = PathDressup.toolController(op).ToolNumber |
| | toolProfile = self.GetToolProfile(tool, 0.5) |
| | self.millSim.AddTool(toolProfile, toolNumber, tool.Diameter, 1) |
| | opCommands = PathUtils.getPathWithPlacement(op).Commands |
| | for cmd in opCommands: |
| | self.millSim.AddCommand(cmd) |
| | self.millSim.BeginSimulation(self.stock, self.quality) |
| | if self.baseShape is not None: |
| | self.millSim.SetBaseShape(self.baseShape, 1) |
| |
|
| | def cancel(self): |
| | """Cancel the simulation""" |
| |
|
| |
|
| | class CommandCAMSimulate: |
| | """FreeCAD invoke simulation task panel command""" |
| |
|
| | def GetResources(self): |
| | """Command info""" |
| | return { |
| | "Pixmap": "CAM_SimulatorGL", |
| | "MenuText": QtCore.QT_TRANSLATE_NOOP("CAM_Simulator", "CAM Simulator"), |
| | "Accel": "P, N", |
| | "ToolTip": QtCore.QT_TRANSLATE_NOOP("CAM_Simulator", "Simulates G-code on stock"), |
| | } |
| |
|
| | def IsActive(self): |
| | """Command is active if at least one CAM job exists""" |
| | if FreeCAD.ActiveDocument is not None: |
| | for o in FreeCAD.ActiveDocument.Objects: |
| | if o.Name[:3] == "Job": |
| | return True |
| | return False |
| |
|
| | def Activated(self): |
| | """Activate the simulation""" |
| | CamSimulation = CAMSimulation() |
| | CamSimulation.Activate() |
| |
|
| |
|
| | if FreeCAD.GuiUp: |
| | |
| | FreeCADGui.addCommand("CAM_SimulatorGL", CommandCAMSimulate()) |
| | FreeCAD.Console.PrintLog("Loading PathSimulator Gui… done\n") |
| |
|