| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | import FreeCAD |
| | from PathScripts.PathUtils import waiting_effects |
| | from PySide.QtCore import QT_TRANSLATE_NOOP |
| | import Path |
| | import Path.Base.Util as PathUtil |
| | import PathScripts.PathUtils as PathUtils |
| | import math |
| | import time |
| |
|
| |
|
| | |
| | from lazy_loader.lazy_loader import LazyLoader |
| |
|
| | Part = LazyLoader("Part", globals(), "Part") |
| |
|
| | __title__ = "Base class for all operations." |
| | __author__ = "sliptonic (Brad Collette)" |
| | __url__ = "https://www.freecad.org" |
| | __doc__ = "Base class and properties implementation for all CAM operations." |
| |
|
| | 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 |
| |
|
| |
|
| | FeatureTool = 0x0001 |
| | FeatureDepths = 0x0002 |
| | FeatureHeights = 0x0004 |
| | FeatureStartPoint = 0x0008 |
| | FeatureFinishDepth = 0x0010 |
| | FeatureStepDown = 0x0020 |
| | FeatureNoFinalDepth = 0x0040 |
| | FeatureBaseVertexes = 0x0100 |
| | FeatureBaseEdges = 0x0200 |
| | FeatureBaseFaces = 0x0400 |
| | FeatureBasePanels = 0x0800 |
| | FeatureLocations = 0x1000 |
| | FeatureCoolant = 0x2000 |
| | FeatureDiameters = 0x4000 |
| |
|
| | FeatureBaseGeometry = FeatureBaseVertexes | FeatureBaseFaces | FeatureBaseEdges |
| |
|
| |
|
| | class PathNoTCException(Exception): |
| | """PathNoTCException is raised when no TC was selected or matches the input |
| | criteria. This can happen intentionally by the user when they cancel the TC |
| | selection dialog.""" |
| |
|
| | def __init__(self): |
| | super().__init__("No Tool Controller found") |
| |
|
| |
|
| | class ObjectOp(object): |
| | """ |
| | Base class for proxy objects of all Path operations. |
| | |
| | Use this class as a base class for new operations. It provides properties |
| | and some functionality for the standard properties each operation supports. |
| | By OR'ing features from the feature list an operation can select which ones |
| | of the standard features it requires and/or supports. |
| | |
| | The currently supported features are: |
| | FeatureTool ... Use of a ToolController |
| | FeatureDepths ... Depths, for start, final |
| | FeatureHeights ... Heights, safe and clearance |
| | FeatureStartPoint ... Supports setting a start point |
| | FeatureFinishDepth ... Operation supports a finish depth |
| | FeatureStepDown ... Support for step down |
| | FeatureNoFinalDepth ... Disable support for final depth modifications |
| | FeatureBaseVertexes ... Base geometry support for vertexes |
| | FeatureBaseEdges ... Base geometry support for edges |
| | FeatureBaseFaces ... Base geometry support for faces |
| | FeatureLocations ... Base location support |
| | FeatureCoolant ... Support for operation coolant |
| | FeatureDiameters ... Support for turning operation diameters |
| | |
| | The base class handles all base API and forwards calls to subclasses with |
| | an op prefix. For instance, an op is not expected to overwrite onChanged(), |
| | but implement the function opOnChanged(). |
| | If a base class overwrites a base API function it should call the super's |
| | implementation - otherwise the base functionality might be broken. |
| | """ |
| |
|
| | def addBaseProperty(self, obj): |
| | obj.addProperty( |
| | "App::PropertyLinkSubListGlobal", |
| | "Base", |
| | "Path", |
| | QT_TRANSLATE_NOOP("App::Property", "The base geometry for this operation"), |
| | ) |
| |
|
| | def addOpValues(self, obj, values): |
| | if "start" in values: |
| | obj.addProperty( |
| | "App::PropertyDistance", |
| | "OpStartDepth", |
| | "Op Values", |
| | QT_TRANSLATE_NOOP("App::Property", "Holds the calculated value for the StartDepth"), |
| | ) |
| | obj.setEditorMode("OpStartDepth", 1) |
| | if "final" in values: |
| | obj.addProperty( |
| | "App::PropertyDistance", |
| | "OpFinalDepth", |
| | "Op Values", |
| | QT_TRANSLATE_NOOP("App::Property", "Holds the calculated value for the FinalDepth"), |
| | ) |
| | obj.setEditorMode("OpFinalDepth", 1) |
| | if "tooldia" in values: |
| | obj.addProperty( |
| | "App::PropertyDistance", |
| | "OpToolDiameter", |
| | "Op Values", |
| | QT_TRANSLATE_NOOP("App::Property", "Holds the diameter of the tool"), |
| | ) |
| | obj.setEditorMode("OpToolDiameter", 1) |
| | if "stockz" in values: |
| | obj.addProperty( |
| | "App::PropertyDistance", |
| | "OpStockZMax", |
| | "Op Values", |
| | QT_TRANSLATE_NOOP("App::Property", "Holds the max Z value of Stock"), |
| | ) |
| | obj.setEditorMode("OpStockZMax", 1) |
| | obj.addProperty( |
| | "App::PropertyDistance", |
| | "OpStockZMin", |
| | "Op Values", |
| | QT_TRANSLATE_NOOP("App::Property", "Holds the min Z value of Stock"), |
| | ) |
| | obj.setEditorMode("OpStockZMin", 1) |
| |
|
| | def __init__(self, obj, name, parentJob=None): |
| | Path.Log.track() |
| |
|
| | obj.addProperty( |
| | "App::PropertyBool", |
| | "Active", |
| | "Path", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", "Make False, to prevent operation from generating code" |
| | ), |
| | ) |
| | obj.addProperty( |
| | "App::PropertyString", |
| | "Comment", |
| | "Path", |
| | QT_TRANSLATE_NOOP("App::Property", "An optional comment for this Operation"), |
| | ) |
| | obj.addProperty( |
| | "App::PropertyString", |
| | "UserLabel", |
| | "Path", |
| | QT_TRANSLATE_NOOP("App::Property", "User Assigned Label"), |
| | ) |
| | obj.addProperty( |
| | "App::PropertyString", |
| | "CycleTime", |
| | "Path", |
| | QT_TRANSLATE_NOOP("App::Property", "Operations Cycle Time Estimation"), |
| | ) |
| | obj.setEditorMode("CycleTime", 1) |
| |
|
| | features = self.opFeatures(obj) |
| |
|
| | if FeatureBaseGeometry & features: |
| | self.addBaseProperty(obj) |
| |
|
| | if FeatureLocations & features: |
| | obj.addProperty( |
| | "App::PropertyVectorList", |
| | "Locations", |
| | "Path", |
| | QT_TRANSLATE_NOOP("App::Property", "Base locations for this operation"), |
| | ) |
| |
|
| | if FeatureTool & features: |
| | obj.addProperty( |
| | "App::PropertyLink", |
| | "ToolController", |
| | "Path", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "The tool controller that will be used to calculate the path", |
| | ), |
| | ) |
| | self.addOpValues(obj, ["tooldia"]) |
| |
|
| | if FeatureCoolant & features: |
| | obj.addProperty( |
| | "App::PropertyEnumeration", |
| | "CoolantMode", |
| | "Path", |
| | QT_TRANSLATE_NOOP("App::Property", "Coolant mode for this operation"), |
| | ) |
| |
|
| | if FeatureDepths & features: |
| | obj.addProperty( |
| | "App::PropertyDistance", |
| | "StartDepth", |
| | "Depth", |
| | QT_TRANSLATE_NOOP("App::Property", "Starting Depth of Tool- first cut depth in Z"), |
| | ) |
| | obj.addProperty( |
| | "App::PropertyDistance", |
| | "FinalDepth", |
| | "Depth", |
| | QT_TRANSLATE_NOOP("App::Property", "Final Depth of Tool- lowest value in Z"), |
| | ) |
| | if FeatureNoFinalDepth & features: |
| | obj.setEditorMode("FinalDepth", 2) |
| | self.addOpValues(obj, ["start", "final"]) |
| | else: |
| | |
| | obj.addProperty( |
| | "App::PropertyDistance", |
| | "StartDepth", |
| | "Depth", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "Starting Depth internal use only for derived values", |
| | ), |
| | ) |
| | obj.setEditorMode("StartDepth", 1) |
| |
|
| | self.addOpValues(obj, ["stockz"]) |
| |
|
| | if FeatureStepDown & features: |
| | obj.addProperty( |
| | "App::PropertyDistance", |
| | "StepDown", |
| | "Depth", |
| | QT_TRANSLATE_NOOP("App::Property", "Incremental Step Down of Tool"), |
| | ) |
| |
|
| | if FeatureFinishDepth & features: |
| | obj.addProperty( |
| | "App::PropertyDistance", |
| | "FinishDepth", |
| | "Depth", |
| | QT_TRANSLATE_NOOP("App::Property", "Maximum material removed on final pass."), |
| | ) |
| |
|
| | if FeatureHeights & features: |
| | obj.addProperty( |
| | "App::PropertyDistance", |
| | "ClearanceHeight", |
| | "Depth", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "The height needed to clear clamps and obstructions", |
| | ), |
| | ) |
| | obj.addProperty( |
| | "App::PropertyDistance", |
| | "SafeHeight", |
| | "Depth", |
| | QT_TRANSLATE_NOOP("App::Property", "Rapid Safety Height between locations."), |
| | ) |
| |
|
| | if FeatureStartPoint & features: |
| | obj.addProperty( |
| | "App::PropertyVectorDistance", |
| | "StartPoint", |
| | "Start Point", |
| | QT_TRANSLATE_NOOP("App::Property", "The start point of this path"), |
| | ) |
| | obj.addProperty( |
| | "App::PropertyBool", |
| | "UseStartPoint", |
| | "Start Point", |
| | QT_TRANSLATE_NOOP("App::Property", "Make True, if specifying a Start Point"), |
| | ) |
| |
|
| | if FeatureDiameters & features: |
| | obj.addProperty( |
| | "App::PropertyDistance", |
| | "MinDiameter", |
| | "Diameter", |
| | QT_TRANSLATE_NOOP("App::Property", "Lower limit of the turning diameter"), |
| | ) |
| | obj.addProperty( |
| | "App::PropertyDistance", |
| | "MaxDiameter", |
| | "Diameter", |
| | QT_TRANSLATE_NOOP("App::Property", "Upper limit of the turning diameter."), |
| | ) |
| |
|
| | |
| | self.commandlist = None |
| | self.horizFeed = None |
| | self.horizRapid = None |
| | self.job = None |
| | self.model = None |
| | self.radius = None |
| | self.stock = None |
| | self.tool = None |
| | self.vertFeed = None |
| | self.vertRapid = None |
| | self.addNewProps = None |
| |
|
| | self.initOperation(obj) |
| |
|
| | for n in self.opPropertyEnumerations(): |
| | Path.Log.debug("n: {}".format(n)) |
| | Path.Log.debug("n[0]: {} n[1]: {}".format(n[0], n[1])) |
| | if hasattr(obj, n[0]): |
| | setattr(obj, n[0], n[1]) |
| |
|
| | if not hasattr(obj, "DoNotSetDefaultValues") or not obj.DoNotSetDefaultValues: |
| | if parentJob: |
| | self.job = parentJob |
| | self.model = parentJob.Model.Group if parentJob.Model else [] |
| | self.stock = parentJob.Stock if hasattr(parentJob, "Stock") else None |
| | PathUtils.addToJob(obj, jobname=parentJob.Name) |
| | job = self.setDefaultValues(obj) |
| | if job: |
| | job.SetupSheet.Proxy.setOperationProperties(obj, name) |
| | obj.recompute() |
| | obj.Proxy = self |
| |
|
| | @classmethod |
| | def opPropertyEnumerations(self, dataType="data"): |
| | """opPropertyEnumerations(dataType="data")... return property enumeration lists of specified dataType. |
| | Args: |
| | dataType = 'data', 'raw', 'translated' |
| | Notes: |
| | 'data' is list of internal string literals used in code |
| | 'raw' is list of (translated_text, data_string) tuples |
| | 'translated' is list of translated string literals |
| | """ |
| |
|
| | enums = { |
| | "CoolantMode": [ |
| | (translate("CAM_Operation", "None"), "None"), |
| | (translate("CAM_Operation", "Flood"), "Flood"), |
| | (translate("CAM_Operation", "Mist"), "Mist"), |
| | ], |
| | } |
| |
|
| | if dataType == "raw": |
| | return enums |
| |
|
| | data = list() |
| | idx = 0 if dataType == "translated" else 1 |
| |
|
| | Path.Log.debug(enums) |
| |
|
| | for k, v in enumerate(enums): |
| | data.append((v, [tup[idx] for tup in enums[v]])) |
| | Path.Log.debug(data) |
| |
|
| | return data |
| |
|
| | def setEditorModes(self, obj, features): |
| | """Editor modes are not preserved during document store/restore, set editor modes for all properties""" |
| |
|
| | for op in ["OpStartDepth", "OpFinalDepth", "OpToolDiameter", "CycleTime"]: |
| | if hasattr(obj, op): |
| | obj.setEditorMode(op, 1) |
| |
|
| | if FeatureDepths & features: |
| | if FeatureNoFinalDepth & features: |
| | obj.setEditorMode("OpFinalDepth", 2) |
| |
|
| | def onDocumentRestored(self, obj): |
| | Path.Log.track() |
| | features = self.opFeatures(obj) |
| | if ( |
| | FeatureBaseGeometry & features |
| | and "App::PropertyLinkSubList" == obj.getTypeIdOfProperty("Base") |
| | ): |
| | Path.Log.info("Replacing link property with global link (%s)." % obj.State) |
| | base = obj.Base |
| | obj.removeProperty("Base") |
| | self.addBaseProperty(obj) |
| | obj.Base = base |
| | obj.touch() |
| | obj.Document.recompute() |
| |
|
| | if FeatureTool & features and not hasattr(obj, "OpToolDiameter"): |
| | self.addOpValues(obj, ["tooldia"]) |
| |
|
| | if FeatureCoolant & features: |
| | oldvalue = str(obj.CoolantMode) if hasattr(obj, "CoolantMode") else "None" |
| | if ( |
| | hasattr(obj, "CoolantMode") |
| | and not obj.getTypeIdOfProperty("CoolantMode") == "App::PropertyEnumeration" |
| | ): |
| | obj.removeProperty("CoolantMode") |
| |
|
| | if not hasattr(obj, "CoolantMode"): |
| | obj.addProperty( |
| | "App::PropertyEnumeration", |
| | "CoolantMode", |
| | "Path", |
| | QT_TRANSLATE_NOOP("App::Property", "Coolant option for this operation"), |
| | ) |
| | for n in self.opPropertyEnumerations(): |
| | if n[0] == "CoolantMode": |
| | setattr(obj, n[0], n[1]) |
| | obj.CoolantMode = oldvalue |
| |
|
| | if FeatureDepths & features and not hasattr(obj, "OpStartDepth"): |
| | self.addOpValues(obj, ["start", "final"]) |
| | if FeatureNoFinalDepth & features: |
| | obj.setEditorMode("OpFinalDepth", 2) |
| |
|
| | if not hasattr(obj, "OpStockZMax"): |
| | self.addOpValues(obj, ["stockz"]) |
| |
|
| | if not hasattr(obj, "CycleTime"): |
| | obj.addProperty( |
| | "App::PropertyString", |
| | "CycleTime", |
| | "Path", |
| | QT_TRANSLATE_NOOP("App::Property", "Operations Cycle Time Estimation"), |
| | ) |
| |
|
| | if FeatureStepDown & features and not hasattr(obj, "StepDown"): |
| | obj.addProperty( |
| | "App::PropertyDistance", |
| | "StepDown", |
| | "Depth", |
| | QT_TRANSLATE_NOOP("App::Property", "Incremental Step Down of Tool"), |
| | ) |
| | obj.StepDown = 0 |
| |
|
| | self.setEditorModes(obj, features) |
| | self.opOnDocumentRestored(obj) |
| |
|
| | def dumps(self): |
| | """__getstat__(self) ... called when receiver is saved. |
| | Can safely be overwritten by subclasses.""" |
| | return None |
| |
|
| | def loads(self, state): |
| | """__getstat__(self) ... called when receiver is restored. |
| | Can safely be overwritten by subclasses.""" |
| | return None |
| |
|
| | def opFeatures(self, obj): |
| | """opFeatures(obj) ... returns the OR'ed list of features used and supported by the operation. |
| | The default implementation returns "FeatureTool | FeatureDepths | FeatureHeights | FeatureStartPoint" |
| | Should be overwritten by subclasses.""" |
| | return ( |
| | FeatureTool |
| | | FeatureDepths |
| | | FeatureHeights |
| | | FeatureStartPoint |
| | | FeatureBaseGeometry |
| | | FeatureFinishDepth |
| | | FeatureCoolant |
| | ) |
| |
|
| | def initOperation(self, obj): |
| | """initOperation(obj) ... implement to create additional properties. |
| | Should be overwritten by subclasses.""" |
| | pass |
| |
|
| | def opOnDocumentRestored(self, obj): |
| | """opOnDocumentRestored(obj) ... implement if an op needs special handling like migrating the data model. |
| | Should be overwritten by subclasses.""" |
| | pass |
| |
|
| | def opOnChanged(self, obj, prop): |
| | """opOnChanged(obj, prop) ... overwrite to process property changes. |
| | This is a callback function that is invoked each time a property of the |
| | receiver is assigned a value. Note that the FC framework does not |
| | distinguish between assigning a different value and assigning the same |
| | value again. |
| | Can safely be overwritten by subclasses.""" |
| | pass |
| |
|
| | def opSetDefaultValues(self, obj, job): |
| | """opSetDefaultValues(obj, job) ... overwrite to set initial default values. |
| | Called after the receiver has been fully created with all properties. |
| | Can safely be overwritten by subclasses.""" |
| | pass |
| |
|
| | def opUpdateDepths(self, obj): |
| | """opUpdateDepths(obj) ... overwrite to implement special depths calculation. |
| | Can safely be overwritten by subclass.""" |
| | pass |
| |
|
| | def opExecute(self, obj): |
| | """opExecute(obj) ... called whenever the receiver needs to be recalculated. |
| | See documentation of execute() for a list of base functionality provided. |
| | Should be overwritten by subclasses.""" |
| | pass |
| |
|
| | def opRejectAddBase(self, obj, base, sub): |
| | """opRejectAddBase(base, sub) ... if op returns True the addition of the feature is prevented. |
| | Should be overwritten by subclasses.""" |
| | return False |
| |
|
| | def onChanged(self, obj, prop): |
| | """onChanged(obj, prop) ... base implementation of the FC notification framework. |
| | Do not overwrite, overwrite opOnChanged() instead.""" |
| |
|
| | |
| | |
| | |
| | if prop == "Base" and self.sanitizeBase(obj): |
| | return |
| |
|
| | if "Restore" not in obj.State and prop in ["Base", "StartDepth", "FinalDepth"]: |
| | self.updateDepths(obj, True) |
| |
|
| | self.opOnChanged(obj, prop) |
| |
|
| | if prop == "Active" and obj.ViewObject: |
| | obj.ViewObject.signalChangeIcon() |
| |
|
| | def applyExpression(self, obj, prop, expr): |
| | """applyExpression(obj, prop, expr) ... set expression expr on obj.prop if expr is set""" |
| | if expr: |
| | obj.setExpression(prop, expr) |
| | return True |
| | return False |
| |
|
| | def setDefaultValues(self, obj): |
| | """setDefaultValues(obj) ... base implementation. |
| | Do not overwrite, overwrite opSetDefaultValues() instead.""" |
| | if self.job: |
| | job = self.job |
| | else: |
| | job = PathUtils.addToJob(obj) |
| | if not job: |
| | raise ValueError( |
| | "No job associated with the operation. Please ensure the operation is part of a job." |
| | ) |
| | obj.Active = True |
| |
|
| | features = self.opFeatures(obj) |
| |
|
| | if FeatureTool & features: |
| | for op in job.Operations.Group[-2::-1]: |
| | obj.ToolController = PathUtil.toolControllerForOp(op) |
| | if obj.ToolController: |
| | break |
| | else: |
| | obj.ToolController = PathUtils.findToolController(obj, self) |
| | if not obj.ToolController: |
| | raise PathNoTCException() |
| | obj.OpToolDiameter = obj.ToolController.Tool.Diameter |
| |
|
| | if FeatureCoolant & features: |
| | Path.Log.track() |
| | Path.Log.debug(obj.getEnumerationsOfProperty("CoolantMode")) |
| | obj.CoolantMode = job.SetupSheet.CoolantMode |
| |
|
| | if FeatureDepths & features: |
| | if self.applyExpression(obj, "StartDepth", job.SetupSheet.StartDepthExpression): |
| | obj.OpStartDepth = 1.0 |
| | else: |
| | obj.StartDepth = 1.0 |
| | if self.applyExpression(obj, "FinalDepth", job.SetupSheet.FinalDepthExpression): |
| | obj.OpFinalDepth = 0.0 |
| | else: |
| | obj.FinalDepth = 0.0 |
| | else: |
| | obj.StartDepth = 1.0 |
| |
|
| | if FeatureStepDown & features: |
| | if not self.applyExpression(obj, "StepDown", job.SetupSheet.StepDownExpression): |
| | obj.StepDown = "1 mm" |
| |
|
| | if FeatureHeights & features: |
| | if job.SetupSheet.SafeHeightExpression: |
| | if not self.applyExpression(obj, "SafeHeight", job.SetupSheet.SafeHeightExpression): |
| | obj.SafeHeight = "3 mm" |
| | if job.SetupSheet.ClearanceHeightExpression: |
| | if not self.applyExpression( |
| | obj, "ClearanceHeight", job.SetupSheet.ClearanceHeightExpression |
| | ): |
| | obj.ClearanceHeight = "5 mm" |
| |
|
| | if FeatureDiameters & features: |
| | obj.MinDiameter = "0 mm" |
| | obj.MaxDiameter = "0 mm" |
| | if job.Stock: |
| | obj.MaxDiameter = job.Stock.Shape.BoundBox.XLength |
| |
|
| | if FeatureStartPoint & features: |
| | obj.UseStartPoint = False |
| |
|
| | self.opSetDefaultValues(obj, job) |
| | return job |
| |
|
| | def _setBaseAndStock(self, obj, ignoreErrors=False): |
| | job = PathUtils.findParentJob(obj) |
| |
|
| | if not job: |
| | if not ignoreErrors: |
| | Path.Log.error(translate("CAM", "No parent job found for operation.")) |
| | return False |
| | if not job.Model.Group: |
| | if not ignoreErrors: |
| | Path.Log.error( |
| | translate("CAM", "Parent job %s doesn't have a base object") % job.Label |
| | ) |
| | return False |
| | self.job = job |
| | self.model = job.Model.Group |
| | self.stock = job.Stock |
| | return True |
| |
|
| | def getJob(self, obj): |
| | """getJob(obj) ... return the job this operation is part of.""" |
| | if not hasattr(self, "job") or self.job is None: |
| | if not self._setBaseAndStock(obj): |
| | return None |
| | return self.job |
| |
|
| | def updateDepths(self, obj, ignoreErrors=False): |
| | """updateDepths(obj) ... base implementation calculating depths depending on base geometry. |
| | Should not be overwritten.""" |
| |
|
| | def faceZmin(bb, fbb): |
| | if fbb.ZMax == fbb.ZMin and fbb.ZMax == bb.ZMax: |
| | return fbb.ZMin |
| | elif fbb.ZMax > fbb.ZMin and fbb.ZMax == bb.ZMax: |
| | return fbb.ZMin |
| | elif fbb.ZMax > fbb.ZMin and fbb.ZMin > bb.ZMin: |
| | return fbb.ZMin |
| | elif fbb.ZMax == fbb.ZMin and fbb.ZMax > bb.ZMin: |
| | return fbb.ZMin |
| | return bb.ZMin |
| |
|
| | if not self._setBaseAndStock(obj, ignoreErrors): |
| | return False |
| |
|
| | stockBB = self.stock.Shape.BoundBox |
| | zmin = stockBB.ZMin |
| | zmax = stockBB.ZMax |
| |
|
| | obj.OpStockZMin = zmin |
| | obj.OpStockZMax = zmax |
| |
|
| | if hasattr(obj, "Base") and obj.Base: |
| | for base, sublist in obj.Base: |
| | bb = base.Shape.BoundBox |
| | zmax = max(zmax, bb.ZMax) |
| | for sub in sublist: |
| | try: |
| | if sub: |
| | fbb = base.Shape.getElement(sub).BoundBox |
| | else: |
| | fbb = base.Shape.BoundBox |
| | zmin = max(zmin, faceZmin(bb, fbb)) |
| | zmax = max(zmax, fbb.ZMax) |
| | except Part.OCCError as e: |
| | Path.Log.error(e) |
| |
|
| | else: |
| | |
| | job = PathUtils.findParentJob(obj) |
| | zmax = stockBB.ZMax |
| | zmin = job.Proxy.modelBoundBox(job).ZMax |
| |
|
| | if FeatureDepths & self.opFeatures(obj): |
| | |
| | if not Path.Geom.isRoughly(obj.OpFinalDepth.Value, zmin): |
| | obj.OpFinalDepth = zmin |
| | zmin = obj.OpFinalDepth.Value |
| |
|
| | def minZmax(z): |
| | if hasattr(obj, "StepDown") and not Path.Geom.isRoughly(obj.StepDown.Value, 0): |
| | return z + obj.StepDown.Value |
| | else: |
| | return z + 1 |
| |
|
| | |
| | if (zmax - 0.0001) <= zmin: |
| | zmax = minZmax(zmin) |
| |
|
| | |
| | if not Path.Geom.isRoughly(obj.OpStartDepth.Value, zmax): |
| | obj.OpStartDepth = zmax |
| | else: |
| | |
| | if obj.StartDepth.Value != zmax: |
| | obj.StartDepth = zmax |
| |
|
| | self.opUpdateDepths(obj) |
| |
|
| | def sanitizeBase(self, obj): |
| | """sanitizeBase(obj) ... check if Base is valid and clear on errors.""" |
| | if hasattr(obj, "Base"): |
| | try: |
| | for o, sublist in obj.Base: |
| | for sub in sublist: |
| | o.Shape.getElement(sub) |
| | except Part.OCCError: |
| | Path.Log.error("{} - stale base geometry detected - clearing.".format(obj.Label)) |
| | obj.Base = [] |
| | return True |
| | return False |
| |
|
| | @waiting_effects |
| | def execute(self, obj): |
| | """execute(obj) ... base implementation - do not overwrite! |
| | Verifies that the operation is assigned to a job and that the job also has a valid Base. |
| | It also sets the following instance variables that can and should be safely be used by |
| | implementation of opExecute(): |
| | self.model ... List of base objects of the Job itself |
| | self.stock ... Stock object for the Job itself |
| | self.vertFeed ... vertical feed rate of assigned tool |
| | self.vertRapid ... vertical rapid rate of assigned tool |
| | self.horizFeed ... horizontal feed rate of assigned tool |
| | self.horizRapid ... norizontal rapid rate of assigned tool |
| | self.tool ... the actual tool being used |
| | self.radius ... the main radius of the tool being used |
| | self.commandlist ... a list for collecting all commands produced by the operation |
| | |
| | Once everything is validated and above variables are set the implementation calls |
| | opExecute(obj) - which is expected to add the generated commands to self.commandlist |
| | Finally the base implementation adds a rapid move to clearance height and assigns |
| | the receiver's Path property from the command list. |
| | """ |
| | Path.Log.track() |
| |
|
| | if not obj.Active: |
| | path = Path.Path("(inactive operation)") |
| | obj.Path = path |
| | return |
| |
|
| | if not self._setBaseAndStock(obj): |
| | return |
| |
|
| | |
| | self.sanitizeBase(obj) |
| |
|
| | if FeatureTool & self.opFeatures(obj): |
| | tc = obj.ToolController |
| | if tc is None or tc.ToolNumber == 0: |
| | Path.Log.error( |
| | translate( |
| | "CAM", |
| | "No Tool Controller is selected. We need a tool to build a Path.", |
| | ) |
| | ) |
| | return |
| | else: |
| | self.vertFeed = tc.VertFeed.Value |
| | self.horizFeed = tc.HorizFeed.Value |
| | self.vertRapid = tc.VertRapid.Value |
| | self.horizRapid = tc.HorizRapid.Value |
| | tool = tc.Proxy.getTool(tc) |
| | if not tool or float(tool.Diameter) == 0: |
| | Path.Log.error( |
| | translate( |
| | "CAM", |
| | "No Tool found or diameter is zero. We need a tool to build a Path.", |
| | ) |
| | ) |
| | return |
| | self.radius = float(tool.Diameter) / 2.0 |
| | self.tool = tool |
| | obj.OpToolDiameter = tool.Diameter |
| |
|
| | self.updateDepths(obj) |
| | |
| | |
| | obj.recompute() |
| |
|
| | self.commandlist = [] |
| | self.commandlist.append(Path.Command("(%s)" % obj.Label)) |
| | if obj.Comment: |
| | self.commandlist.append(Path.Command("(%s)" % obj.Comment)) |
| |
|
| | result = self.opExecute(obj) |
| |
|
| | if self.commandlist and (FeatureHeights & self.opFeatures(obj)): |
| | |
| | self.commandlist.append(Path.Command("G0", {"Z": obj.ClearanceHeight.Value})) |
| |
|
| | path = Path.Path(self.commandlist) |
| | obj.Path = path |
| | obj.CycleTime = getCycleTimeEstimate(obj) |
| | self.job.Proxy.getCycleTime() |
| | return result |
| |
|
| | def addBase(self, obj, base, sub): |
| | Path.Log.track(obj, base, sub) |
| | base = PathUtil.getPublicObject(base) |
| |
|
| | if self._setBaseAndStock(obj): |
| | for model in self.job.Model.Group: |
| | if base == self.job.Proxy.baseObject(self.job, model): |
| | base = model |
| | break |
| |
|
| | baselist = obj.Base |
| | if baselist is None: |
| | baselist = [] |
| |
|
| | for p, el in baselist: |
| | if p == base and sub in el: |
| | Path.Log.notice( |
| | (translate("CAM", "Base object %s.%s already in the list") + "\n") |
| | % (base.Label, sub) |
| | ) |
| | return |
| |
|
| | if not self.opRejectAddBase(obj, base, sub): |
| | baselist.append((base, sub)) |
| | obj.Base = baselist |
| | else: |
| | Path.Log.notice( |
| | (translate("CAM", "Base object %s.%s rejected by operation") + "\n") |
| | % (base.Label, sub) |
| | ) |
| |
|
| | def isToolSupported(self, obj, tool): |
| | """toolSupported(obj, tool) ... Returns true if the op supports the given tool. |
| | This function can safely be overwritten by subclasses.""" |
| |
|
| | return True |
| |
|
| |
|
| | class Compass: |
| | """ |
| | A compass is a tool to help with direction so the Compass is a helper |
| | class to manage settings that affect tool and spindle direction. |
| | |
| | Settings managed: |
| | - Spindle Direction: Forward / Reverse / None |
| | - Cut Side: Inside / Outside (for perimeter operations) |
| | - Cut Mode: Climb / Conventional |
| | - Path Direction: CW / CCW (derived for perimeter operations) |
| | - Operation Type: Perimeter / Area (for facing/pocketing operations) |
| | |
| | This class allows the user to set and get any of these properties and the rest will update accordingly. |
| | Supports both perimeter operations (profiling) and area operations (facing, pocketing). |
| | |
| | Args: |
| | spindle_direction: "Forward", "Reverse", or "None" |
| | operation_type: "Perimeter" or "Area" (defaults to "Perimeter") |
| | """ |
| |
|
| | FORWARD = "Forward" |
| | REVERSE = "Reverse" |
| | NONE = "None" |
| | CW = "CW" |
| | CCW = "CCW" |
| | CLIMB = "Climb" |
| | CONVENTIONAL = "Conventional" |
| | INSIDE = "Inside" |
| | OUTSIDE = "Outside" |
| | PERIMETER = "Perimeter" |
| | AREA = "Area" |
| |
|
| | def __init__(self, spindle_direction, operation_type=None): |
| | self._spindle_dir = ( |
| | spindle_direction |
| | if spindle_direction in (self.FORWARD, self.REVERSE, self.NONE) |
| | else self.NONE |
| | ) |
| | self._cut_side = self.OUTSIDE |
| | self._cut_mode = self.CLIMB |
| | self._operation_type = ( |
| | operation_type or self.PERIMETER |
| | ) |
| | self._path_dir = self._calculate_path_dir() |
| |
|
| | @property |
| | def spindle_dir(self): |
| | return self._spindle_dir |
| |
|
| | @spindle_dir.setter |
| | def spindle_dir(self, value): |
| | if value in (self.FORWARD, self.REVERSE, self.NONE): |
| | self._spindle_dir = value |
| | self._path_dir = self._calculate_path_dir() |
| | else: |
| | self._spindle_dir = self.NONE |
| | self._path_dir = self._calculate_path_dir() |
| |
|
| | @property |
| | def cut_side(self): |
| | return self._cut_side |
| |
|
| | @cut_side.setter |
| | def cut_side(self, value): |
| | self._cut_side = value.capitalize() |
| | self._path_dir = self._calculate_path_dir() |
| |
|
| | @property |
| | def cut_mode(self): |
| | return self._cut_mode |
| |
|
| | @cut_mode.setter |
| | def cut_mode(self, value): |
| | self._cut_mode = value.capitalize() |
| | self._path_dir = self._calculate_path_dir() |
| |
|
| | @property |
| | def operation_type(self): |
| | return self._operation_type |
| |
|
| | @operation_type.setter |
| | def operation_type(self, value): |
| | self._operation_type = value.capitalize() |
| | self._path_dir = self._calculate_path_dir() |
| |
|
| | @property |
| | def path_dir(self): |
| | return self._path_dir |
| |
|
| | def _calculate_path_dir(self): |
| | if self.spindle_dir == self.NONE: |
| | return "UNKNOWN" |
| |
|
| | |
| | if self._operation_type == self.AREA: |
| | return "N/A" |
| |
|
| | spindle_rotation = self._rotation_from_spindle(self.spindle_dir) |
| |
|
| | for candidate in (self.CW, self.CCW): |
| | mode = self._expected_cut_mode(self._cut_side, spindle_rotation, candidate) |
| | if mode == self._cut_mode: |
| | return candidate |
| |
|
| | return "UNKNOWN" |
| |
|
| | def _rotation_from_spindle(self, direction): |
| | return self.CW if direction == self.FORWARD else self.CCW |
| |
|
| | def _expected_cut_mode(self, cut_side, spindle_rotation, path_dir): |
| | lookup = { |
| | (self.INSIDE, self.CW, self.CCW): self.CLIMB, |
| | (self.INSIDE, self.CCW, self.CW): self.CLIMB, |
| | (self.OUTSIDE, self.CW, self.CW): self.CLIMB, |
| | (self.OUTSIDE, self.CCW, self.CCW): self.CLIMB, |
| | } |
| | return lookup.get((cut_side, spindle_rotation, path_dir), self.CONVENTIONAL) |
| |
|
| | def get_step_direction(self, approach_direction): |
| | """ |
| | For area operations, determine the step direction for climb/conventional milling. |
| | |
| | Args: |
| | approach_direction: "X+", "X-", "Y+", "Y-" - the primary cutting direction |
| | |
| | Returns: |
| | True if steps should be in positive direction, False for negative direction |
| | """ |
| | if self._operation_type != self.AREA: |
| | raise ValueError("Step direction is only applicable for area operations") |
| |
|
| | if self.spindle_dir == self.NONE: |
| | return True |
| |
|
| | spindle_rotation = self._rotation_from_spindle(self.spindle_dir) |
| |
|
| | |
| | |
| | if approach_direction in ["X-", "X+"]: |
| | |
| | if self._cut_mode == self.CLIMB: |
| | |
| | return (approach_direction == "X-") == (spindle_rotation == self.CW) |
| | else: |
| | |
| | return (approach_direction == "X-") != (spindle_rotation == self.CW) |
| | else: |
| | |
| | if self._cut_mode == self.CLIMB: |
| | |
| | return (approach_direction == "Y-") == (spindle_rotation == self.CW) |
| | else: |
| | |
| | return (approach_direction == "Y-") != (spindle_rotation == self.CW) |
| |
|
| | def get_cutting_direction(self, approach_direction, pass_index=0, pattern="zigzag"): |
| | """ |
| | For area operations, determine the cutting direction for each pass. |
| | |
| | Args: |
| | approach_direction: "X+", "X-", "Y+", "Y-" - the primary cutting direction |
| | pass_index: Index of the current pass (0-based) |
| | pattern: "zigzag", "unidirectional", "spiral" |
| | |
| | Returns: |
| | True if cutting should be in forward direction, False for reverse |
| | """ |
| | if self._operation_type != self.AREA: |
| | raise ValueError("Cutting direction is only applicable for area operations") |
| |
|
| | if self.spindle_dir == self.NONE: |
| | return True |
| |
|
| | spindle_rotation = self._rotation_from_spindle(self.spindle_dir) |
| |
|
| | |
| | if approach_direction in ["X-", "X+"]: |
| | |
| | if self._cut_mode == self.CLIMB: |
| | base_forward = (approach_direction == "X-") == (spindle_rotation == self.CW) |
| | else: |
| | base_forward = (approach_direction == "X-") != (spindle_rotation == self.CW) |
| | else: |
| | |
| | if self._cut_mode == self.CLIMB: |
| | base_forward = (approach_direction == "Y-") == (spindle_rotation == self.CW) |
| | else: |
| | base_forward = (approach_direction == "Y-") != (spindle_rotation == self.CW) |
| |
|
| | |
| | if pattern == "zigzag" and pass_index % 2 == 1: |
| | base_forward = not base_forward |
| | elif pattern == "unidirectional": |
| | |
| | pass |
| |
|
| | return base_forward |
| |
|
| | def report(self): |
| | report_data = { |
| | "spindle_dir": self.spindle_dir, |
| | "cut_side": self.cut_side, |
| | "cut_mode": self.cut_mode, |
| | "operation_type": self.operation_type, |
| | "path_dir": self.path_dir, |
| | } |
| |
|
| | Path.Log.debug("Machining Compass config:") |
| | for k, v in report_data.items(): |
| | Path.Log.debug(f" {k:15s}: {v}") |
| | return report_data |
| |
|
| |
|
| | def getCycleTimeEstimate(obj): |
| | tc = obj.ToolController |
| |
|
| | if tc is None or tc.ToolNumber == 0: |
| | Path.Log.error(translate("CAM", "No Tool Controller selected.")) |
| | return translate("CAM", "Tool Error") |
| |
|
| | hFeedrate = tc.HorizFeed.Value |
| | vFeedrate = tc.VertFeed.Value |
| | hRapidrate = tc.HorizRapid.Value |
| | vRapidrate = tc.VertRapid.Value |
| |
|
| | if hFeedrate == 0 or vFeedrate == 0: |
| | if not Path.Preferences.suppressAllSpeedsWarning(): |
| | Path.Log.warning( |
| | translate( |
| | "CAM", |
| | "Tool Controller feedrates required to calculate the cycle time.", |
| | ) |
| | ) |
| | return translate("CAM", "Tool Feedrate Error") |
| |
|
| | if (hRapidrate == 0 or vRapidrate == 0) and not Path.Preferences.suppressRapidSpeedsWarning(): |
| | Path.Log.warning( |
| | translate( |
| | "CAM", |
| | "Add Tool Controller Rapid Speeds on the SetupSheet for more accurate cycle times.", |
| | ) |
| | ) |
| |
|
| | |
| | seconds = obj.Path.getCycleTime(hFeedrate, vFeedrate, hRapidrate, vRapidrate) |
| |
|
| | if math.isnan(seconds): |
| | return translate("CAM", "Cycletime Error") |
| |
|
| | |
| | cycleTime = time.strftime("%H:%M:%S", time.gmtime(seconds)) |
| |
|
| | return cycleTime |
| |
|