# -*- coding: utf-8 -*- # *************************************************************************** # * Copyright (c) 2017 Shai Seger * # * * # * 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 * # * * # *************************************************************************** """ 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 # 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 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): # this will create a Qt widget from our ui file 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 # sometimes a flat circle is without edge, so return edge with # same height and later a connecting edge will be interpolated 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(): # v1 = edge.firstVertex() # v2 = edge.lastVertex() # tp = "arc" if type(edge.Curve) is Part.Circle else "line" sideEdgeList.append(edge) # sort edges as a single 3d line on the x-z plane # first find the topmost edge edge, p1, p2 = self.FindTopMostEdge(sideEdgeList) profile = [RadiusAt(edge, p1), edge.valueAt(p1).z] endrad = 0.0 # one by one find all connecting edges while edge is not None: sideEdgeList.remove(edge) if isinstance(edge.Curve, Part.Circle): # if edge is curved, approximate it with lines based on resolution 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()] # self.SetupSimulation() def _populateJobSelection(self, form): """Make Job selection combobox""" setJobIdx = 0 jobName = "" jIdx = 0 # Get list of Job objects in active document jobList = FreeCAD.ActiveDocument.findObjects("Path::FeaturePython", "Job.*") jCnt = len(jobList) # Check if user has selected a specific job for simulation guiSelection = FreeCADGui.Selection.getSelectionEx() if guiSelection: # Identify job selected by user sel = guiSelection[0] if hasattr(sel.Object, "Proxy") and isinstance(sel.Object.Proxy, PathJob.ObjectJob): jobName = sel.Object.Name FreeCADGui.Selection.clearSelection() # populate the job selection combobox 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 # Pre-select GUI-selected job in the combobox 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", "New CAM Simulator"), "Accel": "P, N", "ToolTip": QtCore.QT_TRANSLATE_NOOP("CAM_Simulator", "Simulate 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: # register the FreeCAD command FreeCADGui.addCommand("CAM_SimulatorGL", CommandCAMSimulate()) FreeCAD.Console.PrintLog("Loading PathSimulator Gui... done\n")