| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
|
|
| __title__ = "CAM Surface Operation" |
| __author__ = "sliptonic (Brad Collette)" |
| __url__ = "https://www.freecad.org" |
| __doc__ = "Class and implementation of 3D Surface operation." |
| __contributors__ = "russ4262 (Russell Johnson)" |
|
|
| import FreeCAD |
|
|
| translate = FreeCAD.Qt.translate |
|
|
| |
| try: |
| try: |
| import ocl |
| except ImportError: |
| import opencamlib as ocl |
| except ImportError: |
| msg = translate("PathSurface", "This operation requires OpenCamLib to be installed.") |
| FreeCAD.Console.PrintError(msg + "\n") |
| raise ImportError |
| |
| |
|
|
| from PySide.QtCore import QT_TRANSLATE_NOOP |
| import Path |
| import Path.Op.Base as PathOp |
| import Path.Op.SurfaceSupport as PathSurfaceSupport |
| import PathScripts.PathUtils as PathUtils |
| import math |
| import time |
|
|
| |
| from lazy_loader.lazy_loader import LazyLoader |
|
|
| Part = LazyLoader("Part", globals(), "Part") |
|
|
| if FreeCAD.GuiUp: |
| import FreeCADGui |
|
|
|
|
| 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()) |
|
|
| FLOAT_EPSILON = 1e-6 |
|
|
|
|
| class ObjectSurface(PathOp.ObjectOp): |
| """Proxy object for Surfacing operation.""" |
|
|
| def opFeatures(self, obj): |
| """opFeatures(obj) ... return all standard features""" |
| return ( |
| PathOp.FeatureTool |
| | PathOp.FeatureDepths |
| | PathOp.FeatureHeights |
| | PathOp.FeatureStepDown |
| | PathOp.FeatureCoolant |
| | PathOp.FeatureBaseFaces |
| ) |
|
|
| def initOperation(self, obj): |
| """initOperation(obj) ... Initialize the operation by |
| managing property creation and property editor status.""" |
| self.propertiesReady = False |
|
|
| self.initOpProperties(obj) |
|
|
| |
| if Path.Log.getLevel(Path.Log.thisModule()) != 4: |
| obj.setEditorMode("ShowTempObjects", 2) |
|
|
| if not hasattr(obj, "DoNotSetDefaultValues"): |
| self.setEditorProperties(obj) |
|
|
| def initOpProperties(self, obj, warn=False): |
| """initOpProperties(obj) ... create operation specific properties""" |
| self.addNewProps = [] |
|
|
| for prtyp, nm, grp, tt in self.opPropertyDefinitions(): |
| if not hasattr(obj, nm): |
| obj.addProperty(prtyp, nm, grp, tt) |
| self.addNewProps.append(nm) |
|
|
| |
| for n in self.propertyEnumerations(): |
| setattr(obj, n[0], n[1]) |
|
|
| self.propertiesReady = True |
|
|
| def opPropertyDefinitions(self): |
| """opPropertyDefinitions(obj) ... Store operation specific properties""" |
|
|
| return [ |
| ( |
| "App::PropertyBool", |
| "ShowTempObjects", |
| "Debug", |
| QT_TRANSLATE_NOOP( |
| "App::Property", |
| "Show the temporary path construction objects when module is in DEBUG mode.", |
| ), |
| ), |
| ( |
| "App::PropertyDistance", |
| "AngularDeflection", |
| "Mesh Conversion", |
| QT_TRANSLATE_NOOP( |
| "App::Property", |
| "Smaller values yield a finer, more accurate mesh. Smaller values increase processing time a lot.", |
| ), |
| ), |
| ( |
| "App::PropertyDistance", |
| "LinearDeflection", |
| "Mesh Conversion", |
| QT_TRANSLATE_NOOP( |
| "App::Property", |
| "Smaller values yield a finer, more accurate mesh. Smaller values do not increase processing time much.", |
| ), |
| ), |
| ( |
| "App::PropertyFloat", |
| "CutterTilt", |
| "Rotation", |
| QT_TRANSLATE_NOOP("App::Property", "Stop index(angle) for rotational scan"), |
| ), |
| ( |
| "App::PropertyEnumeration", |
| "DropCutterDir", |
| "Rotation", |
| QT_TRANSLATE_NOOP( |
| "App::Property", |
| "Dropcutter lines are created parallel to this axis.", |
| ), |
| ), |
| ( |
| "App::PropertyVectorDistance", |
| "DropCutterExtraOffset", |
| "Rotation", |
| QT_TRANSLATE_NOOP( |
| "App::Property", "Additional offset to the selected bounding box" |
| ), |
| ), |
| ( |
| "App::PropertyEnumeration", |
| "RotationAxis", |
| "Rotation", |
| QT_TRANSLATE_NOOP("App::Property", "The model will be rotated around this axis."), |
| ), |
| ( |
| "App::PropertyFloat", |
| "StartIndex", |
| "Rotation", |
| QT_TRANSLATE_NOOP("App::Property", "Start index(angle) for rotational scan"), |
| ), |
| ( |
| "App::PropertyFloat", |
| "StopIndex", |
| "Rotation", |
| QT_TRANSLATE_NOOP("App::Property", "Stop index(angle) for rotational scan"), |
| ), |
| ( |
| "App::PropertyEnumeration", |
| "ScanType", |
| "Surface", |
| QT_TRANSLATE_NOOP( |
| "App::Property", |
| "Planar: Flat, 3D surface scan. Rotational: 4th-axis rotational scan.", |
| ), |
| ), |
| ( |
| "App::PropertyInteger", |
| "AvoidLastX_Faces", |
| "Selected Geometry Settings", |
| QT_TRANSLATE_NOOP( |
| "App::Property", |
| "Avoid cutting the last 'N' faces in the Base Geometry list of selected faces.", |
| ), |
| ), |
| ( |
| "App::PropertyBool", |
| "AvoidLastX_InternalFeatures", |
| "Selected Geometry Settings", |
| QT_TRANSLATE_NOOP( |
| "App::Property", "Do not cut internal features on avoided faces." |
| ), |
| ), |
| ( |
| "App::PropertyDistance", |
| "BoundaryAdjustment", |
| "Selected Geometry Settings", |
| QT_TRANSLATE_NOOP( |
| "App::Property", |
| "Positive values push the cutter toward, or beyond, the boundary. Negative values retract the cutter away from the boundary.", |
| ), |
| ), |
| ( |
| "App::PropertyBool", |
| "BoundaryEnforcement", |
| "Selected Geometry Settings", |
| QT_TRANSLATE_NOOP( |
| "App::Property", |
| "If true, the cutter will remain inside the boundaries of the model or selected face(s).", |
| ), |
| ), |
| ( |
| "App::PropertyEnumeration", |
| "HandleMultipleFeatures", |
| "Selected Geometry Settings", |
| QT_TRANSLATE_NOOP( |
| "App::Property", |
| "Choose how to process multiple Base Geometry features.", |
| ), |
| ), |
| ( |
| "App::PropertyDistance", |
| "InternalFeaturesAdjustment", |
| "Selected Geometry Settings", |
| QT_TRANSLATE_NOOP( |
| "App::Property", |
| "Positive values push the cutter toward, or into, the feature. Negative values retract the cutter away from the feature.", |
| ), |
| ), |
| ( |
| "App::PropertyBool", |
| "InternalFeaturesCut", |
| "Selected Geometry Settings", |
| QT_TRANSLATE_NOOP( |
| "App::Property", |
| "Cut internal feature areas within a larger selected face.", |
| ), |
| ), |
| ( |
| "App::PropertyEnumeration", |
| "BoundBox", |
| "Clearing Options", |
| QT_TRANSLATE_NOOP( |
| "App::Property", "Select the overall boundary for the operation." |
| ), |
| ), |
| ( |
| "App::PropertyEnumeration", |
| "CutMode", |
| "Clearing Options", |
| QT_TRANSLATE_NOOP( |
| "App::Property", |
| "Set the direction for the cutting tool to engage the material: Climb (ClockWise) or Conventional (CounterClockWise)", |
| ), |
| ), |
| ( |
| "App::PropertyEnumeration", |
| "CutPattern", |
| "Clearing Options", |
| QT_TRANSLATE_NOOP( |
| "App::Property", |
| "Set the geometric clearing pattern to use for the operation.", |
| ), |
| ), |
| ( |
| "App::PropertyFloat", |
| "CutPatternAngle", |
| "Clearing Options", |
| QT_TRANSLATE_NOOP( |
| "App::Property", "The yaw angle used for certain clearing patterns" |
| ), |
| ), |
| ( |
| "App::PropertyBool", |
| "CutPatternReversed", |
| "Clearing Options", |
| QT_TRANSLATE_NOOP( |
| "App::Property", |
| "Reverse the cut order of the stepover paths. For circular cut patterns, begin at the outside and work toward the center.", |
| ), |
| ), |
| ( |
| "App::PropertyDistance", |
| "DepthOffset", |
| "Clearing Options", |
| QT_TRANSLATE_NOOP( |
| "App::Property", |
| "Set the Z-axis depth offset from the target surface.", |
| ), |
| ), |
| ( |
| "App::PropertyEnumeration", |
| "LayerMode", |
| "Clearing Options", |
| QT_TRANSLATE_NOOP( |
| "App::Property", |
| "Complete the operation in a single pass at depth, or multiple passes to final depth.", |
| ), |
| ), |
| ( |
| "App::PropertyVectorDistance", |
| "PatternCenterCustom", |
| "Clearing Options", |
| QT_TRANSLATE_NOOP("App::Property", "Set the start point for the cut pattern."), |
| ), |
| ( |
| "App::PropertyEnumeration", |
| "PatternCenterAt", |
| "Clearing Options", |
| QT_TRANSLATE_NOOP( |
| "App::Property", |
| "Choose location of the center point for starting the cut pattern.", |
| ), |
| ), |
| ( |
| "App::PropertyEnumeration", |
| "ProfileEdges", |
| "Clearing Options", |
| QT_TRANSLATE_NOOP("App::Property", "Profile the edges of the selection."), |
| ), |
| ( |
| "App::PropertyDistance", |
| "SampleInterval", |
| "Clearing Options", |
| QT_TRANSLATE_NOOP( |
| "App::Property", |
| "Set the sampling resolution. Smaller values quickly increase processing time.", |
| ), |
| ), |
| ( |
| "App::PropertyFloat", |
| "StepOver", |
| "Clearing Options", |
| QT_TRANSLATE_NOOP( |
| "App::Property", |
| "Set the stepover percentage, based on the tool's diameter.", |
| ), |
| ), |
| ( |
| "App::PropertyBool", |
| "OptimizeLinearPaths", |
| "Optimization", |
| QT_TRANSLATE_NOOP( |
| "App::Property", |
| "Enable optimization of linear paths (co-linear points). Removes unnecessary co-linear points from G-code output.", |
| ), |
| ), |
| ( |
| "App::PropertyBool", |
| "OptimizeStepOverTransitions", |
| "Optimization", |
| QT_TRANSLATE_NOOP( |
| "App::Property", |
| "Enable separate optimization of transitions between, and breaks within, each step over path.", |
| ), |
| ), |
| ( |
| "App::PropertyBool", |
| "CircularUseG2G3", |
| "Optimization", |
| QT_TRANSLATE_NOOP( |
| "App::Property", |
| "Convert co-planar arcs to G2/G3 G-code commands for `Circular` and `CircularZigZag` cut patterns.", |
| ), |
| ), |
| ( |
| "App::PropertyDistance", |
| "GapThreshold", |
| "Optimization", |
| QT_TRANSLATE_NOOP( |
| "App::Property", |
| "Collinear and co-radial artifact gaps that are smaller than this threshold are closed in the path.", |
| ), |
| ), |
| ( |
| "App::PropertyString", |
| "GapSizes", |
| "Optimization", |
| QT_TRANSLATE_NOOP( |
| "App::Property", |
| "Feedback: three smallest gaps identified in the path geometry.", |
| ), |
| ), |
| ( |
| "App::PropertyVectorDistance", |
| "StartPoint", |
| "Start Point", |
| QT_TRANSLATE_NOOP( |
| "App::Property", |
| "The custom start point for the path of this operation", |
| ), |
| ), |
| ( |
| "App::PropertyBool", |
| "UseStartPoint", |
| "Start Point", |
| QT_TRANSLATE_NOOP("App::Property", "Make True, if specifying a Start Point"), |
| ), |
| ] |
|
|
| @classmethod |
| def propertyEnumerations(cls, dataType="data"): |
| """propertyEnumerations(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 = { |
| "BoundBox": [ |
| (translate("CAM_Surface", "BaseBoundBox"), "BaseBoundBox"), |
| (translate("CAM_Surface", "Stock"), "Stock"), |
| ], |
| "PatternCenterAt": [ |
| (translate("CAM_Surface", "CenterOfMass"), "CenterOfMass"), |
| (translate("CAM_Surface", "CenterOfBoundBox"), "CenterOfBoundBox"), |
| (translate("CAM_Surface", "XminYmin"), "XminYmin"), |
| (translate("CAM_Surface", "Custom"), "Custom"), |
| ], |
| "CutMode": [ |
| (translate("CAM_Surface", "Conventional"), "Conventional"), |
| (translate("CAM_Surface", "Climb"), "Climb"), |
| ], |
| "CutPattern": [ |
| (translate("CAM_Surface", "Circular"), "Circular"), |
| (translate("CAM_Surface", "CircularZigZag"), "CircularZigZag"), |
| (translate("CAM_Surface", "Line"), "Line"), |
| (translate("CAM_Surface", "Offset"), "Offset"), |
| (translate("CAM_Surface", "Spiral"), "Spiral"), |
| (translate("CAM_Surface", "ZigZag"), "ZigZag"), |
| ], |
| "DropCutterDir": [ |
| (translate("CAM_Surface", "X"), "X"), |
| (translate("CAM_Surface", "Y"), "Y"), |
| ], |
| "HandleMultipleFeatures": [ |
| (translate("CAM_Surface", "Collectively"), "Collectively"), |
| (translate("CAM_Surface", "Individually"), "Individually"), |
| ], |
| "LayerMode": [ |
| (translate("CAM_Surface", "Single-pass"), "Single-pass"), |
| (translate("CAM_Surface", "Multi-pass"), "Multi-pass"), |
| ], |
| "ProfileEdges": [ |
| (translate("CAM_Surface", "None"), "None"), |
| (translate("CAM_Surface", "Only"), "Only"), |
| (translate("CAM_Surface", "First"), "First"), |
| (translate("CAM_Surface", "Last"), "Last"), |
| ], |
| "RotationAxis": [ |
| (translate("CAM_Surface", "X"), "X"), |
| (translate("CAM_Surface", "Y"), "Y"), |
| ], |
| "ScanType": [ |
| (translate("CAM_Surface", "Planar"), "Planar"), |
| (translate("CAM_Surface", "Rotational"), "Rotational"), |
| ], |
| } |
|
|
| if dataType == "raw": |
| return enums |
|
|
| data = [] |
| idx = 0 if dataType == "translated" else 1 |
|
|
| for k, v in enumerate(enums): |
| data.append((v, [tup[idx] for tup in enums[v]])) |
|
|
| return data |
|
|
| def opPropertyDefaults(self, obj, job): |
| """opPropertyDefaults(obj, job) ... returns a dictionary of default values |
| for the operation's properties.""" |
| defaults = { |
| "OptimizeLinearPaths": True, |
| "InternalFeaturesCut": True, |
| "OptimizeStepOverTransitions": False, |
| "CircularUseG2G3": False, |
| "BoundaryEnforcement": True, |
| "UseStartPoint": False, |
| "AvoidLastX_InternalFeatures": True, |
| "CutPatternReversed": False, |
| "StartPoint": FreeCAD.Vector(0.0, 0.0, obj.ClearanceHeight.Value), |
| "ProfileEdges": "None", |
| "LayerMode": "Single-pass", |
| "ScanType": "Planar", |
| "RotationAxis": "X", |
| "CutMode": "Conventional", |
| "CutPattern": "Line", |
| "HandleMultipleFeatures": "Collectively", |
| "PatternCenterAt": "CenterOfMass", |
| "GapSizes": "No gaps identified.", |
| "StepOver": 100.0, |
| "CutPatternAngle": 0.0, |
| "CutterTilt": 0.0, |
| "StartIndex": 0.0, |
| "StopIndex": 360.0, |
| "SampleInterval": 1.0, |
| "BoundaryAdjustment": 0.0, |
| "InternalFeaturesAdjustment": 0.0, |
| "AvoidLastX_Faces": 0, |
| "PatternCenterCustom": FreeCAD.Vector(0.0, 0.0, 0.0), |
| "GapThreshold": 0.005, |
| "AngularDeflection": 0.25, |
| |
| "LinearDeflection": 0.001, |
| |
| "ShowTempObjects": False, |
| } |
|
|
| warn = True |
| if hasattr(job, "GeometryTolerance"): |
| if job.GeometryTolerance.Value != 0.0: |
| warn = False |
| |
| |
| |
| |
| defaults["LinearDeflection"] = job.GeometryTolerance.Value / 4 |
| if warn: |
| msg = translate("PathSurface", "The GeometryTolerance for this Job is 0.0.") |
| msg += translate("PathSurface", "Initializing LinearDeflection to 0.001 mm.") |
| FreeCAD.Console.PrintWarning(msg + "\n") |
|
|
| return defaults |
|
|
| def setEditorProperties(self, obj): |
| |
|
|
| P0 = R2 = 0 |
| P2 = R0 = 2 |
| if obj.ScanType == "Planar": |
| |
| if obj.CutPattern in ["Circular", "CircularZigZag", "Spiral"]: |
| P0 = 2 |
| P2 = 0 |
| elif obj.CutPattern == "Offset": |
| P0 = 2 |
| elif obj.ScanType == "Rotational": |
| R2 = P0 = P2 = 2 |
| R0 = 0 |
| obj.setEditorMode("DropCutterDir", R0) |
| obj.setEditorMode("DropCutterExtraOffset", R0) |
| obj.setEditorMode("RotationAxis", R0) |
| obj.setEditorMode("StartIndex", R0) |
| obj.setEditorMode("StopIndex", R0) |
| obj.setEditorMode("CutterTilt", R0) |
| obj.setEditorMode("CutPattern", R2) |
| obj.setEditorMode("CutPatternAngle", P0) |
| obj.setEditorMode("PatternCenterAt", P2) |
| obj.setEditorMode("PatternCenterCustom", P2) |
|
|
| def onChanged(self, obj, prop): |
| if hasattr(self, "propertiesReady"): |
| if self.propertiesReady: |
| if prop in ["ScanType", "CutPattern"]: |
| self.setEditorProperties(obj) |
|
|
| if prop == "Active" and obj.ViewObject: |
| obj.ViewObject.signalChangeIcon() |
|
|
| def opOnDocumentRestored(self, obj): |
| self.propertiesReady = False |
| job = PathUtils.findParentJob(obj) |
|
|
| self.initOpProperties(obj, warn=True) |
| self.opApplyPropertyDefaults(obj, job, self.addNewProps) |
|
|
| mode = 2 if Path.Log.getLevel(Path.Log.thisModule()) != 4 else 0 |
| obj.setEditorMode("ShowTempObjects", mode) |
|
|
| |
| for prop, enums in ObjectSurface.propertyEnumerations(): |
| restore = False |
| if hasattr(obj, prop): |
| val = obj.getPropertyByName(prop) |
| restore = True |
| setattr(obj, prop, enums) |
| if restore: |
| setattr(obj, prop, val) |
|
|
| self.setEditorProperties(obj) |
|
|
| def opApplyPropertyDefaults(self, obj, job, propList): |
| |
| PROP_DFLTS = self.opPropertyDefaults(obj, job) |
| for n in PROP_DFLTS: |
| if n in propList: |
| prop = getattr(obj, n) |
| val = PROP_DFLTS[n] |
| setVal = False |
| if hasattr(prop, "Value"): |
| if isinstance(val, int) or isinstance(val, float): |
| setVal = True |
| if setVal: |
| setattr(prop, "Value", val) |
| else: |
| setattr(obj, n, val) |
|
|
| def opSetDefaultValues(self, obj, job): |
| """opSetDefaultValues(obj, job) ... initialize defaults""" |
| job = PathUtils.findParentJob(obj) |
|
|
| self.opApplyPropertyDefaults(obj, job, self.addNewProps) |
|
|
| |
| d = None |
| if job: |
| if job.Stock: |
| d = PathUtils.guessDepths(job.Stock.Shape, None) |
| Path.Log.debug("job.Stock exists") |
| else: |
| Path.Log.debug("job.Stock NOT exist") |
| else: |
| Path.Log.debug("job NOT exist") |
|
|
| if d is not None: |
| obj.OpFinalDepth.Value = d.final_depth |
| obj.OpStartDepth.Value = d.start_depth |
| else: |
| obj.OpFinalDepth.Value = -10 |
| obj.OpStartDepth.Value = 10 |
|
|
| Path.Log.debug("Default OpFinalDepth: {}".format(obj.OpFinalDepth.Value)) |
| Path.Log.debug("Default OpStartDepth: {}".format(obj.OpStartDepth.Value)) |
|
|
| def opApplyPropertyLimits(self, obj): |
| """opApplyPropertyLimits(obj) ... Apply necessary limits to user input property values before performing main operation.""" |
| |
| if obj.StartIndex < 0.0: |
| obj.StartIndex = 0.0 |
| if obj.StartIndex > 360.0: |
| obj.StartIndex = 360.0 |
|
|
| |
| if obj.StopIndex > 360.0: |
| obj.StopIndex = 360.0 |
| if obj.StopIndex < 0.0: |
| obj.StopIndex = 0.0 |
|
|
| |
| if obj.CutterTilt < -90.0: |
| obj.CutterTilt = -90.0 |
| if obj.CutterTilt > 90.0: |
| obj.CutterTilt = 90.0 |
|
|
| |
| if obj.SampleInterval.Value < 0.0001: |
| obj.SampleInterval.Value = 0.0001 |
| Path.Log.error("Sample interval limits are 0.001 to 25.4 millimeters.") |
|
|
| if obj.SampleInterval.Value > 25.4: |
| obj.SampleInterval.Value = 25.4 |
| Path.Log.error("Sample interval limits are 0.001 to 25.4 millimeters.") |
|
|
| |
| if obj.CutPatternAngle < -360.0: |
| obj.CutPatternAngle = 0.0 |
| Path.Log.error("Cut pattern angle limits are +-360 degrees.") |
|
|
| if obj.CutPatternAngle >= 360.0: |
| obj.CutPatternAngle = 0.0 |
| Path.Log.error("Cut pattern angle limits are +- 360 degrees.") |
|
|
| |
| if obj.StepOver > 100.0: |
| obj.StepOver = 100.0 |
| if obj.StepOver < 1.0: |
| obj.StepOver = 1.0 |
|
|
| |
| if obj.AvoidLastX_Faces < 0: |
| obj.AvoidLastX_Faces = 0 |
| Path.Log.error("AvoidLastX_Faces: Only zero or positive values permitted.") |
|
|
| if obj.AvoidLastX_Faces > 100: |
| obj.AvoidLastX_Faces = 100 |
| Path.Log.error("AvoidLastX_Faces: Avoid last X faces count limited to 100.") |
|
|
| def opUpdateDepths(self, obj): |
| if hasattr(obj, "Base") and obj.Base: |
| base, sublist = obj.Base[0] |
| fbb = base.Shape.getElement(sublist[0]).BoundBox |
| zmin = fbb.ZMax |
| for base, sublist in obj.Base: |
| for sub in sublist: |
| try: |
| fbb = base.Shape.getElement(sub).BoundBox |
| zmin = min(zmin, fbb.ZMin) |
| except Part.OCCError as e: |
| Path.Log.error(e) |
| obj.OpFinalDepth = zmin |
| elif self.job: |
| if hasattr(obj, "BoundBox"): |
| if obj.BoundBox == "BaseBoundBox": |
| models = self.job.Model.Group |
| zmin = models[0].Shape.BoundBox.ZMin |
| for M in models: |
| zmin = min(zmin, M.Shape.BoundBox.ZMin) |
| obj.OpFinalDepth = zmin |
| if obj.BoundBox == "Stock": |
| models = self.job.Stock |
| obj.OpFinalDepth = self.job.Stock.Shape.BoundBox.ZMin |
|
|
| def opExecute(self, obj): |
| """opExecute(obj) ... process surface operation""" |
| Path.Log.track() |
|
|
| self.modelSTLs = [] |
| self.safeSTLs = [] |
| self.modelTypes = [] |
| self.boundBoxes = [] |
| self.profileShapes = [] |
| self.collectiveShapes = [] |
| self.individualShapes = [] |
| self.avoidShapes = [] |
| self.tempGroup = None |
| self.CutClimb = False |
| self.closedGap = False |
| self.tmpCOM = None |
| self.gaps = [0.1, 0.2, 0.3] |
| self.cancelOperation = False |
| CMDS = [] |
| modelVisibility = [] |
| FCAD = FreeCAD.ActiveDocument |
|
|
| try: |
| dotIdx = __name__.index(".") + 1 |
| except Exception: |
| dotIdx = 0 |
| self.module = __name__[dotIdx:] |
|
|
| |
| self.showDebugObjects = False |
| self.showDebugObjects = obj.ShowTempObjects |
| deleteTempsFlag = True |
| if Path.Log.getLevel(Path.Log.thisModule()) == 4: |
| deleteTempsFlag = False |
| else: |
| self.showDebugObjects = False |
|
|
| |
| startTime = time.time() |
|
|
| |
| JOB = PathUtils.findParentJob(obj) |
| self.JOB = JOB |
| if JOB is None: |
| Path.Log.error(translate("PathSurface", "No job")) |
| return |
| self.stockZMin = JOB.Stock.Shape.BoundBox.ZMin |
|
|
| |
| if obj.CutMode == "Climb": |
| self.CutClimb = True |
| if obj.CutPatternReversed: |
| if self.CutClimb: |
| self.CutClimb = False |
| else: |
| self.CutClimb = True |
|
|
| |
| self.resetOpVariables() |
|
|
| |
| oclTool = PathSurfaceSupport.OCL_Tool(ocl, obj) |
| self.cutter = oclTool.getOclTool() |
| if not self.cutter: |
| Path.Log.error( |
| translate( |
| "PathSurface", |
| "Canceling 3D Surface operation. Error creating OCL cutter.", |
| ) |
| ) |
| return |
| self.toolDiam = self.cutter.getDiameter() |
| self.radius = self.toolDiam / 2.0 |
| self.useTiltCutter = oclTool.useTiltCutter() |
| self.cutOut = self.toolDiam * (float(obj.StepOver) / 100.0) |
| self.gaps = [self.toolDiam, self.toolDiam, self.toolDiam] |
|
|
| |
| |
| output = "" |
| if obj.Comment != "": |
| self.commandlist.append(Path.Command("N ({})".format(str(obj.Comment)), {})) |
| self.commandlist.append(Path.Command("N ({})".format(obj.Label), {})) |
| self.commandlist.append(Path.Command("N (Tool type: {})".format(oclTool.toolType), {})) |
| self.commandlist.append( |
| Path.Command("N (Compensated Tool Path. Diameter: {})".format(oclTool.diameter), {}) |
| ) |
| self.commandlist.append( |
| Path.Command("N (Sample interval: {})".format(str(obj.SampleInterval.Value)), {}) |
| ) |
| self.commandlist.append(Path.Command("N (Step over %: {})".format(str(obj.StepOver)), {})) |
| self.commandlist.append(Path.Command("N ({})".format(output), {})) |
| self.commandlist.append( |
| Path.Command("G0", {"Z": obj.ClearanceHeight.Value, "F": self.vertRapid}) |
| ) |
| if obj.UseStartPoint is True: |
| self.commandlist.append( |
| Path.Command( |
| "G0", |
| { |
| "X": obj.StartPoint.x, |
| "Y": obj.StartPoint.y, |
| "F": self.horizRapid, |
| }, |
| ) |
| ) |
|
|
| |
| self.opApplyPropertyLimits(obj) |
|
|
| |
| tempGroupName = "tempPathSurfaceGroup" |
| if FCAD.getObject(tempGroupName): |
| for to in FCAD.getObject(tempGroupName).Group: |
| FCAD.removeObject(to.Name) |
| FCAD.removeObject(tempGroupName) |
| if FCAD.getObject(tempGroupName + "001"): |
| for to in FCAD.getObject(tempGroupName + "001").Group: |
| FCAD.removeObject(to.Name) |
| FCAD.removeObject(tempGroupName + "001") |
| tempGroup = FCAD.addObject("App::DocumentObjectGroup", tempGroupName) |
| tempGroupName = tempGroup.Name |
| self.tempGroup = tempGroup |
| tempGroup.purgeTouched() |
| |
| |
|
|
| |
| self.SafeHeightOffset = JOB.SetupSheet.SafeHeightOffset.Value |
| self.ClearHeightOffset = JOB.SetupSheet.ClearanceHeightOffset.Value |
|
|
| |
| self.depthParams = PathUtils.depth_params( |
| obj.ClearanceHeight.Value, |
| obj.SafeHeight.Value, |
| obj.StartDepth.Value, |
| obj.StepDown.Value, |
| 0.0, |
| obj.FinalDepth.Value, |
| ) |
| self.midDep = (obj.StartDepth.Value + obj.FinalDepth.Value) / 2.0 |
|
|
| |
| if FreeCAD.GuiUp: |
| for model in JOB.Model.Group: |
| mNm = model.Name |
| modelVisibility.append(FreeCADGui.ActiveDocument.getObject(mNm).Visibility) |
|
|
| |
| for model in JOB.Model.Group: |
| self.modelSTLs.append(False) |
| self.safeSTLs.append(False) |
| self.profileShapes.append(False) |
| |
| if obj.BoundBox == "BaseBoundBox": |
| if model.TypeId.startswith("Mesh"): |
| self.modelTypes.append("M") |
| self.boundBoxes.append(model.Mesh.BoundBox) |
| else: |
| self.modelTypes.append("S") |
| self.boundBoxes.append(model.Shape.BoundBox) |
| elif obj.BoundBox == "Stock": |
| self.modelTypes.append("S") |
| self.boundBoxes.append(JOB.Stock.Shape.BoundBox) |
|
|
| |
|
|
| |
| PSF = PathSurfaceSupport.ProcessSelectedFaces(JOB, obj) |
| PSF.setShowDebugObjects(tempGroup, self.showDebugObjects) |
| PSF.radius = self.radius |
| PSF.depthParams = self.depthParams |
| pPM = PSF.preProcessModel(self.module) |
|
|
| |
| if pPM: |
| self.cancelOperation = False |
| (FACES, VOIDS) = pPM |
| self.modelSTLs = PSF.modelSTLs |
| self.profileShapes = PSF.profileShapes |
|
|
| for idx, model in enumerate(JOB.Model.Group): |
| Path.Log.debug(idx) |
| |
| PathSurfaceSupport._prepareModelSTLs(self, JOB, obj, idx, ocl) |
|
|
| if FACES[idx]: |
| Path.Log.debug("Working on Model.Group[{}]: {}".format(idx, model.Label)) |
| if idx > 0: |
| |
| CMDS.append(Path.Command("N (Transition to base: {}.)".format(model.Label))) |
| CMDS.append( |
| Path.Command( |
| "G0", |
| {"Z": obj.ClearanceHeight.Value, "F": self.vertRapid}, |
| ) |
| ) |
| |
| PathSurfaceSupport._makeSafeSTL( |
| self, JOB, obj, idx, FACES[idx], VOIDS[idx], ocl |
| ) |
| |
| CMDS.extend(self._processCutAreas(JOB, obj, idx, FACES[idx], VOIDS[idx])) |
| else: |
| Path.Log.debug("No data for model base: {}".format(model.Label)) |
|
|
| |
| self.commandlist.extend(CMDS) |
| else: |
| Path.Log.error("Failed to pre-process model and/or selected face(s).") |
|
|
| |
|
|
| |
| |
| if FreeCAD.GuiUp: |
| FreeCADGui.ActiveDocument.getObject(tempGroupName).Visibility = False |
| for m in range(0, len(JOB.Model.Group)): |
| M = JOB.Model.Group[m] |
| M.Visibility = modelVisibility[m] |
|
|
| if deleteTempsFlag is True: |
| for to in tempGroup.Group: |
| if hasattr(to, "Group"): |
| for go in to.Group: |
| FCAD.removeObject(go.Name) |
| FCAD.removeObject(to.Name) |
| FCAD.removeObject(tempGroupName) |
| else: |
| if len(tempGroup.Group) == 0: |
| FCAD.removeObject(tempGroupName) |
| else: |
| tempGroup.purgeTouched() |
|
|
| |
| gaps = [] |
| for g in self.gaps: |
| if g != self.toolDiam: |
| gaps.append(g) |
| if len(gaps) > 0: |
| obj.GapSizes = "{} mm".format(gaps) |
| else: |
| if self.closedGap is True: |
| obj.GapSizes = "Closed gaps < Gap Threshold." |
| else: |
| obj.GapSizes = "No gaps identified." |
|
|
| |
| self.resetOpVariables() |
| self.deleteOpVariables() |
|
|
| self.modelSTLs = None |
| self.safeSTLs = None |
| self.modelTypes = None |
| self.boundBoxes = None |
| self.gaps = None |
| self.closedGap = None |
| self.SafeHeightOffset = None |
| self.ClearHeightOffset = None |
| self.depthParams = None |
| self.midDep = None |
| del self.modelSTLs |
| del self.safeSTLs |
| del self.modelTypes |
| del self.boundBoxes |
| del self.gaps |
| del self.closedGap |
| del self.SafeHeightOffset |
| del self.ClearHeightOffset |
| del self.depthParams |
| del self.midDep |
|
|
| execTime = time.time() - startTime |
| if execTime > 60.0: |
| tMins = math.floor(execTime / 60.0) |
| tSecs = execTime - (tMins * 60.0) |
| exTime = str(tMins) + " min. " + str(round(tSecs, 5)) + " sec." |
| else: |
| exTime = str(round(execTime, 5)) + " sec." |
| msg = translate("PathSurface", "operation time is") |
| FreeCAD.Console.PrintMessage("3D Surface " + msg + " {}\n".format(exTime)) |
|
|
| if self.cancelOperation: |
| FreeCAD.ActiveDocument.openTransaction( |
| translate("PathSurface", "Canceled 3D Surface operation.") |
| ) |
| FreeCAD.ActiveDocument.removeObject(obj.Name) |
| FreeCAD.ActiveDocument.commitTransaction() |
|
|
| return True |
|
|
| |
| def _processCutAreas(self, JOB, obj, mdlIdx, FCS, VDS): |
| """_processCutAreas(JOB, obj, mdlIdx, FCS, VDS)... |
| This method applies any avoided faces or regions to the selected faces. |
| It then calls the correct scan method depending on the ScanType property.""" |
| Path.Log.debug("_processCutAreas()") |
|
|
| final = [] |
|
|
| |
| if obj.HandleMultipleFeatures == "Collectively": |
| if FCS is True: |
| COMP = False |
| else: |
| ADD = Part.makeCompound(FCS) |
| if VDS is not False: |
| DEL = Part.makeCompound(VDS) |
| COMP = ADD.cut(DEL) |
| else: |
| COMP = ADD |
|
|
| if obj.ScanType == "Planar": |
| final.extend(self._processPlanarOp(JOB, obj, mdlIdx, COMP, 0)) |
| elif obj.ScanType == "Rotational": |
| final.extend(self._processRotationalOp(JOB, obj, mdlIdx, COMP)) |
|
|
| elif obj.HandleMultipleFeatures == "Individually": |
| for fsi in range(0, len(FCS)): |
| fShp = FCS[fsi] |
| |
| self.resetOpVariables(all=False) |
|
|
| if fShp is True: |
| COMP = False |
| else: |
| ADD = Part.makeCompound([fShp]) |
| if VDS is not False: |
| DEL = Part.makeCompound(VDS) |
| COMP = ADD.cut(DEL) |
| else: |
| COMP = ADD |
|
|
| if obj.ScanType == "Planar": |
| final.extend(self._processPlanarOp(JOB, obj, mdlIdx, COMP, fsi)) |
| elif obj.ScanType == "Rotational": |
| final.extend(self._processRotationalOp(JOB, obj, mdlIdx, COMP)) |
| COMP = None |
| |
|
|
| return final |
|
|
| def _processPlanarOp(self, JOB, obj, mdlIdx, cmpdShp, fsi): |
| """_processPlanarOp(JOB, obj, mdlIdx, cmpdShp)... |
| This method compiles the main components for the procedural portion of a planar operation (non-rotational). |
| It creates the OCL PathDropCutter objects: model and safeTravel. |
| It makes the necessary facial geometries for the actual cut area. |
| It calls the correct Single or Multi-pass method as needed. |
| It returns the gcode for the operation.""" |
| Path.Log.debug("_processPlanarOp()") |
| final = [] |
| SCANDATA = [] |
|
|
| def getTransition(two): |
| first = two[0][0][0] |
| safe = obj.SafeHeight.Value + 0.1 |
| trans = [[FreeCAD.Vector(first.x, first.y, safe)]] |
| return trans |
|
|
| |
| if obj.LayerMode == "Single-pass": |
| depthparams = [obj.FinalDepth.Value] |
| elif obj.LayerMode == "Multi-pass": |
| depthparams = [i for i in self.depthParams] |
| lenDP = len(depthparams) |
|
|
| |
| pdc = self._planarGetPDC( |
| self.modelSTLs[mdlIdx], |
| depthparams[lenDP - 1], |
| obj.SampleInterval.Value, |
| self.cutter, |
| ) |
| safePDC = self._planarGetPDC( |
| self.safeSTLs[mdlIdx], |
| depthparams[lenDP - 1], |
| obj.SampleInterval.Value, |
| self.cutter, |
| ) |
|
|
| profScan = [] |
| if obj.ProfileEdges != "None": |
| prflShp = self.profileShapes[mdlIdx][fsi] |
| if prflShp is False: |
| msg = translate("PathSurface", "No profile geometry shape returned.") |
| Path.Log.error(msg) |
| return [] |
| self.showDebugObject(prflShp, "NewProfileShape") |
| |
| pathOffsetGeom = self._offsetFacesToPointData(obj, prflShp) |
| if pathOffsetGeom is False: |
| msg = translate("PathSurface", "No profile path geometry returned.") |
| Path.Log.error(msg) |
| return [] |
| profScan = [self._planarPerformOclScan(obj, pdc, pathOffsetGeom, True)] |
|
|
| geoScan = [] |
| if obj.ProfileEdges != "Only": |
| self.showDebugObject(cmpdShp, "CutArea") |
| |
| PGG = PathSurfaceSupport.PathGeometryGenerator(obj, cmpdShp, obj.CutPattern) |
| if self.showDebugObjects: |
| PGG.setDebugObjectsGroup(self.tempGroup) |
| self.tmpCOM = PGG.getCenterOfPattern() |
| pathGeom = PGG.generatePathGeometry() |
| if pathGeom is False: |
| msg = translate("PathSurface", "No clearing shape returned.") |
| Path.Log.error(msg) |
| return [] |
| if obj.CutPattern == "Offset": |
| useGeom = self._offsetFacesToPointData(obj, pathGeom, profile=False) |
| if useGeom is False: |
| msg = translate("PathSurface", "No clearing path geometry returned.") |
| Path.Log.error(msg) |
| return [] |
| geoScan = [self._planarPerformOclScan(obj, pdc, useGeom, True)] |
| else: |
| geoScan = self._planarPerformOclScan(obj, pdc, pathGeom, False) |
|
|
| if obj.ProfileEdges == "Only": |
| SCANDATA.extend(profScan) |
| if obj.ProfileEdges == "None": |
| SCANDATA.extend(geoScan) |
| if obj.ProfileEdges == "First": |
| profScan.append(getTransition(geoScan)) |
| SCANDATA.extend(profScan) |
| SCANDATA.extend(geoScan) |
| if obj.ProfileEdges == "Last": |
| SCANDATA.extend(geoScan) |
| SCANDATA.extend(profScan) |
|
|
| if len(SCANDATA) == 0: |
| msg = translate("PathSurface", "No scan data to convert to G-code.") |
| Path.Log.error(msg) |
| return [] |
|
|
| |
| if obj.DepthOffset.Value != 0.0: |
| self._planarApplyDepthOffset(SCANDATA, obj.DepthOffset.Value) |
|
|
| |
| |
| self.preOLP = obj.OptimizeLinearPaths |
| if obj.CutPattern in ["Circular", "CircularZigZag"]: |
| obj.OptimizeLinearPaths = False |
|
|
| |
| if obj.LayerMode == "Single-pass": |
| final.extend(self._planarDropCutSingle(JOB, obj, pdc, safePDC, depthparams, SCANDATA)) |
| elif obj.LayerMode == "Multi-pass": |
| final.extend(self._planarDropCutMulti(JOB, obj, pdc, safePDC, depthparams, SCANDATA)) |
|
|
| |
| if obj.CutPattern in ["Circular", "CircularZigZag"]: |
| obj.OptimizeLinearPaths = self.preOLP |
|
|
| |
| if obj.HandleMultipleFeatures == "Individually": |
| final.insert(0, Path.Command("G0", {"Z": obj.SafeHeight.Value, "F": self.vertRapid})) |
|
|
| return final |
|
|
| def _offsetFacesToPointData(self, obj, subShp, profile=True): |
| Path.Log.debug("_offsetFacesToPointData()") |
|
|
| offsetLists = [] |
| dist = obj.SampleInterval.Value / 5.0 |
| |
|
|
| if not profile: |
| |
| for w in range(len(subShp.Wires) - 1, -1, -1): |
| W = subShp.Wires[w] |
| PNTS = W.discretize(Distance=dist) |
| |
| if self.CutClimb: |
| PNTS.reverse() |
| offsetLists.append(PNTS) |
| else: |
| |
| for fc in subShp.Faces: |
| |
| for w in range(len(fc.Wires) - 1, -1, -1): |
| W = fc.Wires[w] |
| PNTS = W.discretize(Distance=dist) |
| |
| if self.CutClimb: |
| PNTS.reverse() |
| offsetLists.append(PNTS) |
|
|
| return offsetLists |
|
|
| def _planarPerformOclScan(self, obj, pdc, pathGeom, offsetPoints=False): |
| """_planarPerformOclScan(obj, pdc, pathGeom, offsetPoints=False)... |
| Switching function for calling the appropriate path-geometry to OCL points conversion function |
| for the various cut patterns.""" |
| Path.Log.debug("_planarPerformOclScan()") |
| SCANS = [] |
|
|
| if offsetPoints or obj.CutPattern == "Offset": |
| PNTSET = PathSurfaceSupport.pathGeomToOffsetPointSet(obj, pathGeom) |
| for D in PNTSET: |
| stpOvr = [] |
| ofst = [] |
| for I in D: |
| if I == "BRK": |
| stpOvr.append(ofst) |
| stpOvr.append(I) |
| ofst = [] |
| else: |
| |
| (A, B) = I |
| ofst.extend(self._planarDropCutScan(pdc, A, B)) |
| if len(ofst) > 0: |
| stpOvr.append(ofst) |
| SCANS.extend(stpOvr) |
| elif obj.CutPattern in ["Line", "Spiral", "ZigZag"]: |
| stpOvr = [] |
| if obj.CutPattern == "Line": |
| |
| PNTSET = PathSurfaceSupport.pathGeomToLinesPointSet(self, obj, pathGeom) |
| elif obj.CutPattern == "ZigZag": |
| |
| PNTSET = PathSurfaceSupport.pathGeomToZigzagPointSet(self, obj, pathGeom) |
| elif obj.CutPattern == "Spiral": |
| PNTSET = PathSurfaceSupport.pathGeomToSpiralPointSet(obj, pathGeom) |
|
|
| for STEP in PNTSET: |
| for LN in STEP: |
| if LN == "BRK": |
| stpOvr.append(LN) |
| else: |
| |
| (A, B) = LN |
| stpOvr.append(self._planarDropCutScan(pdc, A, B)) |
| SCANS.append(stpOvr) |
| stpOvr = [] |
| elif obj.CutPattern in ["Circular", "CircularZigZag"]: |
| |
| |
| |
| PNTSET = PathSurfaceSupport.pathGeomToCircularPointSet(self, obj, pathGeom) |
|
|
| for so in range(0, len(PNTSET)): |
| stpOvr = [] |
| erFlg = False |
| (aTyp, dirFlg, ARCS) = PNTSET[so] |
|
|
| if dirFlg == 1: |
| cMode = True |
| else: |
| cMode = False |
|
|
| for a in range(0, len(ARCS)): |
| Arc = ARCS[a] |
| if Arc == "BRK": |
| stpOvr.append("BRK") |
| else: |
| scan = self._planarCircularDropCutScan(pdc, Arc, cMode) |
| if scan is False: |
| erFlg = True |
| else: |
| if aTyp == "L": |
| scan.append(FreeCAD.Vector(scan[0].x, scan[0].y, scan[0].z)) |
| stpOvr.append(scan) |
| if erFlg is False: |
| SCANS.append(stpOvr) |
| |
|
|
| return SCANS |
|
|
| def _planarDropCutScan(self, pdc, A, B): |
| (x1, y1) = A |
| (x2, y2) = B |
| path = ocl.Path() |
| p1 = ocl.Point(x1, y1, 0) |
| p2 = ocl.Point(x2, y2, 0) |
| lo = ocl.Line(p1, p2) |
| path.append(lo) |
| pdc.setPath(path) |
| pdc.run() |
| CLP = pdc.getCLPoints() |
| PNTS = [FreeCAD.Vector(p.x, p.y, p.z) for p in CLP] |
| return PNTS |
|
|
| def _planarCircularDropCutScan(self, pdc, Arc, cMode): |
| path = ocl.Path() |
| (sp, ep, cp) = Arc |
|
|
| |
| p1 = ocl.Point(sp[0], sp[1], 0) |
| p2 = ocl.Point(ep[0], ep[1], 0) |
| C = ocl.Point(cp[0], cp[1], 0) |
| ao = ocl.Arc(p1, p2, C, cMode) |
| path.append(ao) |
| pdc.setPath(path) |
| pdc.run() |
| CLP = pdc.getCLPoints() |
|
|
| |
| return [FreeCAD.Vector(p.x, p.y, p.z) for p in CLP] |
|
|
| |
| def _planarDropCutSingle(self, JOB, obj, pdc, safePDC, depthparams, SCANDATA): |
| Path.Log.debug("_planarDropCutSingle()") |
|
|
| GCODE = [Path.Command("N (Beginning of Single-pass layer.)", {})] |
| tolrnc = JOB.GeometryTolerance.Value |
| lenSCANDATA = len(SCANDATA) |
| gDIR = ["G3", "G2"] |
|
|
| if self.CutClimb: |
| gDIR = ["G2", "G3"] |
|
|
| |
| peIdx = lenSCANDATA |
| if obj.ProfileEdges == "Only": |
| peIdx = -1 |
| elif obj.ProfileEdges == "First": |
| peIdx = 0 |
| elif obj.ProfileEdges == "Last": |
| peIdx = lenSCANDATA - 1 |
|
|
| |
| first = SCANDATA[0][0][0] |
| GCODE.append(Path.Command("G0", {"X": first.x, "Y": first.y, "F": self.horizRapid})) |
|
|
| |
| odd = True |
| lstStpEnd = None |
| for so in range(0, lenSCANDATA): |
| cmds = [] |
| PRTS = SCANDATA[so] |
| lenPRTS = len(PRTS) |
| first = PRTS[0][0] |
| last = None |
| cmds.append(Path.Command("N (Begin step {}.)".format(so), {})) |
|
|
| if so > 0: |
| if obj.CutPattern == "CircularZigZag": |
| if odd: |
| odd = False |
| else: |
| odd = True |
| cmds.extend(self._stepTransitionCmds(obj, lstStpEnd, first, safePDC, tolrnc)) |
| |
| |
| if so == peIdx or peIdx == -1: |
| obj.OptimizeLinearPaths = self.preOLP |
|
|
| |
| for i in range(0, lenPRTS): |
| prt = PRTS[i] |
| lenPrt = len(prt) |
| if prt == "BRK": |
| nxtStart = PRTS[i + 1][0] |
| cmds.append(Path.Command("N (Break)", {})) |
| cmds.extend(self._stepTransitionCmds(obj, last, nxtStart, safePDC, tolrnc)) |
| else: |
| cmds.append(Path.Command("N (part {}.)".format(i + 1), {})) |
| last = prt[lenPrt - 1] |
| if so == peIdx or peIdx == -1: |
| cmds.extend(self._planarSinglepassProcess(obj, prt)) |
| elif ( |
| obj.CutPattern in ["Circular", "CircularZigZag"] |
| and obj.CircularUseG2G3 is True |
| and lenPrt > 2 |
| ): |
| (rtnVal, gcode) = self._arcsToG2G3(prt, lenPrt, odd, gDIR, tolrnc) |
| if rtnVal: |
| cmds.extend(gcode) |
| else: |
| cmds.extend(self._planarSinglepassProcess(obj, prt)) |
| else: |
| cmds.extend(self._planarSinglepassProcess(obj, prt)) |
| cmds.append(Path.Command("N (End of step {}.)".format(so), {})) |
| GCODE.extend(cmds) |
| lstStpEnd = last |
|
|
| |
| if so == peIdx or peIdx == -1: |
| if obj.CutPattern in ["Circular", "CircularZigZag"]: |
| obj.OptimizeLinearPaths = False |
| |
|
|
| return GCODE |
|
|
| def _planarSinglepassProcess(self, obj, points): |
| if obj.OptimizeLinearPaths: |
| points = PathUtils.simplify3dLine(points, tolerance=obj.LinearDeflection.Value) |
| |
| commands = [] |
| for pnt in points: |
| commands.append( |
| Path.Command("G1", {"X": pnt.x, "Y": pnt.y, "Z": pnt.z, "F": self.horizFeed}) |
| ) |
| return commands |
|
|
| def _planarDropCutMulti(self, JOB, obj, pdc, safePDC, depthparams, SCANDATA): |
| GCODE = [Path.Command("N (Beginning of Multi-pass layers.)", {})] |
| tolrnc = JOB.GeometryTolerance.Value |
| lenDP = len(depthparams) |
| prevDepth = depthparams[0] |
| lenSCANDATA = len(SCANDATA) |
| gDIR = ["G3", "G2"] |
|
|
| if self.CutClimb: |
| gDIR = ["G2", "G3"] |
|
|
| |
| peIdx = lenSCANDATA |
| if obj.ProfileEdges == "Only": |
| peIdx = -1 |
| elif obj.ProfileEdges == "First": |
| peIdx = 0 |
| elif obj.ProfileEdges == "Last": |
| peIdx = lenSCANDATA - 1 |
|
|
| |
| lastPrvStpLast = None |
| for lyr in range(0, lenDP): |
| odd = True |
| lyrHasCmds = False |
| actvSteps = 0 |
| LYR = [] |
| |
| |
| |
| prvStpLast = None |
| lyrDep = depthparams[lyr] |
| Path.Log.debug("Multi-pass lyrDep: {}".format(round(lyrDep, 4))) |
|
|
| |
| for so in range(0, len(SCANDATA)): |
| SO = SCANDATA[so] |
| lenSO = len(SO) |
|
|
| |
| ADJPRTS = [] |
| LMAX = [] |
| soHasPnts = False |
| brkFlg = False |
| for i in range(0, lenSO): |
| prt = SO[i] |
| lenPrt = len(prt) |
| if prt == "BRK": |
| if brkFlg: |
| ADJPRTS.append(prt) |
| LMAX.append(prt) |
| brkFlg = False |
| else: |
| (PTS, lMax) = self._planarMultipassPreProcess(obj, prt, prevDepth, lyrDep) |
| if len(PTS) > 0: |
| ADJPRTS.append(PTS) |
| soHasPnts = True |
| brkFlg = True |
| LMAX.append(lMax) |
| |
| lenAdjPrts = len(ADJPRTS) |
|
|
| |
| prtsHasCmds = False |
| stepHasCmds = False |
| prtsCmds = [] |
| stpOvrCmds = [] |
| transCmds = [] |
| if soHasPnts is True: |
| first = ADJPRTS[0][0] |
| last = None |
|
|
| |
| if so > 0: |
| |
| if obj.CutPattern == "CircularZigZag": |
| if odd is True: |
| odd = False |
| else: |
| odd = True |
| |
| if prvStpLast is None: |
| prvStpLast = lastPrvStpLast |
| transCmds.extend( |
| self._stepTransitionCmds(obj, prvStpLast, first, safePDC, tolrnc) |
| ) |
|
|
| |
| if so == peIdx or peIdx == -1: |
| obj.OptimizeLinearPaths = self.preOLP |
|
|
| |
| for i in range(0, lenAdjPrts): |
| prt = ADJPRTS[i] |
| lenPrt = len(prt) |
| if prt == "BRK" and prtsHasCmds: |
| if i + 1 < lenAdjPrts: |
| nxtStart = ADJPRTS[i + 1][0] |
| prtsCmds.append(Path.Command("N (--Break)", {})) |
| else: |
| |
| nxtStart = FreeCAD.Vector(last.x, last.y, obj.SafeHeight.Value) |
| prtsCmds.extend( |
| self._stepTransitionCmds(obj, last, nxtStart, safePDC, tolrnc) |
| ) |
| else: |
| segCmds = False |
| prtsCmds.append(Path.Command("N (part {})".format(i + 1), {})) |
| last = prt[lenPrt - 1] |
| if so == peIdx or peIdx == -1: |
| segCmds = self._planarSinglepassProcess(obj, prt) |
| elif ( |
| obj.CutPattern in ["Circular", "CircularZigZag"] |
| and obj.CircularUseG2G3 is True |
| and lenPrt > 2 |
| ): |
| (rtnVal, gcode) = self._arcsToG2G3(prt, lenPrt, odd, gDIR, tolrnc) |
| if rtnVal is True: |
| segCmds = gcode |
| else: |
| segCmds = self._planarSinglepassProcess(obj, prt) |
| else: |
| segCmds = self._planarSinglepassProcess(obj, prt) |
|
|
| if segCmds is not False: |
| prtsCmds.extend(segCmds) |
| prtsHasCmds = True |
| prvStpLast = last |
| |
| |
| |
|
|
| |
| if so == peIdx or peIdx == -1: |
| if obj.CutPattern in ["Circular", "CircularZigZag"]: |
| obj.OptimizeLinearPaths = False |
|
|
| |
| if prtsHasCmds is True: |
| stepHasCmds = True |
| actvSteps += 1 |
| stpOvrCmds.extend(transCmds) |
| stpOvrCmds.append(Path.Command("N (Begin step {}.)".format(so), {})) |
| stpOvrCmds.append( |
| Path.Command("G0", {"X": first.x, "Y": first.y, "F": self.horizRapid}) |
| ) |
| stpOvrCmds.extend(prtsCmds) |
| stpOvrCmds.append(Path.Command("N (End of step {}.)".format(so), {})) |
|
|
| |
| if actvSteps == 1: |
| LYR.append(Path.Command("N (Layer {} begins)".format(lyr), {})) |
| if lyr > 0: |
| LYR.append(Path.Command("N (Layer transition)", {})) |
| LYR.append( |
| Path.Command("G0", {"Z": obj.SafeHeight.Value, "F": self.vertRapid}) |
| ) |
| LYR.append( |
| Path.Command("G0", {"X": first.x, "Y": first.y, "F": self.horizRapid}) |
| ) |
|
|
| if stepHasCmds is True: |
| lyrHasCmds = True |
| LYR.extend(stpOvrCmds) |
| |
|
|
| |
| if lyrHasCmds is True: |
| GCODE.extend(LYR) |
| GCODE.append(Path.Command("N (End of layer {})".format(lyr), {})) |
|
|
| |
| prevDepth = lyrDep |
| |
|
|
| Path.Log.debug("Multi-pass op has {} layers (step downs).".format(lyr + 1)) |
|
|
| return GCODE |
|
|
| def _planarMultipassPreProcess(self, obj, LN, prvDep, layDep): |
| ALL = [] |
| PTS = [] |
| optLinTrans = obj.OptimizeStepOverTransitions |
| safe = math.ceil(obj.SafeHeight.Value) |
|
|
| if optLinTrans is True: |
| for P in LN: |
| ALL.append(P) |
| |
| if P.z <= layDep: |
| PTS.append(FreeCAD.Vector(P.x, P.y, layDep)) |
| elif P.z > prvDep: |
| PTS.append(FreeCAD.Vector(P.x, P.y, safe)) |
| else: |
| PTS.append(FreeCAD.Vector(P.x, P.y, P.z)) |
| |
| else: |
| for P in LN: |
| ALL.append(P) |
| |
| if P.z <= layDep: |
| PTS.append(FreeCAD.Vector(P.x, P.y, layDep)) |
| else: |
| PTS.append(FreeCAD.Vector(P.x, P.y, P.z)) |
| |
|
|
| if optLinTrans is True: |
| |
| popList = [] |
| for i in range(0, len(PTS)): |
| if PTS[i].z == safe: |
| popList.append(i) |
| else: |
| break |
| popList.sort(reverse=True) |
| for p in popList: |
| PTS.pop(p) |
| ALL.pop(p) |
| popList = [] |
| for i in range(len(PTS) - 1, -1, -1): |
| if PTS[i].z == safe: |
| popList.append(i) |
| else: |
| break |
| popList.sort(reverse=True) |
| for p in popList: |
| PTS.pop(p) |
| ALL.pop(p) |
|
|
| |
| lMax = obj.FinalDepth.Value |
| if len(ALL) > 0: |
| lMax = ALL[0].z |
| for P in ALL: |
| if P.z > lMax: |
| lMax = P.z |
|
|
| return (PTS, lMax) |
|
|
| def _planarMultipassProcess(self, obj, PNTS, lMax): |
| output = [] |
| optimize = obj.OptimizeLinearPaths |
| safe = math.ceil(obj.SafeHeight.Value) |
| lenPNTS = len(PNTS) |
| prcs = True |
| onHold = False |
| onLine = False |
| clrScnLn = lMax + 2.0 |
|
|
| |
| nxt = None |
| pnt = PNTS[0] |
| prev = FreeCAD.Vector(-442064564.6, 258539656553.27, 3538553425.847) |
|
|
| |
| PNTS.append(FreeCAD.Vector(-4895747464.6, -25855763553.2, 35865763425)) |
|
|
| |
| for i in range(0, lenPNTS): |
| prcs = True |
| nxt = PNTS[i + 1] |
|
|
| if pnt.z == safe: |
| prcs = False |
| if onHold is False: |
| onHold = True |
| output.append(Path.Command("N (Start hold)", {})) |
| output.append(Path.Command("G0", {"Z": clrScnLn, "F": self.vertRapid})) |
| else: |
| if onHold is True: |
| onHold = False |
| output.append(Path.Command("N (End hold)", {})) |
| output.append( |
| Path.Command("G0", {"X": pnt.x, "Y": pnt.y, "F": self.horizRapid}) |
| ) |
|
|
| |
| if prcs is True: |
| if optimize is True: |
| |
| iPOL = pnt.isOnLineSegment(prev, nxt) |
| if iPOL is True: |
| onLine = True |
| else: |
| onLine = False |
| output.append( |
| Path.Command( |
| "G1", |
| { |
| "X": pnt.x, |
| "Y": pnt.y, |
| "Z": pnt.z, |
| "F": self.horizFeed, |
| }, |
| ) |
| ) |
| else: |
| output.append( |
| Path.Command( |
| "G1", |
| {"X": pnt.x, "Y": pnt.y, "Z": pnt.z, "F": self.horizFeed}, |
| ) |
| ) |
|
|
| |
| if onLine is False: |
| prev = pnt |
| pnt = nxt |
| |
|
|
| PNTS.pop() |
|
|
| return output |
|
|
| def _stepTransitionCmds(self, obj, p1, p2, safePDC, tolrnc): |
| """Generate transition commands / paths between two dropcutter steps or |
| passes, as well as other kinds of breaks. When |
| OptimizeStepOverTransitions is enabled, uses safePDC to safely optimize |
| short (~order of cutter diameter) transitions.""" |
| cmds = [] |
| rtpd = False |
| height = obj.SafeHeight.Value |
| |
| |
| |
| |
| |
| maxXYDistanceSqrd = (self.cutter.getDiameter() * 2) ** 2 |
|
|
| if obj.OptimizeStepOverTransitions: |
| if p1 and p2: |
| |
| xyDistanceSqrd = (p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2 |
| |
| if xyDistanceSqrd <= maxXYDistanceSqrd: |
| |
| (transLine, minZ, maxZ) = self._getTransitionLine(safePDC, p1, p2, obj) |
| |
| |
| |
| zFloor = min(p1.z, p2.z) |
| if abs(minZ - maxZ) < self.cutter.getDiameter(): |
| for pt in transLine[1:-1]: |
| cmds.append( |
| Path.Command( |
| "G1", |
| { |
| "X": pt.x, |
| "Y": pt.y, |
| |
| "Z": max(pt.z, zFloor), |
| "F": self.horizFeed, |
| }, |
| ) |
| ) |
| |
| cmds.append( |
| Path.Command( |
| "G1", |
| {"X": p2.x, "Y": p2.y, "Z": p2.z, "F": self.horizFeed}, |
| ) |
| ) |
| return cmds |
| |
| |
| |
| |
| stepDown = obj.StepDown.Value if hasattr(obj, "StepDown") else 0 |
| rtpd = min(height, p2.z + stepDown + 2) |
| elif not p1: |
| Path.Log.debug("_stepTransitionCmds() p1 is None") |
| elif not p2: |
| Path.Log.debug("_stepTransitionCmds() p2 is None") |
|
|
| |
| if height is not False: |
| cmds.append(Path.Command("G0", {"Z": height, "F": self.vertRapid})) |
| cmds.append(Path.Command("G0", {"X": p2.x, "Y": p2.y, "F": self.horizRapid})) |
| if rtpd is not False: |
| cmds.append(Path.Command("G0", {"Z": rtpd, "F": self.vertRapid})) |
|
|
| return cmds |
|
|
| def _arcsToG2G3(self, LN, numPts, odd, gDIR, tolrnc): |
| cmds = [] |
| strtPnt = LN[0] |
| endPnt = LN[numPts - 1] |
| strtHght = strtPnt.z |
| coPlanar = True |
| isCircle = False |
| gdi = 0 |
| if odd is True: |
| gdi = 1 |
|
|
| |
| if abs(strtPnt.x - endPnt.x) < tolrnc: |
| if abs(strtPnt.y - endPnt.y) < tolrnc: |
| if abs(strtPnt.z - endPnt.z) < tolrnc: |
| isCircle = True |
| isCircle = False |
|
|
| if isCircle is True: |
| |
| |
| |
| |
|
|
| |
| ijk = self.tmpCOM - strtPnt |
| xyz = self.tmpCOM.add(ijk) |
| cmds.append( |
| Path.Command( |
| "G1", |
| { |
| "X": strtPnt.x, |
| "Y": strtPnt.y, |
| "Z": strtPnt.z, |
| "F": self.horizFeed, |
| }, |
| ) |
| ) |
| cmds.append( |
| Path.Command( |
| gDIR[gdi], |
| { |
| "X": xyz.x, |
| "Y": xyz.y, |
| "Z": xyz.z, |
| "I": ijk.x, |
| "J": ijk.y, |
| "K": ijk.z, |
| "F": self.horizFeed, |
| }, |
| ) |
| ) |
| cmds.append( |
| Path.Command("G1", {"X": xyz.x, "Y": xyz.y, "Z": xyz.z, "F": self.horizFeed}) |
| ) |
| ijk = self.tmpCOM - xyz |
| rst = strtPnt |
| cmds.append( |
| Path.Command( |
| gDIR[gdi], |
| { |
| "X": rst.x, |
| "Y": rst.y, |
| "Z": rst.z, |
| "I": ijk.x, |
| "J": ijk.y, |
| "K": ijk.z, |
| "F": self.horizFeed, |
| }, |
| ) |
| ) |
| cmds.append( |
| Path.Command( |
| "G1", |
| { |
| "X": strtPnt.x, |
| "Y": strtPnt.y, |
| "Z": strtPnt.z, |
| "F": self.horizFeed, |
| }, |
| ) |
| ) |
| else: |
| for pt in LN: |
| if abs(pt.z - strtHght) > tolrnc: |
| coPlanar = False |
| break |
| if coPlanar is True: |
| |
| ijk = self.tmpCOM.sub(strtPnt) |
| xyz = endPnt |
| cmds.append( |
| Path.Command( |
| "G1", |
| { |
| "X": strtPnt.x, |
| "Y": strtPnt.y, |
| "Z": strtPnt.z, |
| "F": self.horizFeed, |
| }, |
| ) |
| ) |
| cmds.append( |
| Path.Command( |
| gDIR[gdi], |
| { |
| "X": xyz.x, |
| "Y": xyz.y, |
| "Z": xyz.z, |
| "I": ijk.x, |
| "J": ijk.y, |
| "K": ijk.z, |
| "F": self.horizFeed, |
| }, |
| ) |
| ) |
| cmds.append( |
| Path.Command( |
| "G1", |
| { |
| "X": endPnt.x, |
| "Y": endPnt.y, |
| "Z": endPnt.z, |
| "F": self.horizFeed, |
| }, |
| ) |
| ) |
|
|
| return (coPlanar, cmds) |
|
|
| def _planarApplyDepthOffset(self, SCANDATA, DepthOffset): |
| Path.Log.debug("Applying DepthOffset value: {}".format(DepthOffset)) |
| lenScans = len(SCANDATA) |
| for s in range(0, lenScans): |
| SO = SCANDATA[s] |
| numParts = len(SO) |
| for prt in range(0, numParts): |
| PRT = SO[prt] |
| if PRT != "BRK": |
| numPts = len(PRT) |
| for pt in range(0, numPts): |
| SCANDATA[s][prt][pt].z += DepthOffset |
|
|
| def _planarGetPDC(self, stl, finalDep, SampleInterval, cutter): |
| pdc = ocl.PathDropCutter() |
| pdc.setSTL(stl) |
| pdc.setCutter(cutter) |
| pdc.setZ(finalDep) |
| pdc.setSampling(SampleInterval) |
| return pdc |
|
|
| |
| def _processRotationalOp(self, JOB, obj, mdlIdx, compoundFaces=None): |
| Path.Log.debug("_processRotationalOp(self, JOB, obj, mdlIdx, compoundFaces=None)") |
|
|
| base = JOB.Model.Group[mdlIdx] |
| bb = self.boundBoxes[mdlIdx] |
| stl = self.modelSTLs[mdlIdx] |
|
|
| |
| initIdx = obj.CutterTilt + obj.StartIndex |
| if initIdx != 0.0: |
| self.basePlacement = FreeCAD.ActiveDocument.getObject(base.Name).Placement |
| if obj.RotationAxis == "X": |
| base.Placement = FreeCAD.Placement( |
| FreeCAD.Vector(0.0, 0.0, 0.0), |
| FreeCAD.Rotation(FreeCAD.Vector(1.0, 0.0, 0.0), initIdx), |
| ) |
| else: |
| base.Placement = FreeCAD.Placement( |
| FreeCAD.Vector(0.0, 0.0, 0.0), |
| FreeCAD.Rotation(FreeCAD.Vector(0.0, 1.0, 0.0), initIdx), |
| ) |
|
|
| |
| if self.holdPoint is None: |
| self.holdPoint = FreeCAD.Vector(0.0, 0.0, 0.0) |
| if self.layerEndPnt is None: |
| self.layerEndPnt = FreeCAD.Vector(0.0, 0.0, 0.0) |
|
|
| |
| if obj.FinalDepth.Value == 0.0: |
| zero = obj.SampleInterval.Value |
| self.FinalDepth = zero |
| |
| else: |
| self.FinalDepth = obj.FinalDepth.Value |
|
|
| |
| if math.fabs(bb.ZMin) > math.fabs(bb.ZMax): |
| vlim = bb.ZMin |
| else: |
| vlim = bb.ZMax |
| if obj.RotationAxis == "X": |
| |
| if math.fabs(bb.YMin) > math.fabs(bb.YMax): |
| hlim = bb.YMin |
| else: |
| hlim = bb.YMax |
| else: |
| |
| if math.fabs(bb.XMin) > math.fabs(bb.XMax): |
| hlim = bb.XMin |
| else: |
| hlim = bb.XMax |
|
|
| |
| self.bbRadius = math.sqrt(hlim**2 + vlim**2) |
| self.clearHeight = self.bbRadius + JOB.SetupSheet.ClearanceHeightOffset.Value |
| self.safeHeight = self.bbRadius + JOB.SetupSheet.ClearanceHeightOffset.Value |
|
|
| return self._rotationalDropCutterOp(obj, stl, bb) |
|
|
| def _rotationalDropCutterOp(self, obj, stl, bb): |
| self.resetTolerance = 0.0000001 |
| self.layerEndzMax = 0.0 |
| commands = [] |
| scanLines = [] |
| advances = [] |
| iSTG = [] |
| rSTG = [] |
| rings = [] |
| lCnt = 0 |
| rNum = 0 |
| bbRad = self.bbRadius |
|
|
| def invertAdvances(advances): |
| idxs = [1.1] |
| for adv in advances: |
| idxs.append(-1 * adv) |
| idxs.pop(0) |
| return idxs |
|
|
| def linesToPointRings(scanLines): |
| rngs = [] |
| numPnts = len( |
| scanLines[0] |
| ) |
| for line in scanLines: |
| if len(line) != numPnts: |
| Path.Log.debug("Error: line lengths not equal") |
| return rngs |
|
|
| for num in range(0, numPnts): |
| rngs.append([1.1]) |
| for line in scanLines: |
| rngs[num].append(line[num]) |
| rngs[num].pop(0) |
| return rngs |
|
|
| def indexAdvances(arc, stepDeg): |
| indexes = [0.0] |
| numSteps = int(math.floor(arc / stepDeg)) |
| for ns in range(0, numSteps): |
| indexes.append(stepDeg) |
|
|
| travel = sum(indexes) |
| if arc == 360.0: |
| indexes.insert(0, 0.0) |
| else: |
| indexes.append(arc - travel) |
|
|
| return indexes |
|
|
| |
| if obj.LayerMode == "Single-pass": |
| depthparams = [self.FinalDepth] |
| else: |
| dep_par = PathUtils.depth_params( |
| self.clearHeight, |
| self.safeHeight, |
| self.bbRadius, |
| obj.StepDown.Value, |
| 0.0, |
| self.FinalDepth, |
| ) |
| depthparams = [i for i in dep_par] |
| prevDepth = depthparams[0] |
| lenDP = len(depthparams) |
|
|
| |
| cdeoX = obj.DropCutterExtraOffset.x |
| cdeoY = obj.DropCutterExtraOffset.y |
|
|
| |
| bb.ZMin = -1 * bbRad |
| bb.ZMax = bbRad |
| if obj.RotationAxis == "X": |
| bb.YMin = -1 * bbRad |
| bb.YMax = bbRad |
| ymin = 0.0 |
| ymax = 0.0 |
| xmin = bb.XMin - cdeoX |
| xmax = bb.XMax + cdeoX |
| else: |
| bb.XMin = -1 * bbRad |
| bb.XMax = bbRad |
| ymin = bb.YMin - cdeoY |
| ymax = bb.YMax + cdeoY |
| xmin = 0.0 |
| xmax = 0.0 |
|
|
| |
| begIdx = obj.StartIndex |
| endIdx = obj.StopIndex |
| if endIdx < begIdx: |
| begIdx -= 360.0 |
| arc = endIdx - begIdx |
|
|
| |
| commands.append(Path.Command("G0", {"Z": self.safeHeight, "F": self.vertRapid})) |
|
|
| |
| for layDep in depthparams: |
| t_before = time.time() |
|
|
| |
| layCircum = 2 * math.pi * layDep |
| if lenDP == 1: |
| layCircum = 2 * math.pi * bbRad |
|
|
| |
| self.axialFeed = 360 / layCircum * self.horizFeed |
| self.axialRapid = 360 / layCircum * self.horizRapid |
|
|
| |
| if obj.RotationAxis == obj.DropCutterDir: |
| stepDeg = (self.cutOut / layCircum) * 360.0 |
| else: |
| stepDeg = (obj.SampleInterval.Value / layCircum) * 360.0 |
|
|
| |
| if stepDeg > 120.0: |
| stepDeg = 120.0 |
| advances = indexAdvances(arc, stepDeg) |
|
|
| |
| if obj.RotationAxis == obj.DropCutterDir: |
| sample = obj.SampleInterval.Value |
| else: |
| sample = self.cutOut |
| scanLines = self._indexedDropCutScan( |
| obj, stl, advances, xmin, ymin, xmax, ymax, layDep, sample |
| ) |
|
|
| |
| if arc == 360.0: |
| advances.append(360.0 - sum(advances)) |
| advances.pop(0) |
| zero = scanLines.pop(0) |
| scanLines.append(zero) |
|
|
| |
| if ( |
| obj.RotationAxis == obj.DropCutterDir |
| ): |
| |
| sumAdv = begIdx |
| for sl in range(0, len(scanLines)): |
| sumAdv += advances[sl] |
| |
| iSTG = self._indexedScanToGcode( |
| obj, sl, scanLines[sl], sumAdv, prevDepth, layDep, lenDP |
| ) |
| commands.extend(iSTG) |
|
|
| |
| commands.append( |
| Path.Command("G0", {"Z": self.clearHeight, "F": self.vertRapid}) |
| ) |
| |
| else: |
| if self.CutClimb is False: |
| advances = invertAdvances(advances) |
| advances.reverse() |
| scanLines.reverse() |
|
|
| |
| commands.append(Path.Command("G0", {"Z": self.clearHeight, "F": self.vertRapid})) |
|
|
| |
| rings = linesToPointRings(scanLines) |
| rNum = 0 |
| for rng in rings: |
| rSTG = self._rotationalScanToGcode(obj, rng, rNum, prevDepth, layDep, advances) |
| commands.extend(rSTG) |
| if arc != 360.0: |
| commands.append( |
| Path.Command("G0", {"Z": self.clearHeight, "F": self.vertRapid}) |
| ) |
| rNum += 1 |
| |
|
|
| prevDepth = layDep |
| lCnt += 1 |
| Path.Log.debug( |
| "--Layer " |
| + str(lCnt) |
| + ": " |
| + str(len(advances)) |
| + " OCL scans and gcode in " |
| + str(time.time() - t_before) |
| + " s" |
| ) |
| |
|
|
| return commands |
|
|
| def _indexedDropCutScan(self, obj, stl, advances, xmin, ymin, xmax, ymax, layDep, sample): |
| cutterOfst = 0.0 |
| iCnt = 0 |
| Lines = [] |
| result = None |
|
|
| pdc = ocl.PathDropCutter() |
| pdc.setCutter(self.cutter) |
| pdc.setZ(layDep) |
| pdc.setSampling(sample) |
|
|
| |
| if obj.CutterTilt != 0.0: |
| cutterOfst = layDep * math.sin(math.radians(obj.CutterTilt)) |
| Path.Log.debug("CutterTilt: cutterOfst is " + str(cutterOfst)) |
|
|
| sumAdv = 0.0 |
| for adv in advances: |
| sumAdv += adv |
| if adv > 0.0: |
| |
| radsRot = math.radians(adv) |
| if obj.RotationAxis == "X": |
| stl.rotate(radsRot, 0.0, 0.0) |
| else: |
| stl.rotate(0.0, radsRot, 0.0) |
|
|
| |
| pdc.setSTL(stl) |
|
|
| |
| if obj.RotationAxis == "X": |
| p1 = ocl.Point(xmin, cutterOfst, 0.0) |
| p2 = ocl.Point(xmax, cutterOfst, 0.0) |
| else: |
| p1 = ocl.Point(cutterOfst, ymin, 0.0) |
| p2 = ocl.Point(cutterOfst, ymax, 0.0) |
|
|
| |
| if obj.RotationAxis == obj.DropCutterDir: |
| if obj.CutPattern == "ZigZag": |
| if iCnt % 2 == 0.0: |
| lo = ocl.Line(p1, p2) |
| else: |
| lo = ocl.Line(p2, p1) |
| elif obj.CutPattern == "Line": |
| if self.CutClimb is True: |
| lo = ocl.Line(p2, p1) |
| else: |
| lo = ocl.Line(p1, p2) |
| else: |
| |
| lo = ocl.Line(p1, p2) |
| else: |
| lo = ocl.Line(p1, p2) |
|
|
| path = ocl.Path() |
| path.append(lo) |
| pdc.setPath(path) |
| pdc.run() |
| result = pdc.getCLPoints() |
|
|
| |
| if obj.DepthOffset.Value != 0.0: |
| Lines.append( |
| [FreeCAD.Vector(p.x, p.y, p.z + obj.DepthOffset.Value) for p in result] |
| ) |
| else: |
| Lines.append([FreeCAD.Vector(p.x, p.y, p.z) for p in result]) |
|
|
| iCnt += 1 |
| |
|
|
| |
| reset = -1 * math.radians(sumAdv - self.resetTolerance) |
| if obj.RotationAxis == "X": |
| stl.rotate(reset, 0.0, 0.0) |
| else: |
| stl.rotate(0.0, reset, 0.0) |
| self.resetTolerance = 0.0 |
|
|
| return Lines |
|
|
| def _indexedScanToGcode(self, obj, li, CLP, idxAng, prvDep, layerDepth, numDeps): |
| |
| output = [] |
| optimize = obj.OptimizeLinearPaths |
| holdCount = 0 |
| holdStart = False |
| holdStop = False |
| zMax = prvDep |
| lenCLP = len(CLP) |
| lastCLP = lenCLP - 1 |
| prev = FreeCAD.Vector(0.0, 0.0, 0.0) |
| nxt = FreeCAD.Vector(0.0, 0.0, 0.0) |
|
|
| |
| pnt = CLP[0] |
|
|
| |
| output.append(Path.Command("G0", {"Z": self.clearHeight, "F": self.vertRapid})) |
|
|
| |
| if obj.RotationAxis == "X": |
| output.append(Path.Command("G0", {"A": idxAng, "F": self.axialFeed})) |
| else: |
| output.append(Path.Command("G0", {"B": idxAng, "F": self.axialFeed})) |
|
|
| output.append(Path.Command("G0", {"X": pnt.x, "Y": pnt.y, "F": self.horizRapid})) |
| output.append(Path.Command("G1", {"Z": pnt.z, "F": self.vertFeed})) |
|
|
| for i in range(0, lenCLP): |
| if i < lastCLP: |
| nxt = CLP[i + 1] |
| else: |
| optimize = False |
|
|
| |
| if pnt.z > zMax: |
| zMax = pnt.z |
|
|
| if obj.LayerMode == "Multi-pass": |
| |
| if pnt.z > prvDep and optimize is True: |
| if self.onHold is False: |
| holdStart = True |
| self.onHold = True |
|
|
| if self.onHold is True: |
| if holdStart is True: |
| |
| output.append( |
| Path.Command( |
| "G1", |
| { |
| "X": pnt.x, |
| "Y": pnt.y, |
| "Z": pnt.z, |
| "F": self.horizFeed, |
| }, |
| ) |
| ) |
| |
| self.holdPoint = pnt |
| holdCount += 1 |
| holdStart = False |
|
|
| |
| if pnt.z <= prvDep: |
| holdStop = True |
|
|
| if holdStop is True: |
| |
| zMax += 2.0 |
| for cmd in self.holdStopCmds(obj, zMax, prvDep, pnt, "Hold Stop: in-line"): |
| output.append(cmd) |
| |
| zMax = prvDep |
| holdStop = False |
| self.onHold = False |
| self.holdPoint = FreeCAD.Vector(0.0, 0.0, 0.0) |
|
|
| if self.onHold is False: |
| if not optimize or not pnt.isOnLineSegment(prev, nxt): |
| output.append( |
| Path.Command( |
| "G1", |
| {"X": pnt.x, "Y": pnt.y, "Z": pnt.z, "F": self.horizFeed}, |
| ) |
| ) |
|
|
| |
| prev = pnt |
| pnt = nxt |
| output.append(Path.Command("N (End index angle " + str(round(idxAng, 4)) + ")", {})) |
|
|
| |
| self.layerEndPnt = pnt |
|
|
| return output |
|
|
| def _rotationalScanToGcode(self, obj, RNG, rN, prvDep, layDep, advances): |
| """_rotationalScanToGcode(obj, RNG, rN, prvDep, layDep, advances) ... |
| Convert rotational scan data to gcode path commands.""" |
| output = [] |
| nxtAng = 0 |
| zMax = 0.0 |
| nxt = FreeCAD.Vector(0.0, 0.0, 0.0) |
|
|
| begIdx = obj.StartIndex |
| endIdx = obj.StopIndex |
| if endIdx < begIdx: |
| begIdx -= 360.0 |
|
|
| |
| axisOfRot = "A" |
| if obj.RotationAxis == "Y": |
| axisOfRot = "B" |
|
|
| |
| ang = 0.0 + obj.CutterTilt |
| pnt = RNG[0] |
|
|
| |
| |
| output.append(Path.Command("G1", {"Z": self.clearHeight, "F": self.vertRapid})) |
|
|
| output.append(Path.Command("G0", {axisOfRot: ang, "F": self.axialFeed})) |
| output.append(Path.Command("G1", {"X": pnt.x, "Y": pnt.y, "F": self.axialFeed})) |
| output.append(Path.Command("G1", {"Z": pnt.z, "F": self.axialFeed})) |
|
|
| lenRNG = len(RNG) |
| lastIdx = lenRNG - 1 |
| for i in range(0, lenRNG): |
| if i < lastIdx: |
| nxtAng = ang + advances[i + 1] |
| nxt = RNG[i + 1] |
|
|
| if pnt.z > zMax: |
| zMax = pnt.z |
|
|
| output.append( |
| Path.Command( |
| "G1", |
| { |
| "X": pnt.x, |
| "Y": pnt.y, |
| "Z": pnt.z, |
| axisOfRot: ang, |
| "F": self.axialFeed, |
| }, |
| ) |
| ) |
| pnt = nxt |
| ang = nxtAng |
|
|
| |
| self.layerEndPnt = RNG[0] |
| self.layerEndzMax = zMax |
|
|
| return output |
|
|
| def holdStopCmds(self, obj, zMax, pd, p2, txt): |
| """holdStopCmds(obj, zMax, pd, p2, txt) ... Gcode commands to be executed at beginning of hold.""" |
| cmds = [] |
| msg = "N (" + txt + ")" |
| cmds.append(Path.Command(msg, {})) |
| cmds.append( |
| Path.Command("G0", {"Z": zMax, "F": self.vertRapid}) |
| ) |
| cmds.append( |
| Path.Command("G0", {"X": p2.x, "Y": p2.y, "F": self.horizRapid}) |
| ) |
| if zMax != pd: |
| cmds.append( |
| Path.Command("G0", {"Z": pd, "F": self.vertRapid}) |
| ) |
| cmds.append( |
| Path.Command("G0", {"Z": p2.z, "F": self.vertFeed}) |
| ) |
| return cmds |
|
|
| |
| def resetOpVariables(self, all=True): |
| """resetOpVariables() ... Reset class variables used for instance of operation.""" |
| self.holdPoint = None |
| self.layerEndPnt = None |
| self.onHold = False |
| self.SafeHeightOffset = 2.0 |
| self.ClearHeightOffset = 4.0 |
| self.layerEndzMax = 0.0 |
| self.resetTolerance = 0.0 |
| self.holdPntCnt = 0 |
| self.bbRadius = 0.0 |
| self.axialFeed = 0.0 |
| self.axialRapid = 0.0 |
| self.FinalDepth = 0.0 |
| self.clearHeight = 0.0 |
| self.safeHeight = 0.0 |
| self.faceZMax = -999999999999.0 |
| if all is True: |
| self.cutter = None |
| self.stl = None |
| self.fullSTL = None |
| self.cutOut = 0.0 |
| self.useTiltCutter = False |
| return True |
|
|
| def deleteOpVariables(self, all=True): |
| """deleteOpVariables() ... Reset class variables used for instance of operation.""" |
| del self.holdPoint |
| del self.layerEndPnt |
| del self.onHold |
| del self.SafeHeightOffset |
| del self.ClearHeightOffset |
| del self.layerEndzMax |
| del self.resetTolerance |
| del self.holdPntCnt |
| del self.bbRadius |
| del self.axialFeed |
| del self.axialRapid |
| del self.FinalDepth |
| del self.clearHeight |
| del self.safeHeight |
| del self.faceZMax |
| if all is True: |
| del self.cutter |
| del self.stl |
| del self.fullSTL |
| del self.cutOut |
| del self.radius |
| del self.useTiltCutter |
| return True |
|
|
| def _getTransitionLine(self, pdc, p1, p2, obj): |
| """Use an OCL PathDropCutter to generate a safe transition path between |
| two points in the x/y plane.""" |
| p1xy, p2xy = ((p1.x, p1.y), (p2.x, p2.y)) |
| pdcLine = self._planarDropCutScan(pdc, p1xy, p2xy) |
| if obj.OptimizeLinearPaths: |
| pdcLine = PathUtils.simplify3dLine(pdcLine, tolerance=obj.LinearDeflection.Value) |
| zs = [obj.z for obj in pdcLine] |
| |
| |
| |
| zDelta = p1.z - pdcLine[0].z |
| if zDelta > 0: |
| for p in pdcLine: |
| p.z += zDelta |
| return (pdcLine, min(zs), max(zs)) |
|
|
| def showDebugObject(self, objShape, objName): |
| if self.showDebugObjects: |
| do = FreeCAD.ActiveDocument.addObject("Part::Feature", "tmp_" + objName) |
| do.Shape = objShape |
| do.purgeTouched() |
| self.tempGroup.addObject(do) |
|
|
|
|
| |
|
|
|
|
| def SetupProperties(): |
| """SetupProperties() ... Return list of properties required for operation.""" |
| return [tup[1] for tup in ObjectSurface.opPropertyDefinitions(False)] |
|
|
|
|
| def Create(name, obj=None, parentJob=None): |
| """Create(name) ... Creates and returns a Surface operation.""" |
| if obj is None: |
| obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name) |
| obj.Proxy = ObjectSurface(obj, name, parentJob) |
| return obj |
|
|