Download src/Mod/CAM/Path/Op/Base.py from AbdulElahGwaith/FreeCAD: direct link, hf CLI and curl.
- Browser
- Download file 42 kB
-
https://huggingface.co/AbdulElahGwaith/FreeCAD/resolve/main/src/Mod/CAM/Path/Op/Base.py
- Command line
-
hf download hf://AbdulElahGwaith/FreeCAD/src/Mod/CAM/Path/Op/Base.py
-
curl -L -o Base.py https://huggingface.co/AbdulElahGwaith/FreeCAD/resolve/main/src/Mod/CAM/Path/Op/Base.py
42 kB
| # SPDX-License-Identifier: LGPL-2.1-or-later | |
| # *************************************************************************** | |
| # * 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 | |
| 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 | |
| # lazily loaded modules | |
| 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 # ToolController | |
| FeatureDepths = 0x0002 # FinalDepth, StartDepth | |
| FeatureHeights = 0x0004 # ClearanceHeight, SafeHeight | |
| FeatureStartPoint = 0x0008 # StartPoint | |
| FeatureFinishDepth = 0x0010 # FinishDepth | |
| FeatureStepDown = 0x0020 # StepDown | |
| FeatureNoFinalDepth = 0x0040 # edit or not edit FinalDepth | |
| FeatureBaseVertexes = 0x0100 # Base | |
| FeatureBaseEdges = 0x0200 # Base | |
| FeatureBaseFaces = 0x0400 # Base | |
| FeatureBasePanels = 0x0800 # Base | |
| FeatureLocations = 0x1000 # Locations | |
| FeatureCoolant = 0x2000 # Coolant | |
| FeatureDiameters = 0x4000 # Turning Diameters | |
| 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) # read-only | |
| 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) # read-only | |
| 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) # read-only | |
| 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) # read-only | |
| obj.addProperty( | |
| "App::PropertyDistance", | |
| "OpStockZMin", | |
| "Op Values", | |
| QT_TRANSLATE_NOOP("App::Property", "Holds the min Z value of Stock"), | |
| ) | |
| obj.setEditorMode("OpStockZMin", 1) # read-only | |
| 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) # read-only | |
| 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) # hide | |
| self.addOpValues(obj, ["start", "final"]) | |
| else: | |
| # StartDepth has become necessary for expressions on other properties | |
| obj.addProperty( | |
| "App::PropertyDistance", | |
| "StartDepth", | |
| "Depth", | |
| QT_TRANSLATE_NOOP( | |
| "App::Property", | |
| "Starting Depth internal use only for derived values", | |
| ), | |
| ) | |
| obj.setEditorMode("StartDepth", 1) # read-only | |
| 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."), | |
| ) | |
| # members being set later | |
| 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 | |
| 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) # read-only | |
| 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.""" | |
| # there's a bit of cycle going on here, if sanitizeBase causes the transaction to | |
| # be cancelled we end right here again with the unsainitized Base - if that is the | |
| # case, stop the cycle and return immediately | |
| 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: # top face | |
| return fbb.ZMin | |
| elif fbb.ZMax > fbb.ZMin and fbb.ZMax == bb.ZMax: # vertical face, full cut | |
| return fbb.ZMin | |
| elif fbb.ZMax > fbb.ZMin and fbb.ZMin > bb.ZMin: # internal vertical wall | |
| return fbb.ZMin | |
| elif fbb.ZMax == fbb.ZMin and fbb.ZMax > bb.ZMin: # face/shelf | |
| 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: | |
| # clearing with stock boundaries | |
| job = PathUtils.findParentJob(obj) | |
| zmax = stockBB.ZMax | |
| zmin = job.Proxy.modelBoundBox(job).ZMax | |
| if FeatureDepths & self.opFeatures(obj): | |
| # first set update final depth, it's value is not negotiable | |
| 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 | |
| # ensure zmax is higher than zmin | |
| if (zmax - 0.0001) <= zmin: | |
| zmax = minZmax(zmin) | |
| # update start depth if requested and required | |
| if not Path.Geom.isRoughly(obj.OpStartDepth.Value, zmax): | |
| obj.OpStartDepth = zmax | |
| else: | |
| # every obj has a StartDepth | |
| 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 | |
| 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 | |
| # make sure Base is still valid or clear it | |
| 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) | |
| # now that all op values are set make sure the user properties get updated accordingly, | |
| # in case they still have an expression referencing any op values | |
| 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)): | |
| # Let's finish by rapid to clearance...just for safety | |
| 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 | |
| ) # Default to perimeter for backward compatibility | |
| self._path_dir = self._calculate_path_dir() | |
| def spindle_dir(self): | |
| return self._spindle_dir | |
| 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() | |
| def cut_side(self): | |
| return self._cut_side | |
| def cut_side(self, value): | |
| self._cut_side = value.capitalize() | |
| self._path_dir = self._calculate_path_dir() | |
| def cut_mode(self): | |
| return self._cut_mode | |
| def cut_mode(self, value): | |
| self._cut_mode = value.capitalize() | |
| self._path_dir = self._calculate_path_dir() | |
| def operation_type(self): | |
| return self._operation_type | |
| def operation_type(self, value): | |
| self._operation_type = value.capitalize() | |
| self._path_dir = self._calculate_path_dir() | |
| def path_dir(self): | |
| return self._path_dir | |
| def _calculate_path_dir(self): | |
| if self.spindle_dir == self.NONE: | |
| return "UNKNOWN" | |
| # For area operations (facing, pocketing), path direction is not applicable | |
| 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 # Default to positive direction | |
| spindle_rotation = self._rotation_from_spindle(self.spindle_dir) | |
| # For area operations, climb/conventional depends on relationship between | |
| # spindle rotation, approach direction, and step direction | |
| if approach_direction in ["X-", "X+"]: | |
| # Stepping in Y direction | |
| if self._cut_mode == self.CLIMB: | |
| # Climb: step direction matches spindle for X- approach | |
| return (approach_direction == "X-") == (spindle_rotation == self.CW) | |
| else: # Conventional | |
| # Conventional: step direction opposite to spindle for X- approach | |
| return (approach_direction == "X-") != (spindle_rotation == self.CW) | |
| else: # Y approach | |
| # Stepping in X direction | |
| if self._cut_mode == self.CLIMB: | |
| # Climb: step direction matches spindle for Y- approach | |
| return (approach_direction == "Y-") == (spindle_rotation == self.CW) | |
| else: # Conventional | |
| # Conventional: step direction opposite to spindle for Y- approach | |
| 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 # Default to forward direction | |
| spindle_rotation = self._rotation_from_spindle(self.spindle_dir) | |
| # Determine base cutting direction for climb/conventional | |
| if approach_direction in ["X-", "X+"]: | |
| # Cutting along Y axis | |
| if self._cut_mode == self.CLIMB: | |
| base_forward = (approach_direction == "X-") == (spindle_rotation == self.CW) | |
| else: # Conventional | |
| base_forward = (approach_direction == "X-") != (spindle_rotation == self.CW) | |
| else: # Y approach | |
| # Cutting along X axis | |
| if self._cut_mode == self.CLIMB: | |
| base_forward = (approach_direction == "Y-") == (spindle_rotation == self.CW) | |
| else: # Conventional | |
| base_forward = (approach_direction == "Y-") != (spindle_rotation == self.CW) | |
| # Apply pattern modifications | |
| if pattern == "zigzag" and pass_index % 2 == 1: | |
| base_forward = not base_forward | |
| elif pattern == "unidirectional": | |
| # Always same direction | |
| 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.", | |
| ) | |
| ) | |
| # Get the cycle time in seconds | |
| seconds = obj.Path.getCycleTime(hFeedrate, vFeedrate, hRapidrate, vRapidrate) | |
| if math.isnan(seconds): | |
| return translate("CAM", "Cycletime Error") | |
| # Convert the cycle time to a HH:MM:SS format | |
| cycleTime = time.strftime("%H:%M:%S", time.gmtime(seconds)) | |
| return cycleTime | |