# -*- coding: utf-8 -*- # *************************************************************************** # * Copyright (c) 2017 sliptonic * # * * # * 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)