freecad-cam/Mod/CAM/Path/Op/Gui/Base.py
2026-02-01 01:59:24 +01:00

1411 lines
54 KiB
Python

# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2017 sliptonic <shopinthewoods@gmail.com> *
# * *
# * 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 *
# * *
# ***************************************************************************
import FreeCAD
import FreeCADGui
import Path
import Path.Base.Gui.GetPoint as PathGetPoint
import Path.Base.Gui.Util as PathGuiUtil
import Path.Base.SetupSheet as PathSetupSheet
import Path.Base.Util as PathUtil
import Path.Main.Job as PathJob
import Path.Op.Base as PathOp
import Path.Op.Gui.Selection as PathSelection
import PathGui
import PathScripts.PathUtils as PathUtils
import importlib
from PySide.QtCore import QT_TRANSLATE_NOOP
from PySide import QtCore, QtGui
__title__ = "CAM Operation UI base classes"
__author__ = "sliptonic (Brad Collette)"
__url__ = "https://www.freecad.org"
__doc__ = "Base classes and framework for CAM operation's UI"
translate = FreeCAD.Qt.translate
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())
class ViewProvider(object):
"""
Generic view provider for path objects.
Deducts the icon name from operation name, brings up the TaskPanel
with pages corresponding to the operation's opFeatures() and forwards
property change notifications to the page controllers.
"""
def __init__(self, vobj, resources):
Path.Log.track()
self.deleteOnReject = True
self.OpIcon = ":/icons/%s.svg" % resources.pixmap
self.OpName = resources.name
self.OpPageModule = resources.opPageClass.__module__
self.OpPageClass = resources.opPageClass.__name__
# initialized later
self.vobj = vobj
self.Object = None
self.panel = None
def attach(self, vobj):
Path.Log.track()
self.vobj = vobj
self.Object = vobj.Object
self.panel = None
return
def deleteObjectsOnReject(self):
"""
deleteObjectsOnReject() ... return true if all objects should
be created if the user hits cancel. This is used during the initial
edit session, if the user does not press OK, it is assumed they've
changed their mind about creating the operation.
"""
Path.Log.track()
return hasattr(self, "deleteOnReject") and self.deleteOnReject
def setDeleteObjectsOnReject(self, state=False):
Path.Log.track()
self.deleteOnReject = state
return self.deleteOnReject
def setEdit(self, vobj=None, mode=0):
"""setEdit(vobj, mode=0) ... initiate editing of receivers model."""
Path.Log.track()
if 0 == mode:
if vobj is None:
vobj = self.vobj
page = self.getTaskPanelOpPage(vobj.Object)
page.setTitle(self.OpName)
page.setIcon(self.OpIcon)
selection = self.getSelectionFactory()
self.setupTaskPanel(
TaskPanel(vobj.Object, self.deleteObjectsOnReject(), page, selection)
)
self.deleteOnReject = False
return True
# no other editing possible
return False
def setupTaskPanel(self, panel):
"""setupTaskPanel(panel) ... internal function to start the editor."""
self.panel = panel
FreeCADGui.Control.closeDialog()
FreeCADGui.Control.showDialog(panel)
panel.setupUi()
job = self.Object.Proxy.getJob(self.Object)
if job:
job.ViewObject.Proxy.setupEditVisibility(job)
else:
Path.Log.info("did not find no job")
def clearTaskPanel(self):
"""clearTaskPanel() ... internal callback function when editing has finished."""
self.panel = None
job = self.Object.Proxy.getJob(self.Object)
if job:
job.ViewObject.Proxy.resetEditVisibility(job)
def unsetEdit(self, arg1, arg2):
if self.panel:
self.panel.reject(False)
def dumps(self):
"""dumps() ... callback before receiver is saved to a file.
Returns a dictionary with the receiver's resources as strings."""
Path.Log.track()
state = {}
state["OpName"] = self.OpName
state["OpIcon"] = self.OpIcon
state["OpPageModule"] = self.OpPageModule
state["OpPageClass"] = self.OpPageClass
return state
def loads(self, state):
"""loads(state) ... callback on restoring a saved instance, pendant to dumps()
state is the dictionary returned by dumps()."""
self.OpName = state["OpName"]
self.OpIcon = state["OpIcon"]
self.OpPageModule = state["OpPageModule"]
self.OpPageClass = state["OpPageClass"]
def getIcon(self):
"""getIcon() ... the icon used in the object tree"""
if self.Object.Active:
return self.OpIcon
else:
return ":/icons/CAM_OpActive.svg"
def getTaskPanelOpPage(self, obj):
"""getTaskPanelOpPage(obj) ... use the stored information to instantiate the receiver op's page controller."""
mod = importlib.import_module(self.OpPageModule)
cls = getattr(mod, self.OpPageClass)
return cls(obj, 0)
def getSelectionFactory(self):
"""getSelectionFactory() ... return a factory function that can be used to create the selection observer."""
return PathSelection.select(self.OpName)
def updateData(self, obj, prop):
"""updateData(obj, prop) ... callback whenever a property of the receiver's model is assigned.
The callback is forwarded to the task panel - in case an editing session is ongoing."""
# Path.Log.track(obj.Label, prop) # Creates a lot of noise
if self.panel:
self.panel.updateData(obj, prop)
def onDelete(self, vobj, arg2=None):
PathUtil.clearExpressionEngine(vobj.Object)
return True
def setupContextMenu(self, vobj, menu):
Path.Log.track()
for action in menu.actions():
menu.removeAction(action)
action = QtGui.QAction(translate("PathOp", "Edit"), menu)
action.triggered.connect(self.setEdit)
menu.addAction(action)
class TaskPanelPage(object):
"""Base class for all task panel pages."""
# task panel interaction framework
def __init__(self, obj, features):
"""__init__(obj, features) ... framework initialisation.
Do not overwrite, implement initPage(obj) instead."""
self.obj = obj
self.job = PathUtils.findParentJob(obj)
self.form = self.getForm()
self.signalDirtyChanged = None
self.setClean()
self.setTitle("-")
self.setIcon(None)
self.features = features
self.isdirty = False
self.parent = None
self.panelTitle = "Operation"
if self._installTCUpdate():
PathJob.Notification.updateTC.connect(self.resetToolController)
def show_error_message(self, title, message):
msg_box = QtGui.QMessageBox()
msg_box.setIcon(QtGui.QMessageBox.Critical)
msg_box.setWindowTitle(title)
msg_box.setText(message)
msg_box.setStandardButtons(QtGui.QMessageBox.Ok)
msg_box.exec_()
def _installTCUpdate(self):
return hasattr(self.form, "toolController")
def setParent(self, parent):
"""setParent() ... used to transfer parent object link to child class.
Do not overwrite."""
self.parent = parent
def onDirtyChanged(self, callback):
"""onDirtyChanged(callback) ... set callback when dirty state changes."""
self.signalDirtyChanged = callback
def setDirty(self):
"""setDirty() ... mark receiver as dirty, causing the model to be recalculated if OK or Apply is pressed."""
self.isdirty = True
if self.signalDirtyChanged:
self.signalDirtyChanged(self)
def setClean(self):
"""setClean() ... mark receiver as clean, indicating there is no need to recalculate the model even if the user presses OK or Apply."""
self.isdirty = False
if self.signalDirtyChanged:
self.signalDirtyChanged(self)
def pageGetFields(self):
"""pageGetFields() ... internal callback.
Do not overwrite, implement getFields(obj) instead."""
self.getFields(self.obj)
self.setDirty()
def pageSetFields(self):
"""pageSetFields() ... internal callback.
Do not overwrite, implement setFields(obj) instead."""
self.setFields(self.obj)
def pageCleanup(self):
"""pageCleanup() ... internal callback.
Do not overwrite, implement cleanupPage(obj) instead."""
if self._installTCUpdate():
PathJob.Notification.updateTC.disconnect(self.resetToolController)
self.cleanupPage(self.obj)
def pageRegisterSignalHandlers(self):
"""pageRegisterSignalHandlers() .. internal callback.
Registers a callback for all signals returned by getSignalsForUpdate(obj).
Do not overwrite, implement getSignalsForUpdate(obj) and/or registerSignalHandlers(obj) instead.
"""
for signal in self.getSignalsForUpdate(self.obj):
signal.connect(self.pageGetFields)
self.registerSignalHandlers(self.obj)
def pageUpdateData(self, obj, prop):
"""pageUpdateData(obj, prop) ... internal callback.
Do not overwrite, implement updateData(obj) instead."""
self.updateData(obj, prop)
def setTitle(self, title):
"""setTitle(title) ... sets a title for the page."""
self.title = title
def getTitle(self, obj):
"""getTitle(obj) ... return title to be used for the receiver page.
The default implementation returns what was previously set with setTitle(title).
Can safely be overwritten by subclasses."""
return self.title
def setIcon(self, icon):
"""setIcon(icon) ... sets the icon for the page."""
self.icon = icon
def getIcon(self, obj):
"""getIcon(obj) ... return icon for page or None.
Can safely be overwritten by subclasses."""
return self.icon
# subclass interface
def initPage(self, obj):
"""initPage(obj) ... overwrite to customize UI for specific model.
Note that this function is invoked after all page controllers have been created.
Should be overwritten by subclasses."""
pass
def cleanupPage(self, obj):
"""cleanupPage(obj) ... overwrite to perform any cleanup tasks before page is destroyed.
Can safely be overwritten by subclasses."""
pass
def modifyStandardButtons(self, buttonBox):
"""modifyStandardButtons(buttonBox) ... overwrite if the task panel standard buttons need to be modified.
Can safely be overwritten by subclasses."""
pass
def getForm(self):
"""getForm() ... return UI form for this page.
Must be overwritten by subclasses."""
pass
def getFields(self, obj):
"""getFields(obj) ... overwrite to transfer values from UI to obj's properties.
Can safely be overwritten by subclasses."""
pass
def setFields(self, obj):
"""setFields(obj) ... overwrite to transfer obj's property values to UI.
Can safely be overwritten by subclasses."""
pass
def getSignalsForUpdate(self, obj):
"""getSignalsForUpdate(obj) ... return signals which, when triggered, cause the receiver to update the model.
See also registerSignalHandlers(obj)
Can safely be overwritten by subclasses."""
return []
def registerSignalHandlers(self, obj):
"""registerSignalHandlers(obj) ... overwrite to register custom signal handlers.
In case an update of a model is not the desired operation of a signal invocation
(see getSignalsForUpdate(obj)) this function can be used to register signal handlers
manually.
Can safely be overwritten by subclasses."""
pass
def updateData(self, obj, prop):
"""updateData(obj, prop) ... overwrite if the receiver needs to react to property changes that might not have been caused by the receiver itself.
Sometimes a model will recalculate properties based on a change of another property. In order to keep the UI up to date with such changes this
function can be used.
Please note that the callback is synchronous with the property assignment operation. Also note that the notification is invoked regardless of the
actual value of the property assignment. In other words it also fires if a property gets assigned the same value it already has.
Taking above observations into account the implementation has to take care that it doesn't overwrite modified UI values by invoking setFields(obj).
This can happen if a subclass unconditionally transfers all values in getFields(obj) to the model and just calls setFields(obj) in this callback.
In such a scenario the first property assignment will cause all changes in the UI of the other fields to be overwritten by setFields(obj).
You have been warned."""
pass
def updateSelection(self, obj, sel):
"""updateSelection(obj, sel) ...
overwrite to customize UI depending on current selection.
Can safely be overwritten by subclasses."""
pass
def selectInComboBox(self, name, combo):
"""selectInComboBox(name, combo) ...
helper function to select a specific value in a combo box."""
blocker = QtCore.QSignalBlocker(combo)
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)
return
def populateCombobox(self, form, enumTups, comboBoxesPropertyMap):
"""populateCombobox(form, enumTups, comboBoxesPropertyMap) ... proxy for PathGuiUtil.populateCombobox()"""
PathGuiUtil.populateCombobox(form, enumTups, comboBoxesPropertyMap)
def resetToolController(self, job, tc):
if self.obj is not None:
self.obj.ToolController = tc
combo = self.form.toolController
self.setupToolController(self.obj, combo)
def setupToolController(self, obj, combo):
"""setupToolController(obj, combo) ...
helper function to setup obj's ToolController
in the given combo box."""
controllers = PathUtils.getToolControllers(self.obj)
labels = [c.Label for c in controllers]
combo.blockSignals(True)
combo.clear()
combo.addItems(labels)
combo.blockSignals(False)
if obj.ToolController is None:
obj.ToolController = PathUtils.findToolController(obj, obj.Proxy)
if obj.ToolController is not None:
self.selectInComboBox(obj.ToolController.Label, combo)
def updateToolController(self, obj, combo):
"""updateToolController(obj, combo) ...
helper function to update obj's ToolController property if a different
one has been selected in the combo box."""
tc = PathUtils.findToolController(obj, obj.Proxy, combo.currentText())
if obj.ToolController != tc:
obj.ToolController = tc
def setupCoolant(self, obj, combo):
"""setupCoolant(obj, combo) ...
helper function to setup obj's Coolant option."""
job = PathUtils.findParentJob(obj)
options = job.SetupSheet.CoolantModes
combo.blockSignals(True)
combo.clear()
combo.addItems(options)
combo.blockSignals(False)
if hasattr(obj, "CoolantMode"):
self.selectInComboBox(obj.CoolantMode, combo)
def updateCoolant(self, obj, combo):
"""updateCoolant(obj, combo) ...
helper function to update obj's Coolant property if a different
one has been selected in the combo box."""
option = combo.currentText()
if hasattr(obj, "CoolantMode"):
if obj.CoolantMode != option:
obj.CoolantMode = option
def updatePanelVisibility(self, panelTitle, obj):
"""updatePanelVisibility(panelTitle, obj) ...
Function to call the `updateVisibility()` GUI method of the
page whose panel title is as indicated."""
if hasattr(self, "parent"):
parent = getattr(self, "parent")
if parent and hasattr(parent, "featurePages"):
for page in parent.featurePages:
if hasattr(page, "panelTitle"):
if page.panelTitle == panelTitle and hasattr(page, "updateVisibility"):
page.updateVisibility()
break
class TaskPanelBaseGeometryPage(TaskPanelPage):
"""Page controller for the base geometry."""
DataObject = QtCore.Qt.ItemDataRole.UserRole
DataObjectSub = QtCore.Qt.ItemDataRole.UserRole + 1
def __init__(self, obj, features):
super(TaskPanelBaseGeometryPage, self).__init__(obj, features)
self.panelTitle = "Base Geometry"
self.OpIcon = ":/icons/CAM_BaseGeometry.svg"
self.setIcon(self.OpIcon)
def getForm(self):
panel = FreeCADGui.PySideUic.loadUi(":/panels/PageBaseGeometryEdit.ui")
self.modifyPanel(panel)
return panel
def modifyPanel(self, panel):
"""modifyPanel(self, panel) ...
Helper method to modify the current form immediately after
it is loaded."""
# Determine if Job operations are available with Base Geometry
availableOps = list()
ops = self.job.Operations.Group
for op in ops:
if hasattr(op, "Base") and isinstance(op.Base, list):
if len(op.Base) > 0:
availableOps.append(op.Label)
# Load available operations into combobox
if len(availableOps) > 0:
# Populate the operations list
panel.geometryImportList.blockSignals(True)
panel.geometryImportList.clear()
availableOps.sort()
for opLbl in availableOps:
panel.geometryImportList.addItem(opLbl)
panel.geometryImportList.blockSignals(False)
else:
panel.geometryImportList.hide()
panel.geometryImportButton.hide()
def getTitle(self, obj):
return translate("PathOp", "Base Geometry")
def getFields(self, obj):
pass
def setFields(self, obj):
self.form.baseList.blockSignals(True)
self.form.baseList.clear()
for base in self.obj.Base:
for sub in base[1]:
item = QtGui.QListWidgetItem("%s.%s" % (base[0].Label, sub))
item.setData(self.DataObject, base[0])
item.setData(self.DataObjectSub, sub)
self.form.baseList.addItem(item)
self.form.baseList.blockSignals(False)
self.resizeBaseList()
def itemActivated(self):
FreeCADGui.Selection.clearSelection()
for item in self.form.baseList.selectedItems():
obj = item.data(self.DataObject)
sub = item.data(self.DataObjectSub)
if sub:
FreeCADGui.Selection.addSelection(obj, sub)
else:
FreeCADGui.Selection.addSelection(obj)
# FreeCADGui.updateGui()
def supportsVertexes(self):
return self.features & PathOp.FeatureBaseVertexes
def supportsEdges(self):
return self.features & PathOp.FeatureBaseEdges
def supportsFaces(self):
return self.features & PathOp.FeatureBaseFaces
def supportsPanels(self):
return self.features & PathOp.FeatureBasePanels
def featureName(self):
if self.supportsEdges() and self.supportsFaces():
return "features"
if self.supportsFaces():
return "faces"
if self.supportsEdges():
return "edges"
return "nothing"
def selectionSupportedAsBaseGeometry(self, selection, ignoreErrors):
if len(selection) != 1:
if not ignoreErrors:
msg = translate(
"PathOp",
"Please select %s from a single solid" % self.featureName(),
)
Path.Log.debug(msg)
return False
sel = selection[0]
if sel.HasSubObjects:
if not self.supportsVertexes() and selection[0].SubObjects[0].ShapeType == "Vertex":
return False
if not self.supportsEdges() and selection[0].SubObjects[0].ShapeType == "Edge":
return False
if not self.supportsFaces() and selection[0].SubObjects[0].ShapeType == "Face":
return False
else:
if not self.supportsPanels() or "Panel" not in sel.Object.Name:
return False
return True
def addBaseGeometry(self, selection):
Path.Log.track(selection)
if self.selectionSupportedAsBaseGeometry(selection, False):
sel = selection[0]
for sub in sel.SubElementNames:
self.obj.Proxy.addBase(self.obj, sel.Object, sub)
return True
return False
def addBase(self):
Path.Log.track()
if self.addBaseGeometry(FreeCADGui.Selection.getSelectionEx()):
# self.obj.Proxy.execute(self.obj)
self.setFields(self.obj)
self.setDirty()
self.updatePanelVisibility("Operation", self.obj)
def deleteBase(self):
Path.Log.track()
selected = self.form.baseList.selectedItems()
for item in selected:
self.form.baseList.takeItem(self.form.baseList.row(item))
self.setDirty()
self.updateBase()
self.updatePanelVisibility("Operation", self.obj)
self.resizeBaseList()
def updateBase(self):
newlist = []
for i in range(self.form.baseList.count()):
item = self.form.baseList.item(i)
obj = item.data(self.DataObject)
sub = item.data(self.DataObjectSub)
if sub:
base = (obj, str(sub))
newlist.append(base)
Path.Log.debug("Setting new base: %s -> %s" % (self.obj.Base, newlist))
self.obj.Base = newlist
def clearBase(self):
self.obj.Base = []
self.setDirty()
self.updatePanelVisibility("Operation", self.obj)
self.resizeBaseList()
def importBaseGeometry(self):
opLabel = str(self.form.geometryImportList.currentText())
ops = FreeCAD.ActiveDocument.getObjectsByLabel(opLabel)
if len(ops) > 1:
msg = translate("PathOp", "Multiple operations are labeled as")
msg += " {}\n".format(opLabel)
FreeCAD.Console.PrintWarning(msg)
for base, subList in ops[0].Base:
FreeCADGui.Selection.clearSelection()
FreeCADGui.Selection.addSelection(base, subList)
self.addBase()
def registerSignalHandlers(self, obj):
self.form.baseList.itemSelectionChanged.connect(self.itemActivated)
self.form.addBase.clicked.connect(self.addBase)
self.form.deleteBase.clicked.connect(self.deleteBase)
self.form.clearBase.clicked.connect(self.clearBase)
self.form.geometryImportButton.clicked.connect(self.importBaseGeometry)
def pageUpdateData(self, obj, prop):
if prop in ["Base"]:
self.setFields(obj)
def updateSelection(self, obj, sel):
if self.selectionSupportedAsBaseGeometry(sel, True):
self.form.addBase.setEnabled(True)
else:
self.form.addBase.setEnabled(False)
def resizeBaseList(self):
# Set base geometry list window to resize based on contents
# Code reference:
# https://stackoverflow.com/questions/6337589/qlistwidget-adjust-size-to-content
# ml: disabling this logic because I can't get it to work on HPD monitor.
# On my systems the values returned by the list object are also incorrect on
# creation, leading to a list object of size 15. count() always returns 0 until
# the list is actually displayed. The same is true for sizeHintForRow(0), which
# returns -1 until the widget is rendered. The widget claims to have a size of
# (100, 30), once it becomes visible the size is (535, 192).
# Leaving the framework here in case somebody figures out how to set this up
# properly.
qList = self.form.baseList
row = (qList.count() + qList.frameWidth()) * 15
# qList.setMinimumHeight(row)
Path.Log.debug(
"baseList({}, {}) {} * {}".format(
qList.size(), row, qList.count(), qList.sizeHintForRow(0)
)
)
class TaskPanelBaseLocationPage(TaskPanelPage):
"""Page controller for base locations. Uses PathGetPoint."""
DataLocation = QtCore.Qt.ItemDataRole.UserRole
def __init__(self, obj, features):
super(TaskPanelBaseLocationPage, self).__init__(obj, features)
# members initialized later
self.editRow = None
self.panelTitle = "Base Location"
def getForm(self):
self.formLoc = FreeCADGui.PySideUic.loadUi(":/panels/PageBaseLocationEdit.ui")
if QtCore.qVersion()[0] == "4":
self.formLoc.baseList.horizontalHeader().setResizeMode(QtGui.QHeaderView.Stretch)
else:
self.formLoc.baseList.horizontalHeader().setSectionResizeMode(QtGui.QHeaderView.Stretch)
self.getPoint = PathGetPoint.TaskPanel(self.formLoc.addRemoveEdit)
return self.formLoc
def modifyStandardButtons(self, buttonBox):
self.getPoint.buttonBox = buttonBox
def getTitle(self, obj):
return translate("PathOp", "Base Location")
def getFields(self, obj):
pass
def setFields(self, obj):
self.formLoc.baseList.blockSignals(True)
self.formLoc.baseList.clearContents()
self.formLoc.baseList.setRowCount(0)
for location in self.obj.Locations:
self.formLoc.baseList.insertRow(self.formLoc.baseList.rowCount())
item = QtGui.QTableWidgetItem("%.2f" % location.x)
item.setData(self.DataLocation, location.x)
self.formLoc.baseList.setItem(self.formLoc.baseList.rowCount() - 1, 0, item)
item = QtGui.QTableWidgetItem("%.2f" % location.y)
item.setData(self.DataLocation, location.y)
self.formLoc.baseList.setItem(self.formLoc.baseList.rowCount() - 1, 1, item)
self.formLoc.baseList.resizeColumnToContents(0)
self.formLoc.baseList.blockSignals(False)
self.itemActivated()
def removeLocation(self):
deletedRows = []
selected = self.formLoc.baseList.selectedItems()
for item in selected:
row = self.formLoc.baseList.row(item)
if row not in deletedRows:
deletedRows.append(row)
self.formLoc.baseList.removeRow(row)
self.updateLocations()
FreeCAD.ActiveDocument.recompute()
def updateLocations(self):
Path.Log.track()
locations = []
for i in range(self.formLoc.baseList.rowCount()):
x = self.formLoc.baseList.item(i, 0).data(self.DataLocation)
y = self.formLoc.baseList.item(i, 1).data(self.DataLocation)
location = FreeCAD.Vector(x, y, 0)
locations.append(location)
self.obj.Locations = locations
def addLocation(self):
self.getPoint.getPoint(self.addLocationAt)
def addLocationAt(self, point, obj):
if point:
locations = self.obj.Locations
locations.append(point)
self.obj.Locations = locations
FreeCAD.ActiveDocument.recompute()
def editLocation(self):
selected = self.formLoc.baseList.selectedItems()
if selected:
row = self.formLoc.baseList.row(selected[0])
self.editRow = row
x = self.formLoc.baseList.item(row, 0).data(self.DataLocation)
y = self.formLoc.baseList.item(row, 1).data(self.DataLocation)
start = FreeCAD.Vector(x, y, 0)
self.getPoint.getPoint(self.editLocationAt, start)
def editLocationAt(self, point, obj):
if point:
self.formLoc.baseList.item(self.editRow, 0).setData(self.DataLocation, point.x)
self.formLoc.baseList.item(self.editRow, 1).setData(self.DataLocation, point.y)
self.updateLocations()
FreeCAD.ActiveDocument.recompute()
def itemActivated(self):
if self.formLoc.baseList.selectedItems():
self.form.removeLocation.setEnabled(True)
self.form.editLocation.setEnabled(True)
else:
self.form.removeLocation.setEnabled(False)
self.form.editLocation.setEnabled(False)
def registerSignalHandlers(self, obj):
self.form.baseList.itemSelectionChanged.connect(self.itemActivated)
self.formLoc.addLocation.clicked.connect(self.addLocation)
self.formLoc.removeLocation.clicked.connect(self.removeLocation)
self.formLoc.editLocation.clicked.connect(self.editLocation)
def pageUpdateData(self, obj, prop):
if prop in ["Locations"]:
self.setFields(obj)
class TaskPanelHeightsPage(TaskPanelPage):
"""Page controller for heights."""
def __init__(self, obj, features):
super(TaskPanelHeightsPage, self).__init__(obj, features)
# members initialized later
self.clearanceHeight = None
self.safeHeight = None
self.panelTitle = "Heights"
self.OpIcon = ":/icons/CAM_Heights.svg"
self.setIcon(self.OpIcon)
def getForm(self):
return FreeCADGui.PySideUic.loadUi(":/panels/PageHeightsEdit.ui")
def initPage(self, obj):
self.safeHeight = PathGuiUtil.QuantitySpinBox(self.form.safeHeight, obj, "SafeHeight")
self.clearanceHeight = PathGuiUtil.QuantitySpinBox(
self.form.clearanceHeight, obj, "ClearanceHeight"
)
def getTitle(self, obj):
return translate("PathOp", "Heights")
def getFields(self, obj):
self.safeHeight.updateProperty()
self.clearanceHeight.updateProperty()
def setFields(self, obj):
self.safeHeight.updateSpinBox()
self.clearanceHeight.updateSpinBox()
def getSignalsForUpdate(self, obj):
signals = []
signals.append(self.form.safeHeight.editingFinished)
signals.append(self.form.clearanceHeight.editingFinished)
return signals
def pageUpdateData(self, obj, prop):
if prop in ["SafeHeight", "ClearanceHeight"]:
self.setFields(obj)
class TaskPanelDepthsPage(TaskPanelPage):
"""Page controller for depths."""
def __init__(self, obj, features):
super(TaskPanelDepthsPage, self).__init__(obj, features)
# members initialized later
self.startDepth = None
self.finalDepth = None
self.finishDepth = None
self.stepDown = None
self.panelTitle = "Depths"
self.OpIcon = ":/icons/CAM_Depths.svg"
self.setIcon(self.OpIcon)
def getForm(self):
return FreeCADGui.PySideUic.loadUi(":/panels/PageDepthsEdit.ui")
def haveStartDepth(self):
return PathOp.FeatureDepths & self.features
def haveFinalDepth(self):
return (
PathOp.FeatureDepths & self.features and not PathOp.FeatureNoFinalDepth & self.features
)
def haveFinishDepth(self):
return PathOp.FeatureDepths & self.features and PathOp.FeatureFinishDepth & self.features
def haveStepDown(self):
return PathOp.FeatureStepDown & self.features
def initPage(self, obj):
if self.haveStartDepth():
self.startDepth = PathGuiUtil.QuantitySpinBox(self.form.startDepth, obj, "StartDepth")
else:
self.form.startDepth.hide()
self.form.startDepthLabel.hide()
self.form.startDepthSet.hide()
if self.haveFinalDepth():
self.finalDepth = PathGuiUtil.QuantitySpinBox(self.form.finalDepth, obj, "FinalDepth")
else:
if self.haveStartDepth():
self.form.finalDepth.setEnabled(False)
self.form.finalDepth.setToolTip(
translate(
"PathOp",
"FinalDepth cannot be modified for this operation.\nIf it is necessary to set the FinalDepth manually please select a different operation.",
)
)
else:
self.form.finalDepth.hide()
self.form.finalDepthLabel.hide()
self.form.finalDepthSet.hide()
if self.haveStepDown():
self.stepDown = PathGuiUtil.QuantitySpinBox(self.form.stepDown, obj, "StepDown")
else:
self.form.stepDown.hide()
self.form.stepDownLabel.hide()
if self.haveFinishDepth():
self.finishDepth = PathGuiUtil.QuantitySpinBox(
self.form.finishDepth, obj, "FinishDepth"
)
else:
self.form.finishDepth.hide()
self.form.finishDepthLabel.hide()
def getTitle(self, obj):
return translate("PathOp", "Depths")
def getFields(self, obj):
if self.haveStartDepth():
self.startDepth.updateProperty()
if self.haveFinalDepth():
self.finalDepth.updateProperty()
if self.haveStepDown():
self.stepDown.updateProperty()
if self.haveFinishDepth():
self.finishDepth.updateProperty()
def setFields(self, obj):
if self.haveStartDepth():
self.startDepth.updateSpinBox()
if self.haveFinalDepth():
self.finalDepth.updateSpinBox()
if self.haveStepDown():
self.stepDown.updateSpinBox()
if self.haveFinishDepth():
self.finishDepth.updateSpinBox()
self.updateSelection(obj, FreeCADGui.Selection.getSelectionEx())
def getSignalsForUpdate(self, obj):
signals = []
if self.haveStartDepth():
signals.append(self.form.startDepth.editingFinished)
if self.haveFinalDepth():
signals.append(self.form.finalDepth.editingFinished)
if self.haveStepDown():
signals.append(self.form.stepDown.editingFinished)
if self.haveFinishDepth():
signals.append(self.form.finishDepth.editingFinished)
return signals
def registerSignalHandlers(self, obj):
if self.haveStartDepth():
self.form.startDepthSet.clicked.connect(
lambda: self.depthSet(obj, self.startDepth, "StartDepth")
)
if self.haveFinalDepth():
self.form.finalDepthSet.clicked.connect(
lambda: self.depthSet(obj, self.finalDepth, "FinalDepth")
)
def pageUpdateData(self, obj, prop):
if prop in ["StartDepth", "FinalDepth", "StepDown", "FinishDepth"]:
self.setFields(obj)
def depthSet(self, obj, spinbox, prop):
z = self.selectionZLevel(FreeCADGui.Selection.getSelectionEx())
if z is not None:
Path.Log.debug("depthSet(%s, %s, %.2f)" % (obj.Label, prop, z))
if spinbox.expression():
obj.setExpression(prop, None)
self.setDirty()
spinbox.updateSpinBox(FreeCAD.Units.Quantity(z, FreeCAD.Units.Length))
if spinbox.updateProperty():
self.setDirty()
else:
Path.Log.info("depthSet(-)")
def selectionZLevel(self, sel):
if len(sel) == 1 and len(sel[0].SubObjects) == 1:
sub = sel[0].SubObjects[0]
if "Vertex" == sub.ShapeType:
return sub.Z
if Path.Geom.isHorizontal(sub):
if "Edge" == sub.ShapeType:
return sub.Vertexes[0].Z
if "Face" == sub.ShapeType:
return sub.BoundBox.ZMax
return None
def updateSelection(self, obj, sel):
if self.selectionZLevel(sel) is not None:
self.form.startDepthSet.setEnabled(True)
self.form.finalDepthSet.setEnabled(True)
else:
self.form.startDepthSet.setEnabled(False)
self.form.finalDepthSet.setEnabled(False)
class TaskPanelDiametersPage(TaskPanelPage):
"""Page controller for diameters."""
def __init__(self, obj, features):
super(TaskPanelDiametersPage, self).__init__(obj, features)
# members initialized later
self.clearanceHeight = None
self.safeHeight = None
def getForm(self):
return FreeCADGui.PySideUic.loadUi(":/panels/PageDiametersEdit.ui")
def initPage(self, obj):
self.minDiameter = PathGuiUtil.QuantitySpinBox(self.form.minDiameter, obj, "MinDiameter")
self.maxDiameter = PathGuiUtil.QuantitySpinBox(self.form.maxDiameter, obj, "MaxDiameter")
def getTitle(self, obj):
return translate("PathOp", "Diameters")
def getFields(self, obj):
self.minDiameter.updateProperty()
self.maxDiameter.updateProperty()
def setFields(self, obj):
self.minDiameter.updateSpinBox()
self.maxDiameter.updateSpinBox()
def getSignalsForUpdate(self, obj):
signals = []
signals.append(self.form.minDiameter.editingFinished)
signals.append(self.form.maxDiameter.editingFinished)
return signals
def pageUpdateData(self, obj, prop):
if prop in ["MinDiameter", "MaxDiameter"]:
self.setFields(obj)
class TaskPanel(object):
"""
Generic TaskPanel implementation handling the standard Path operation layout.
This class only implements the framework and takes care of bringing all pages up and down in a controller fashion.
It implements the standard editor behaviour for OK, Cancel and Apply and tracks if the model is still in sync with
the UI.
However, all display and processing of fields is handled by the page controllers which are managed in a list. All
event callbacks and framework actions are forwarded to the page controllers in turn and each page controller is
expected to process all events concerning the data it manages.
"""
def __init__(self, obj, deleteOnReject, opPage, selectionFactory):
Path.Log.track(obj.Label, deleteOnReject, opPage, selectionFactory)
FreeCAD.ActiveDocument.openTransaction(translate("PathOp", "AreaOp Operation"))
self.obj = obj
self.deleteOnReject = deleteOnReject
self.featurePages = []
self.parent = None
# members initialized later
self.clearanceHeight = None
self.safeHeight = None
self.startDepth = None
self.finishDepth = None
self.finalDepth = None
self.stepDown = None
self.buttonBox = None
self.minDiameter = None
self.maxDiameter = None
features = obj.Proxy.opFeatures(obj)
opPage.features = features
if PathOp.FeatureBaseGeometry & features:
if hasattr(opPage, "taskPanelBaseGeometryPage"):
self.featurePages.append(opPage.taskPanelBaseGeometryPage(obj, features))
else:
self.featurePages.append(TaskPanelBaseGeometryPage(obj, features))
if PathOp.FeatureLocations & features:
if hasattr(opPage, "taskPanelBaseLocationPage"):
self.featurePages.append(opPage.taskPanelBaseLocationPage(obj, features))
else:
self.featurePages.append(TaskPanelBaseLocationPage(obj, features))
if PathOp.FeatureDepths & features or PathOp.FeatureStepDown & features:
if hasattr(opPage, "taskPanelDepthsPage"):
self.featurePages.append(opPage.taskPanelDepthsPage(obj, features))
else:
self.featurePages.append(TaskPanelDepthsPage(obj, features))
if PathOp.FeatureHeights & features:
if hasattr(opPage, "taskPanelHeightsPage"):
self.featurePages.append(opPage.taskPanelHeightsPage(obj, features))
else:
self.featurePages.append(TaskPanelHeightsPage(obj, features))
if PathOp.FeatureDiameters & features:
if hasattr(opPage, "taskPanelDiametersPage"):
self.featurePages.append(opPage.taskPanelDiametersPage(obj, features))
else:
self.featurePages.append(TaskPanelDiametersPage(obj, features))
self.featurePages.append(opPage)
for page in self.featurePages:
page.parent = self # save pointer to this current class as "parent"
page.initPage(obj)
page.onDirtyChanged(self.pageDirtyChanged)
taskPanelLayout = Path.Preferences.defaultTaskPanelLayout()
if taskPanelLayout < 2:
opTitle = opPage.getTitle(obj)
opPage.setTitle(translate("PathOp", "Operation"))
toolbox = QtGui.QToolBox()
if taskPanelLayout == 0:
for page in self.featurePages:
toolbox.addItem(page.form, page.getTitle(obj))
itemIdx = toolbox.count() - 1
if page.icon:
toolbox.setItemIcon(itemIdx, QtGui.QIcon(page.icon))
toolbox.setCurrentIndex(len(self.featurePages) - 1)
else:
for page in reversed(self.featurePages):
toolbox.addItem(page.form, page.getTitle(obj))
itemIdx = toolbox.count() - 1
if page.icon:
toolbox.setItemIcon(itemIdx, QtGui.QIcon(page.icon))
toolbox.setWindowTitle(opTitle)
if opPage.getIcon(obj):
toolbox.setWindowIcon(QtGui.QIcon(opPage.getIcon(obj)))
self.form = toolbox
elif taskPanelLayout == 2:
forms = []
for page in self.featurePages:
page.form.setWindowTitle(page.getTitle(obj))
forms.append(page.form)
self.form = forms
elif taskPanelLayout == 3:
forms = []
for page in reversed(self.featurePages):
page.form.setWindowTitle(page.getTitle(obj))
forms.append(page.form)
self.form = forms
self.selectionFactory = selectionFactory
self.obj = obj
self.isdirty = deleteOnReject
self.visibility = obj.ViewObject.Visibility
obj.ViewObject.Visibility = True
def isDirty(self):
"""isDirty() ... returns true if the model is not in sync with the UI anymore."""
for page in self.featurePages:
if page.isdirty:
return True
return self.isdirty
def setClean(self):
"""setClean() ... set the receiver and all its pages clean."""
self.isdirty = False
for page in self.featurePages:
page.setClean()
def accept(self, resetEdit=True):
"""accept() ... callback invoked when user presses the task panel OK button."""
self.preCleanup()
if self.isDirty():
self.panelGetFields()
FreeCAD.ActiveDocument.commitTransaction()
self.cleanup(resetEdit)
def reject(self, resetEdit=True):
"""reject() ... callback invoked when user presses the task panel Cancel button."""
self.preCleanup()
FreeCAD.ActiveDocument.abortTransaction()
if self.deleteOnReject:
FreeCAD.ActiveDocument.openTransaction(translate("PathOp", "Uncreate AreaOp Operation"))
try:
PathUtil.clearExpressionEngine(self.obj)
FreeCAD.ActiveDocument.removeObject(self.obj.Name)
except Exception as ee:
Path.Log.debug("{}\n".format(ee))
FreeCAD.ActiveDocument.commitTransaction()
self.cleanup(resetEdit)
return True
def preCleanup(self):
for page in self.featurePages:
page.onDirtyChanged(None)
PathSelection.clear()
FreeCADGui.Selection.removeObserver(self)
self.obj.ViewObject.Proxy.clearTaskPanel()
self.obj.ViewObject.Visibility = self.visibility
def cleanup(self, resetEdit):
"""cleanup() ... implements common cleanup tasks."""
self.panelCleanup()
FreeCADGui.Control.closeDialog()
if resetEdit:
FreeCADGui.ActiveDocument.resetEdit()
FreeCAD.ActiveDocument.recompute()
def pageDirtyChanged(self, page):
"""pageDirtyChanged(page) ... internal callback"""
self.buttonBox.button(QtGui.QDialogButtonBox.Apply).setEnabled(self.isDirty())
def clicked(self, button):
"""clicked(button) ... callback invoked when the user presses any of the task panel buttons."""
if button == QtGui.QDialogButtonBox.Apply:
self.panelGetFields()
self.setClean()
FreeCAD.ActiveDocument.recompute()
def modifyStandardButtons(self, buttonBox):
"""modifyStandarButtons(buttonBox) ... callback in case the task panel buttons need to be modified."""
self.buttonBox = buttonBox
for page in self.featurePages:
page.modifyStandardButtons(buttonBox)
self.pageDirtyChanged(None)
def panelGetFields(self):
"""panelGetFields() ... invoked to trigger a complete transfer of UI data to the model."""
Path.Log.track()
for page in self.featurePages:
page.pageGetFields()
def panelSetFields(self):
"""panelSetFields() ... invoked to trigger a complete transfer of the model's properties to the UI."""
Path.Log.track()
self.obj.Proxy.sanitizeBase(self.obj)
for page in self.featurePages:
page.pageSetFields()
def panelCleanup(self):
"""panelCleanup() ... invoked before the receiver is destroyed."""
Path.Log.track()
for page in self.featurePages:
page.pageCleanup()
def open(self):
"""open() ... callback invoked when the task panel is opened."""
self.selectionFactory()
FreeCADGui.Selection.addObserver(self)
def getStandardButtons(self):
"""getStandardButtons() ... returns the Buttons for the task panel."""
return (
QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Apply | QtGui.QDialogButtonBox.Cancel
)
def setupUi(self):
"""setupUi() ... internal function to initialise all pages."""
Path.Log.track(self.deleteOnReject)
if self.deleteOnReject and PathOp.FeatureBaseGeometry & self.obj.Proxy.opFeatures(self.obj):
sel = FreeCADGui.Selection.getSelectionEx()
for page in self.featurePages:
if getattr(page, "InitBase", True) and hasattr(page, "addBase"):
page.clearBase()
page.addBaseGeometry(sel)
# Update properties based upon expressions in case expression value has changed
for prp, expr in self.obj.ExpressionEngine:
val = FreeCAD.Units.Quantity(self.obj.evalExpression(expr))
value = val.Value if hasattr(val, "Value") else val
prop = getattr(self.obj, prp)
if hasattr(prop, "Value"):
prop.Value = value
else:
prop = value
self.panelSetFields()
for page in self.featurePages:
page.pageRegisterSignalHandlers()
def updateData(self, obj, prop):
"""updateDate(obj, prop) ... callback invoked whenever a model's property is assigned a value."""
# Path.Log.track(obj.Label, prop) # creates a lot of noise
for page in self.featurePages:
page.pageUpdateData(obj, prop)
def needsFullSpace(self):
return True
def updateSelection(self):
sel = FreeCADGui.Selection.getSelectionEx()
for page in self.featurePages:
page.updateSelection(self.obj, sel)
# SelectionObserver interface
def addSelection(self, doc, obj, sub, pnt):
self.updateSelection()
def removeSelection(self, doc, obj, sub):
self.updateSelection()
def setSelection(self, doc):
self.updateSelection()
def clearSelection(self, doc):
self.updateSelection()
class CommandSetStartPoint:
"""Command to set the start point for an operation."""
def GetResources(self):
return {
"Pixmap": "CAM_StartPoint",
"MenuText": QT_TRANSLATE_NOOP("PathOp", "Pick Start Point"),
"ToolTip": QT_TRANSLATE_NOOP("PathOp", "Pick Start Point"),
}
def IsActive(self):
if FreeCAD.ActiveDocument is None:
return False
sel = FreeCADGui.Selection.getSelection()
if not sel:
return False
obj = sel[0]
return obj and hasattr(obj, "StartPoint")
def setpoint(self, point, o):
obj = FreeCADGui.Selection.getSelection()[0]
obj.StartPoint.x = point.x
obj.StartPoint.y = point.y
obj.StartPoint.z = obj.ClearanceHeight.Value
def Activated(self):
if not hasattr(FreeCADGui, "Snapper"):
import DraftTools
FreeCADGui.Snapper.getPoint(callback=self.setpoint)
def Create(res):
"""Create(res) ... generic implementation of a create function.
res is an instance of CommandResources. It is not expected that the user invokes
this function directly, but calls the Activated() function of the Command object
that is created in each operations Gui implementation."""
FreeCAD.ActiveDocument.openTransaction("Create %s" % res.name)
try:
obj = res.objFactory(res.name, obj=None, parentJob=res.job)
if obj.Proxy:
obj.ViewObject.Proxy = ViewProvider(obj.ViewObject, res)
obj.ViewObject.Visibility = True
FreeCAD.ActiveDocument.commitTransaction()
obj.ViewObject.Document.setEdit(obj.ViewObject, 0)
return obj
except PathUtils.PathNoTCExistsException:
msg = translate("PathOp", "No suitable tool controller found.\nAborting op creation")
diag = QtGui.QMessageBox(QtGui.QMessageBox.Warning, "Error", msg)
diag.setWindowModality(QtCore.Qt.ApplicationModal)
diag.exec_()
except PathOp.PathNoTCException:
Path.Log.warning(translate("PathOp", "No tool controller, aborting op creation"))
FreeCAD.ActiveDocument.abortTransaction()
FreeCAD.ActiveDocument.recompute()
return None
class CommandPathOp:
"""Generic, data driven implementation of a Path operation creation command.
Instances of this class are stored in all Path operation Gui modules and can
be used to create said operations with view providers and all."""
def __init__(self, resources):
self.res = resources
def GetResources(self):
ress = {
"Pixmap": self.res.pixmap,
"MenuText": self.res.menuText,
"ToolTip": self.res.toolTip,
}
if self.res.accelKey:
ress["Accel"] = self.res.accelKey
return ress
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):
return Create(self.res)
class CommandResources:
"""POD class to hold command specific resources."""
def __init__(self, name, objFactory, opPageClass, pixmap, menuText, accelKey, toolTip):
self.name = name
self.objFactory = objFactory
self.opPageClass = opPageClass
self.pixmap = pixmap
self.menuText = menuText
self.accelKey = accelKey
self.toolTip = toolTip
self.job = None
def SetupOperation(name, objFactory, opPageClass, pixmap, menuText, toolTip, setupProperties=None):
"""SetupOperation(name, objFactory, opPageClass, pixmap, menuText, toolTip, setupProperties=None)
Creates an instance of CommandPathOp with the given parameters and registers the command with FreeCAD.
When activated it creates a model with proxy (by invoking objFactory), assigns a view provider to it
(see ViewProvider in this module) and starts the editor specifically for this operation (driven by opPageClass).
This is an internal function that is automatically called by the initialisation code for each operation.
It is not expected to be called manually.
"""
res = CommandResources(name, objFactory, opPageClass, pixmap, menuText, None, toolTip)
command = CommandPathOp(res)
FreeCADGui.addCommand("CAM_%s" % name.replace(" ", "_"), command)
if setupProperties is not None:
PathSetupSheet.RegisterOperation(name, objFactory, setupProperties)
return command
FreeCADGui.addCommand("CAM_SetStartPoint", CommandSetStartPoint())
FreeCAD.Console.PrintLog("Loading PathOpGui... done\n")