| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| |
|
| | __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 |
| |
|