# SPDX-License-Identifier: LGPL-2.1-or-later # *************************************************************************** # * Copyright (c) 2019 sliptonic * # * 2025 Samuel Abels * # * * # * 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 * # * * # *************************************************************************** from lazy_loader.lazy_loader import LazyLoader from PySide import QtCore, QtGui from PySide.QtCore import QT_TRANSLATE_NOOP import FreeCAD import FreeCADGui import Path import Path.Base.Gui.Util as PathGuiUtil import Path.Base.Util as PathUtil import Path.Tool.Controller as PathToolController from Path.Tool.toolbit.ui.selector import ToolBitSelector Part = LazyLoader("Part", globals(), "Part") if False: Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) Path.Log.trackModule(Path.Log.thisModule()) else: Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule()) translate = FreeCAD.Qt.translate class ViewProvider: def __init__(self, vobj): vobj.Proxy = self self.vobj = vobj def attach(self, vobj): mode = 2 vobj.setEditorMode("LineWidth", mode) vobj.setEditorMode("MarkerColor", mode) vobj.setEditorMode("NormalColor", mode) vobj.setEditorMode("DisplayMode", mode) vobj.setEditorMode("BoundingBox", mode) vobj.setEditorMode("Selectable", mode) vobj.setEditorMode("ShapeAppearance", mode) vobj.setEditorMode("Transparency", mode) vobj.setEditorMode("Visibility", mode) self.vobj = vobj def dumps(self): return None def loads(self, state): return None def getIcon(self): return ":/icons/CAM_ToolController.svg" def onChanged(self, vobj, prop): mode = 2 vobj.setEditorMode("LineWidth", mode) vobj.setEditorMode("MarkerColor", mode) vobj.setEditorMode("NormalColor", mode) vobj.setEditorMode("DisplayMode", mode) vobj.setEditorMode("BoundingBox", mode) vobj.setEditorMode("Selectable", mode) def onDelete(self, vobj, args=None): PathUtil.clearExpressionEngine(vobj.Object) self.vobj.Object.Proxy.onDelete(vobj.Object, args) return True def updateData(self, vobj, prop): # this is executed when a property of the APP OBJECT changes pass def setEdit(self, vobj=None, mode=0): if 0 == mode: if vobj is None: vobj = self.vobj FreeCADGui.Control.closeDialog() taskd = TaskPanel(vobj.Object) FreeCADGui.Control.showDialog(taskd) taskd.setupUi() FreeCAD.ActiveDocument.recompute() return True return False def unsetEdit(self, vobj, mode): # this is executed when the user cancels or terminates edit mode return False def setupContextMenu(self, vobj, menu): Path.Log.track() for action in menu.actions(): menu.removeAction(action) action = QtGui.QAction(translate("CAM", "Edit"), menu) action.triggered.connect(self._editInContextMenuTriggered) menu.addAction(action) def _editInContextMenuTriggered(self, checked): self.setEdit() def claimChildren(self): obj = self.vobj.Object if obj and obj.Proxy and obj.Tool: return [obj.Tool] return [] def Create(name="Default Tool", tool=None, toolNumber=1): Path.Log.track(tool, toolNumber) obj = PathToolController.Create(name, tool, toolNumber) ViewProvider(obj.ViewObject) # ToolBits are visible by default, which is typically not what the user wants if tool and tool.ViewObject and tool.ViewObject.Visibility: tool.ViewObject.Visibility = False return obj class CommandPathToolController(object): def GetResources(self): return { "Pixmap": "CAM_LengthOffset", "MenuText": QT_TRANSLATE_NOOP("CAM_ToolController", "Tool Controller"), "ToolTip": QT_TRANSLATE_NOOP( "CAM_ToolController", "Adds a new tool controller to the active job" ), } def selectedJob(self): if FreeCAD.ActiveDocument: sel = FreeCADGui.Selection.getSelectionEx() if sel and sel[0].Object.Name[:3] == "Job": return sel[0].Object jobs = [o for o in FreeCAD.ActiveDocument.Objects if o.Name[:3] == "Job"] if 1 == len(jobs): return jobs[0] return None def IsActive(self): return self.selectedJob() is not None def Activated(self): Path.Log.track() job = self.selectedJob() if not job: return # Let the user select a toolbit selector = ToolBitSelector() if not selector.exec_(): return tool = selector.get_selected_tool() if not tool: return # Find a tool number toolNr = None for tc in job.Tools.Group: if tc.Tool == tool: toolNr = tc.ToolNumber break if not toolNr: toolNr = max([tc.ToolNumber for tc in job.Tools.Group]) + 1 # Create the new tool controller with the tool. tc = Create("TC: {}".format(tool.Label), tool, toolNr) job.Proxy.addToolController(tc) FreeCAD.ActiveDocument.recompute() class BlockScrollWheel(QtCore.QObject): def eventFilter(self, obj, event): if event.type() == QtCore.QEvent.Type.Wheel: if not obj.hasFocus(): return True return super().eventFilter(obj, event) class ToolControllerEditor(object): def __init__( self, obj, asDialog, notifyChanged=None, showCountLabel=False, disableToolNumber=False ): self.notifyChanged = notifyChanged self.form = FreeCADGui.PySideUic.loadUi(":/panels/DlgToolControllerEdit.ui") self.controller = FreeCADGui.PySideUic.loadUi(":/panels/ToolControllerEdit.ui") self.form.tc_layout.addWidget(self.controller) if not asDialog: self.form.buttonBox.hide() if not showCountLabel: self.controller.tcOperationCountLabel.hide() self.obj = obj comboToPropertyMap = [("spindleDirection", "SpindleDir")] enumTups = PathToolController.ToolController.propertyEnumerations(dataType="raw") PathGuiUtil.populateCombobox(self.controller, enumTups, comboToPropertyMap) self.vertFeed = PathGuiUtil.QuantitySpinBox(self.controller.vertFeed, obj, "VertFeed") self.horizFeed = PathGuiUtil.QuantitySpinBox(self.controller.horizFeed, obj, "HorizFeed") self.leadInFeed = PathGuiUtil.QuantitySpinBox(self.controller.leadInFeed, obj, "LeadInFeed") self.leadOutFeed = PathGuiUtil.QuantitySpinBox( self.controller.leadOutFeed, obj, "LeadOutFeed" ) self.rampFeed = PathGuiUtil.QuantitySpinBox(self.controller.rampFeed, obj, "RampFeed") self.vertRapid = PathGuiUtil.QuantitySpinBox(self.controller.vertRapid, obj, "VertRapid") self.horizRapid = PathGuiUtil.QuantitySpinBox(self.controller.horizRapid, obj, "HorizRapid") self.blockScrollWheel = BlockScrollWheel() self.controller.tcNumber.installEventFilter(self.blockScrollWheel) self.controller.spindleDirection.installEventFilter(self.blockScrollWheel) self.controller.tcNumber.setReadOnly(disableToolNumber) self.editor = None def selectInComboBox(self, name, combo): """selectInComboBox(name, combo) ... helper function to select a specific value in a combo box.""" try: combo.blockSignals(True) index = combo.currentIndex() # Save initial index # Search using currentData and return if found newindex = combo.findData(name) if newindex >= 0: combo.setCurrentIndex(newindex) return # if not found, search using current text newindex = combo.findText(name, QtCore.Qt.MatchFixedString) if newindex >= 0: combo.setCurrentIndex(newindex) return # not found, return unchanged combo.setCurrentIndex(index) finally: combo.blockSignals(False) def updateUi(self): tc = self.obj blockObjects = [ self.controller.tcName, self.controller.tcNumber, self.horizFeed.widget, self.horizRapid.widget, self.leadInFeed.widget, self.leadOutFeed.widget, self.rampFeed.widget, self.vertFeed.widget, self.vertRapid.widget, self.controller.spindleSpeed, self.controller.spindleDirection, ] try: for obj in blockObjects: obj.blockSignals(True) self.controller.tcName.setText(tc.Label) self.controller.tcNumber.setValue(tc.ToolNumber) self.horizFeed.updateWidget() self.horizRapid.updateWidget() self.leadInFeed.updateWidget() self.leadOutFeed.updateWidget() self.rampFeed.updateWidget() self.vertFeed.updateWidget() self.vertRapid.updateWidget() self.controller.spindleSpeed.setValue(tc.SpindleSpeed) self.selectInComboBox(tc.SpindleDir, self.controller.spindleDirection) if self.editor: self.editor.updateUI() finally: for obj in blockObjects: obj.blockSignals(False) def updateToolController(self): tc = self.obj try: tc.Label = self.controller.tcName.text() tc.ToolNumber = self.controller.tcNumber.value() self.horizFeed.updateProperty() self.vertFeed.updateProperty() self.leadInFeed.updateProperty() self.leadOutFeed.updateProperty() self.rampFeed.updateProperty() self.horizRapid.updateProperty() self.vertRapid.updateProperty() tc.SpindleSpeed = self.controller.spindleSpeed.value() tc.SpindleDir = self.controller.spindleDirection.currentData() if self.editor: self.editor.updateTool() tc.Tool = self.editor.tool except Exception as e: Path.Log.error("Error updating TC: {}".format(e)) def changed(self): self.form.blockSignals(True) self.controller.blockSignals(True) self.updateToolController() self.updateUi() self.controller.blockSignals(False) self.form.blockSignals(False) if self.notifyChanged: self.notifyChanged() def setupUi(self): if self.editor: self.editor.setupUI() self.controller.tcName.textChanged.connect(self.changed) self.controller.tcNumber.editingFinished.connect(self.changed) self.vertFeed.widget.textChanged.connect(self.changed) self.horizFeed.widget.textChanged.connect(self.changed) self.leadInFeed.widget.textChanged.connect(self.changed) self.leadOutFeed.widget.textChanged.connect(self.changed) self.rampFeed.widget.textChanged.connect(self.changed) self.vertRapid.widget.textChanged.connect(self.changed) self.horizRapid.widget.textChanged.connect(self.changed) self.controller.spindleSpeed.editingFinished.connect(self.changed) self.controller.spindleDirection.currentIndexChanged.connect(self.changed) class TaskPanel: def __init__(self, obj): self.editor = ToolControllerEditor(obj, False) self.form = self.editor.form self.updating = False self.toolrep = None self.obj = obj def accept(self): self.getFields() FreeCADGui.ActiveDocument.resetEdit() FreeCADGui.Control.closeDialog() if self.toolrep: FreeCAD.ActiveDocument.removeObject(self.toolrep.Name) FreeCAD.ActiveDocument.recompute() def reject(self): FreeCADGui.Control.closeDialog() if self.toolrep: FreeCAD.ActiveDocument.removeObject(self.toolrep.Name) FreeCAD.ActiveDocument.recompute() def getFields(self): self.editor.updateToolController() self.obj.Proxy.execute(self.obj) def setFields(self): self.editor.updateUi() if self.toolrep: tool = self.obj.Tool radius = float(tool.Diameter) / 2 length = tool.CuttingEdgeHeight t = Part.makeCylinder(radius, length) self.toolrep.Shape = t def edit(self, item, column): if not self.updating: self.resetObject() def resetObject(self, remove=None): "transfers the values from the widget to the object" FreeCAD.ActiveDocument.recompute() def setupUi(self): if self.editor.editor: t = Part.makeCylinder(1, 1) self.toolrep = FreeCAD.ActiveDocument.addObject("Part::Feature", "tool") self.toolrep.Shape = t self.setFields() self.editor.setupUi() class DlgToolControllerEdit: def __init__(self, obj): self.editor = ToolControllerEditor(obj, True) self.editor.updateUi() self.editor.setupUi() self.obj = obj def exec_(self): restoreTC = self.obj.Proxy.templateAttrs(self.obj) rc = False if not self.editor.form.exec_(): Path.Log.info("revert") self.obj.Proxy.setFromTemplate(self.obj, restoreTC) rc = True else: self.editor.updateToolController() self.obj.Proxy.execute(self.obj) return rc if FreeCAD.GuiUp: # register the FreeCAD command FreeCADGui.addCommand("CAM_ToolController", CommandPathToolController()) FreeCAD.Console.PrintLog("Loading PathToolControllerGui… done\n")