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

434 lines
17 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 Path
import Path.Base.Util as PathUtil
import Path.Base.SetupSheetOpPrototype as PathSetupSheetOpPrototype
from PySide.QtCore import QT_TRANSLATE_NOOP
__title__ = "Setup Sheet for a Job."
__author__ = "sliptonic (Brad Collette)"
__url__ = "https://www.freecad.org"
__doc__ = "A container for all default values and job specific configuration values."
_RegisteredOps: dict = {}
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 Template:
HorizRapid = "HorizRapid"
VertRapid = "VertRapid"
CoolantMode = "CoolantMode"
SafeHeightOffset = "SafeHeightOffset"
SafeHeightExpression = "SafeHeightExpression"
ClearanceHeightOffset = "ClearanceHeightOffset"
ClearanceHeightExpression = "ClearanceHeightExpression"
StartDepthExpression = "StartDepthExpression"
FinalDepthExpression = "FinalDepthExpression"
StepDownExpression = "StepDownExpression"
Fixtures = "Fixtures"
OrderOutputBy = "OrderOutputBy"
SplitOutput = "SplitOutput"
All = [
HorizRapid,
VertRapid,
CoolantMode,
SafeHeightOffset,
SafeHeightExpression,
ClearanceHeightOffset,
ClearanceHeightExpression,
StartDepthExpression,
FinalDepthExpression,
StepDownExpression,
]
def _traverseTemplateAttributes(attrs, codec):
Path.Log.debug(attrs)
coded = {}
for key, value in attrs.items():
if type(value) == dict:
Path.Log.debug("%s is a dict" % key)
coded[key] = _traverseTemplateAttributes(value, codec)
elif type(value) == list:
Path.Log.debug("%s is a list" % key)
coded[key] = [_traverseTemplateAttributes(attr, codec) for attr in value]
elif isinstance(value, str):
Path.Log.debug("%s is a string" % key)
coded[key] = codec(value)
else:
Path.Log.debug("%s is %s" % (key, type(value)))
coded[key] = value
return coded
class SetupSheet:
"""Property container object used by a Job to hold global reference values."""
TemplateReference = "${SetupSheet}"
DefaultSafeHeightOffset = "3 mm"
DefaultClearanceHeightOffset = "5 mm"
DefaultSafeHeightExpression = "OpStockZMax+${SetupSheet}.SafeHeightOffset"
DefaultClearanceHeightExpression = "OpStockZMax+${SetupSheet}.ClearanceHeightOffset"
DefaultStartDepthExpression = "OpStartDepth"
DefaultFinalDepthExpression = "OpFinalDepth"
DefaultStepDownExpression = "OpToolDiameter"
DefaultCoolantModes = ["None", "Flood", "Mist"]
def __init__(self, obj):
self.obj = obj
obj.addProperty(
"App::PropertySpeed",
"VertRapid",
"ToolController",
QT_TRANSLATE_NOOP("App::Property", "Default speed for horizontal rapid moves."),
)
obj.addProperty(
"App::PropertySpeed",
"HorizRapid",
"ToolController",
QT_TRANSLATE_NOOP("App::Property", "Default speed for vertical rapid moves."),
)
obj.addProperty(
"App::PropertyStringList",
"CoolantModes",
"CoolantMode",
QT_TRANSLATE_NOOP("App::Property", "Coolant Modes"),
)
obj.addProperty(
"App::PropertyEnumeration",
"CoolantMode",
"CoolantMode",
QT_TRANSLATE_NOOP("App::Property", "Default coolant mode."),
)
obj.addProperty(
"App::PropertyLength",
"SafeHeightOffset",
"OperationHeights",
QT_TRANSLATE_NOOP(
"App::Property",
"The usage of this field depends on SafeHeightExpression - by default its value is added to the start depth and used for the safe height of an operation.",
),
)
obj.addProperty(
"App::PropertyString",
"SafeHeightExpression",
"OperationHeights",
QT_TRANSLATE_NOOP("App::Property", "Expression for the safe height of new operations."),
)
obj.addProperty(
"App::PropertyLength",
"ClearanceHeightOffset",
"OperationHeights",
QT_TRANSLATE_NOOP(
"App::Property",
"The usage of this field depends on ClearanceHeightExpression - by default is value is added to the start depth and used for the clearance height of an operation.",
),
)
obj.addProperty(
"App::PropertyString",
"ClearanceHeightExpression",
"OperationHeights",
QT_TRANSLATE_NOOP(
"App::Property",
"Expression for the clearance height of new operations.",
),
)
obj.addProperty(
"App::PropertyString",
"StartDepthExpression",
"OperationDepths",
QT_TRANSLATE_NOOP(
"App::Property", "Expression used for the start depth of new operations."
),
)
obj.addProperty(
"App::PropertyString",
"FinalDepthExpression",
"OperationDepths",
QT_TRANSLATE_NOOP(
"App::Property", "Expression used for the final depth of new operations."
),
)
obj.addProperty(
"App::PropertyString",
"StepDownExpression",
"OperationDepths",
QT_TRANSLATE_NOOP("App::Property", "Expression used for step down of new operations."),
)
obj.SafeHeightOffset = self.decodeAttributeString(self.DefaultSafeHeightOffset)
obj.ClearanceHeightOffset = self.decodeAttributeString(self.DefaultClearanceHeightOffset)
obj.SafeHeightExpression = self.decodeAttributeString(self.DefaultSafeHeightExpression)
obj.ClearanceHeightExpression = self.decodeAttributeString(
self.DefaultClearanceHeightExpression
)
obj.StartDepthExpression = self.decodeAttributeString(self.DefaultStartDepthExpression)
obj.FinalDepthExpression = self.decodeAttributeString(self.DefaultFinalDepthExpression)
obj.StepDownExpression = self.decodeAttributeString(self.DefaultStepDownExpression)
obj.CoolantModes = self.DefaultCoolantModes
obj.CoolantMode = self.DefaultCoolantModes
obj.Proxy = self
def dumps(self):
return None
def loads(self, state):
for obj in FreeCAD.ActiveDocument.Objects:
if hasattr(obj, "Proxy") and obj.Proxy == self:
self.obj = obj
break
return None
def hasDefaultToolRapids(self):
return Path.Geom.isRoughly(self.obj.VertRapid.Value, 0) and Path.Geom.isRoughly(
self.obj.HorizRapid.Value, 0
)
def hasDefaultOperationHeights(self):
if (
self.obj.SafeHeightOffset.UserString
!= FreeCAD.Units.Quantity(self.DefaultSafeHeightOffset).UserString
):
return False
if (
self.obj.ClearanceHeightOffset.UserString
!= FreeCAD.Units.Quantity(self.DefaultClearanceHeightOffset).UserString
):
return False
if self.obj.SafeHeightExpression != self.decodeAttributeString(
self.DefaultSafeHeightExpression
):
return False
if self.obj.ClearanceHeightExpression != self.decodeAttributeString(
self.DefaultClearanceHeightExpression
):
return False
return True
def hasDefaultOperationDepths(self):
if self.obj.StartDepthExpression != self.DefaultStartDepthExpression:
return False
if self.obj.FinalDepthExpression != self.DefaultFinalDepthExpression:
return False
if self.obj.StepDownExpression != self.DefaultStepDownExpression:
return False
return True
def hasDefaultCoolantMode(self):
return self.obj.CoolantMode == "None"
def setFromTemplate(self, attrs):
"""setFromTemplate(attrs) ... sets the default values from the given dictionary."""
for name in Template.All:
if attrs.get(name) is not None:
setattr(self.obj, name, attrs[name])
for opName, op in _RegisteredOps.items():
opSetting = attrs.get(opName)
if opSetting is not None:
prototype = op.prototype(opName)
for propName in op.properties():
value = opSetting.get(propName)
if value is not None:
prop = prototype.getProperty(propName)
propertyName = OpPropertyName(opName, propName)
propertyGroup = OpPropertyGroup(opName)
prop.setupProperty(
self.obj,
propertyName,
propertyGroup,
prop.valueFromString(value),
)
def templateAttributes(
self,
includeRapids=True,
includeCoolantMode=True,
includeHeights=True,
includeDepths=True,
includeOps=None,
):
"""templateAttributes(includeRapids, includeHeights, includeDepths) ... answers a dictionary with the default values."""
attrs = {}
if includeRapids:
attrs[Template.VertRapid] = self.obj.VertRapid.UserString
attrs[Template.HorizRapid] = self.obj.HorizRapid.UserString
if includeCoolantMode:
attrs[Template.CoolantMode] = self.obj.CoolantMode
if includeHeights:
attrs[Template.SafeHeightOffset] = self.obj.SafeHeightOffset.UserString
attrs[Template.SafeHeightExpression] = self.obj.SafeHeightExpression
attrs[Template.ClearanceHeightOffset] = self.obj.ClearanceHeightOffset.UserString
attrs[Template.ClearanceHeightExpression] = self.obj.ClearanceHeightExpression
if includeDepths:
attrs[Template.StartDepthExpression] = self.obj.StartDepthExpression
attrs[Template.FinalDepthExpression] = self.obj.FinalDepthExpression
attrs[Template.StepDownExpression] = self.obj.StepDownExpression
if includeOps:
for opName in includeOps:
settings = {}
op = _RegisteredOps[opName]
for propName in op.properties():
prop = OpPropertyName(opName, propName)
if hasattr(self.obj, prop):
settings[propName] = PathUtil.getPropertyValueString(self.obj, prop)
attrs[opName] = settings
return attrs
def expressionReference(self):
"""expressionReference() ... returns the string to be used in expressions"""
# Using the Name here and not the Label (both would be valid) because the Name 'fails early'.
#
# If there is a Name/Label conflict and an expression is bound to the Name we'll get an error
# on creation (Property not found). Not good, but at least there's some indication that
# something's afoul.
#
# If the expression is based on the Label everything works out nicely - until the document is
# saved and loaded from disk. The Labels change in order to avoid the Name/Label conflict
# but the expression stays the same. If the user's lucky the expression is broken because the
# conflicting object doesn't have the properties reference by the expressions. If the user is
# not so lucky those properties also exist in the other object, there is no indication that
# anything is wrong but the expressions will substitute the values from the wrong object.
#
# I prefer the question: "why do I get this error when I create ..." over "my cnc machine just
# rammed it's tool head into the table ..." or even "I saved my file and now it's corrupt..."
#
# https://forum.freecad.org/viewtopic.php?f=10&t=24839
# https://forum.freecad.org/viewtopic.php?f=10&t=24845
return self.obj.Name
def encodeAttributeString(self, attr):
"""encodeAttributeString(attr) ... return the encoded string of a template attribute."""
return str(attr.replace(self.expressionReference(), self.TemplateReference))
def decodeAttributeString(self, attr):
"""decodeAttributeString(attr) ... return the decoded string of a template attribute."""
return str(attr.replace(self.TemplateReference, self.expressionReference()))
def encodeTemplateAttributes(self, attrs):
"""encodeTemplateAttributes(attrs) ... return a dictionary with all values encoded."""
return _traverseTemplateAttributes(attrs, self.encodeAttributeString)
def decodeTemplateAttributes(self, attrs):
"""decodeTemplateAttributes(attrs) ... expand template attributes to reference the receiver where applicable."""
return _traverseTemplateAttributes(attrs, self.decodeAttributeString)
def operationsWithSettings(self):
"""operationsWithSettings() ... returns a list of operations which currently have some settings defined."""
ops = []
for name, value in _RegisteredOps.items():
for prop in value.registeredPropertyNames(name):
if hasattr(self.obj, prop):
ops.append(name)
break
return list(sorted(ops))
def setOperationProperties(self, obj, opName):
Path.Log.track(obj.Label, opName)
try:
op = _RegisteredOps[opName]
for prop in op.properties():
propName = OpPropertyName(opName, prop)
if hasattr(self.obj, propName):
setattr(obj, prop, getattr(self.obj, propName))
except Exception:
Path.Log.info("SetupSheet has no support for {}".format(opName))
def onDocumentRestored(self, obj):
if not hasattr(obj, "CoolantModes"):
obj.addProperty(
"App::PropertyStringList",
"CoolantModes",
"CoolantMode",
QT_TRANSLATE_NOOP("App::Property", "Coolant Modes"),
)
obj.CoolantModes = self.DefaultCoolantModes
if not hasattr(obj, "CoolantMode"):
obj.addProperty(
"App::PropertyEnumeration",
"CoolantMode",
"CoolantMode",
QT_TRANSLATE_NOOP("App::Property", "Default coolant mode."),
)
obj.CoolantMode = self.DefaultCoolantModes
def Create(name="SetupSheet"):
obj = FreeCAD.ActiveDocument.addObject("App::FeaturePython", name)
obj.Proxy = SetupSheet(obj)
return obj
class _RegisteredOp(object):
def __init__(self, factory, properties):
self.factory = factory
self.properties = properties
def registeredPropertyNames(self, name):
return [OpPropertyName(name, prop) for prop in self.properties()]
def prototype(self, name):
ptt = PathSetupSheetOpPrototype.OpPrototype(name)
self.factory("OpPrototype.%s" % name, ptt)
return ptt
def RegisterOperation(name, objFactory, setupProperties):
global _RegisteredOps
_RegisteredOps[name] = _RegisteredOp(objFactory, setupProperties)
def OpNamePrefix(name):
return name.replace("Path", "").replace(" ", "").replace("_", "")
def OpPropertyName(opName, propName):
return "{}{}".format(OpNamePrefix(opName), propName)
def OpPropertyGroup(opName):
return "Op {}".format(opName)