| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | import FreeCAD |
| |
|
| | __title__ = "CAM Waterline Operation" |
| | __author__ = "russ4262 (Russell Johnson), sliptonic (Brad Collette)" |
| | __url__ = "https://www.freecad.org" |
| | __doc__ = "Class and implementation of Waterline operation." |
| | __contributors__ = "" |
| |
|
| | translate = FreeCAD.Qt.translate |
| |
|
| | |
| | try: |
| | try: |
| | import ocl |
| | except ImportError: |
| | import opencamlib as ocl |
| | except ImportError: |
| | msg = translate("path_waterline", "This operation requires OpenCamLib to be installed.") |
| | FreeCAD.Console.PrintError(msg + "\n") |
| | raise ImportError |
| |
|
| | import Path |
| | import Path.Op.Base as PathOp |
| | import Path.Op.SurfaceSupport as PathSurfaceSupport |
| | import PathScripts.PathUtils as PathUtils |
| | import math |
| | import time |
| | from PySide.QtCore import QT_TRANSLATE_NOOP |
| |
|
| | |
| | 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()) |
| |
|
| |
|
| | class ObjectWaterline(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 |
| | ) |
| |
|
| | @classmethod |
| | def propertyEnumerations(self, 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 = { |
| | "Algorithm": [ |
| | (translate("path_waterline", "OCL Dropcutter"), "OCL Dropcutter"), |
| | (translate("path_waterline", "OCL Adaptive"), "OCL Adaptive"), |
| | (translate("path_waterline", "Experimental"), "Experimental"), |
| | ], |
| | "BoundBox": [ |
| | (translate("path_waterline", "BaseBoundBox"), "BaseBoundBox"), |
| | (translate("path_waterline", "Stock"), "Stock"), |
| | ], |
| | "PatternCenterAt": [ |
| | (translate("path_waterline", "CenterOfMass"), "CenterOfMass"), |
| | (translate("path_waterline", "CenterOfBoundBox"), "CenterOfBoundBox"), |
| | (translate("path_waterline", "XminYmin"), "XminYmin"), |
| | (translate("path_waterline", "Custom"), "Custom"), |
| | ], |
| | "ClearLastLayer": [ |
| | (translate("path_waterline", "Off"), "Off"), |
| | (translate("path_waterline", "Circular"), "Circular"), |
| | (translate("path_waterline", "CircularZigZag"), "CircularZigZag"), |
| | (translate("path_waterline", "Line"), "Line"), |
| | (translate("path_waterline", "Offset"), "Offset"), |
| | (translate("path_waterline", "Spiral"), "Spiral"), |
| | (translate("path_waterline", "ZigZag"), "ZigZag"), |
| | ], |
| | "CutMode": [ |
| | (translate("path_waterline", "Conventional"), "Conventional"), |
| | (translate("path_waterline", "Climb"), "Climb"), |
| | ], |
| | "CutPattern": [ |
| | (translate("path_waterline", "None"), "None"), |
| | (translate("path_waterline", "Circular"), "Circular"), |
| | (translate("path_waterline", "CircularZigZag"), "CircularZigZag"), |
| | (translate("path_waterline", "Line"), "Line"), |
| | (translate("path_waterline", "Offset"), "Offset"), |
| | (translate("path_waterline", "Spiral"), "Spiral"), |
| | (translate("path_waterline", "ZigZag"), "ZigZag"), |
| | ], |
| | "HandleMultipleFeatures": [ |
| | (translate("path_waterline", "Collectively"), "Collectively"), |
| | (translate("path_waterline", "Individually"), "Individually"), |
| | ], |
| | "LayerMode": [ |
| | (translate("path_waterline", "Single-pass"), "Single-pass"), |
| | (translate("path_waterline", "Multi-pass"), "Multi-pass"), |
| | ], |
| | } |
| |
|
| | if dataType == "raw": |
| | return enums |
| |
|
| | data = list() |
| | idx = 0 if dataType == "translated" else 1 |
| |
|
| | Path.Log.debug(enums) |
| |
|
| | for k, v in enumerate(enums): |
| | data.append((v, [tup[idx] for tup in enums[v]])) |
| | Path.Log.debug(data) |
| |
|
| | return data |
| |
|
| | def 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 = list() |
| |
|
| | for prtyp, nm, grp, tt in self.opPropertyDefinitions(): |
| | if not hasattr(obj, nm): |
| | obj.addProperty(prtyp, nm, grp, tt) |
| | self.addNewProps.append(nm) |
| |
|
| | |
| | if len(self.addNewProps) > 0: |
| | ENUMS = self.propertyEnumerations() |
| | for n in ENUMS: |
| | if n[0] in self.addNewProps: |
| | setattr(obj, n[0], n[1]) |
| |
|
| | if warn: |
| | newPropMsg = translate("PathWaterline", "New property added to") |
| | newPropMsg += ' "{}": {}'.format(obj.Label, self.addNewProps) + ". " |
| | newPropMsg += translate("PathWaterline", "Check default value(s).") |
| | FreeCAD.Console.PrintWarning(newPropMsg + "\n") |
| |
|
| | self.propertiesReady = True |
| |
|
| | def opPropertyDefinitions(self): |
| | """opPropertyDefinitions() ... return list of tuples containing 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 the 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 the mesh. Smaller values do not increase processing time much.", |
| | ), |
| | ), |
| | ( |
| | "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", |
| | "Algorithm", |
| | "Clearing Options", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "Select the algorithm to use: OCL Dropcutter*, OCL Adaptive or Experimental (Not OCL based).", |
| | ), |
| | ), |
| | ( |
| | "App::PropertyEnumeration", |
| | "BoundBox", |
| | "Clearing Options", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", "Select the overall boundary for the operation." |
| | ), |
| | ), |
| | ( |
| | "App::PropertyEnumeration", |
| | "ClearLastLayer", |
| | "Clearing Options", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "Set to clear last layer in a `Multi-pass` 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::PropertyDistance", |
| | "IgnoreOuterAbove", |
| | "Clearing Options", |
| | QT_TRANSLATE_NOOP("App::Property", "Ignore outer waterlines above this height."), |
| | ), |
| | ( |
| | "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::PropertyDistance", |
| | "SampleInterval", |
| | "Clearing Options", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "Set the sampling resolution. Smaller values quickly increase processing time.", |
| | ), |
| | ), |
| | ( |
| | "App::PropertyDistance", |
| | "MinSampleInterval", |
| | "Clearing Options", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "Set the minimum 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::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"), |
| | ), |
| | ] |
| |
|
| | 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, |
| | "BoundaryEnforcement": True, |
| | "UseStartPoint": False, |
| | "AvoidLastX_InternalFeatures": True, |
| | "CutPatternReversed": False, |
| | "IgnoreOuterAbove": obj.StartDepth.Value + 0.00001, |
| | "StartPoint": FreeCAD.Vector(0.0, 0.0, obj.ClearanceHeight.Value), |
| | "Algorithm": "OCL Dropcutter", |
| | "LayerMode": "Single-pass", |
| | "CutMode": "Conventional", |
| | "CutPattern": "None", |
| | "HandleMultipleFeatures": "Collectively", |
| | "PatternCenterAt": "CenterOfMass", |
| | "GapSizes": "No gaps identified.", |
| | "ClearLastLayer": "Off", |
| | "StepOver": 100.0, |
| | "CutPatternAngle": 0.0, |
| | "DepthOffset": 0.0, |
| | "SampleInterval": 1.0, |
| | "MinSampleInterval": 0.005, |
| | "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.0001, |
| | |
| | "ShowTempObjects": False, |
| | } |
| |
|
| | warn = True |
| | if hasattr(job, "GeometryTolerance"): |
| | if job.GeometryTolerance.Value != 0.0: |
| | warn = False |
| | defaults["LinearDeflection"] = job.GeometryTolerance.Value |
| | if warn: |
| | msg = translate("PathWaterline", "The GeometryTolerance for this Job is 0.0.") |
| | msg += translate("PathWaterline", "Initializing LinearDeflection to 0.0001 mm.") |
| | FreeCAD.Console.PrintWarning(msg + "\n") |
| |
|
| | return defaults |
| |
|
| | def setEditorProperties(self, obj): |
| | |
| | expMode = G = 0 |
| | show = hide = A = B = C = D = 2 |
| |
|
| | obj.setEditorMode("BoundaryEnforcement", hide) |
| | obj.setEditorMode("InternalFeaturesAdjustment", hide) |
| | obj.setEditorMode("InternalFeaturesCut", hide) |
| | obj.setEditorMode("AvoidLastX_Faces", hide) |
| | obj.setEditorMode("AvoidLastX_InternalFeatures", hide) |
| | obj.setEditorMode("BoundaryAdjustment", hide) |
| | obj.setEditorMode("HandleMultipleFeatures", hide) |
| | obj.setEditorMode("OptimizeLinearPaths", hide) |
| | obj.setEditorMode("OptimizeStepOverTransitions", hide) |
| | obj.setEditorMode("GapThreshold", hide) |
| | obj.setEditorMode("GapSizes", hide) |
| |
|
| | if obj.Algorithm == "OCL Dropcutter": |
| | pass |
| | elif obj.Algorithm == "OCL Adaptive": |
| | D = 0 |
| | expMode = 2 |
| | elif obj.Algorithm == "Experimental": |
| | A = B = C = 0 |
| | expMode = G = D = show = hide = 2 |
| |
|
| | cutPattern = obj.CutPattern |
| | if obj.ClearLastLayer != "Off": |
| | cutPattern = obj.ClearLastLayer |
| |
|
| | if cutPattern == "None": |
| | show = hide = A = 2 |
| | elif cutPattern in ["Line", "ZigZag"]: |
| | show = 0 |
| | elif cutPattern in ["Circular", "CircularZigZag"]: |
| | show = 2 |
| | hide = 0 |
| | elif cutPattern == "Spiral": |
| | G = hide = 0 |
| |
|
| | obj.setEditorMode("CutPatternAngle", show) |
| | obj.setEditorMode("PatternCenterAt", hide) |
| | obj.setEditorMode("PatternCenterCustom", hide) |
| | obj.setEditorMode("CutPatternReversed", A) |
| |
|
| | obj.setEditorMode("ClearLastLayer", C) |
| | obj.setEditorMode("StepOver", B) |
| | obj.setEditorMode("IgnoreOuterAbove", B) |
| | obj.setEditorMode("CutPattern", C) |
| | obj.setEditorMode("SampleInterval", G) |
| | obj.setEditorMode("MinSampleInterval", D) |
| | obj.setEditorMode("LinearDeflection", expMode) |
| | obj.setEditorMode("AngularDeflection", expMode) |
| |
|
| | def onChanged(self, obj, prop): |
| | if hasattr(self, "propertiesReady"): |
| | if self.propertiesReady: |
| | if prop in ["Algorithm", "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) |
| |
|
| | |
| |
|
| | ENUMS = self.propertyEnumerations() |
| | for n in ENUMS: |
| | restore = False |
| | if hasattr(obj, n[0]): |
| | val = obj.getPropertyByName(n[0]) |
| | restore = True |
| | setattr(obj, n[0], n[1]) |
| | if restore: |
| | setattr(obj, n[0], 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) |
| | obj.IgnoreOuterAbove = job.Stock.Shape.BoundBox.ZMax + 0.000001 |
| | 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.SampleInterval.Value < 0.0001: |
| | obj.SampleInterval.Value = 0.0001 |
| | Path.Log.error( |
| | translate( |
| | "PathWaterline", |
| | "Sample interval limits are 0.0001 to 25.4 millimeters.", |
| | ) |
| | ) |
| | if obj.SampleInterval.Value > 25.4: |
| | obj.SampleInterval.Value = 25.4 |
| | Path.Log.error( |
| | translate( |
| | "PathWaterline", |
| | "Sample interval limits are 0.0001 to 25.4 millimeters.", |
| | ) |
| | ) |
| |
|
| | |
| | if obj.MinSampleInterval.Value < 0.0001: |
| | obj.MinSampleInterval.Value = 0.0001 |
| | Path.Log.error( |
| | translate( |
| | "PathWaterline", |
| | "Min Sample interval limits are 0.0001 to 25.4 millimeters.", |
| | ) |
| | ) |
| | if obj.MinSampleInterval.Value > 25.4: |
| | obj.MinSampleInterval.Value = 25.4 |
| | Path.Log.error( |
| | translate( |
| | "PathWaterline", |
| | "Min Sample interval limits are 0.0001 to 25.4 millimeters.", |
| | ) |
| | ) |
| |
|
| | |
| | if obj.CutPatternAngle < -360.0: |
| | obj.CutPatternAngle = 0.0 |
| | Path.Log.error( |
| | translate("PathWaterline", "Cut pattern angle limits are +-360 degrees.") |
| | ) |
| | if obj.CutPatternAngle >= 360.0: |
| | obj.CutPatternAngle = 0.0 |
| | Path.Log.error( |
| | translate("PathWaterline", "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( |
| | translate( |
| | "PathWaterline", |
| | "AvoidLastX_Faces: Only zero or positive values permitted.", |
| | ) |
| | ) |
| | if obj.AvoidLastX_Faces > 100: |
| | obj.AvoidLastX_Faces = 100 |
| | Path.Log.error( |
| | translate( |
| | "PathWaterline", |
| | "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 = list() |
| | self.safeSTLs = list() |
| | self.modelTypes = list() |
| | self.boundBoxes = list() |
| | self.profileShapes = list() |
| | self.collectiveShapes = list() |
| | self.individualShapes = list() |
| | self.avoidShapes = list() |
| | self.geoTlrnc = None |
| | self.tempGroup = None |
| | self.CutClimb = False |
| | self.closedGap = False |
| | self.tmpCOM = None |
| | self.gaps = [0.1, 0.2, 0.3] |
| | CMDS = list() |
| | modelVisibility = list() |
| | FCAD = FreeCAD.ActiveDocument |
| |
|
| | try: |
| | dotIdx = __name__.index(".") + 1 |
| | except Exception: |
| | dotIdx = 0 |
| | self.module = __name__[dotIdx:] |
| |
|
| | |
| | self.wpc = Part.makeCircle(2.0) |
| |
|
| | |
| | self.showDebugObjects = False |
| | self.showDebugObjects = obj.ShowTempObjects |
| | deleteTempsFlag = True |
| | if Path.Log.getLevel(Path.Log.thisModule()) == 4: |
| | deleteTempsFlag = False |
| | else: |
| | self.showDebugObjects = False |
| |
|
| | |
| | Path.Log.info("\nBegin Waterline operation...") |
| | startTime = time.time() |
| |
|
| | |
| | JOB = PathUtils.findParentJob(obj) |
| | if JOB is None: |
| | Path.Log.error(translate("PathWaterline", "No JOB")) |
| | return |
| | self.stockZMin = JOB.Stock.Shape.BoundBox.ZMin |
| |
|
| | |
| | if obj.CutMode == "Climb": |
| | self.CutClimb = True |
| | if obj.CutPatternReversed is True: |
| | if self.CutClimb is True: |
| | 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( |
| | "PathWaterline", |
| | "Canceling Waterline operation. Error creating OCL cutter.", |
| | ) |
| | ) |
| | return |
| | self.toolDiam = self.cutter.getDiameter() |
| | self.radius = self.toolDiam / 2.0 |
| | 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: |
| | self.commandlist.append( |
| | Path.Command( |
| | "G0", |
| | { |
| | "X": obj.StartPoint.x, |
| | "Y": obj.StartPoint.y, |
| | "F": self.horizRapid, |
| | }, |
| | ) |
| | ) |
| |
|
| | |
| | self.opApplyPropertyLimits(obj) |
| |
|
| | |
| | tempGroupName = "tempPathWaterlineGroup" |
| | 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 |
| |
|
| | |
| | useDGT = False |
| | try: |
| | self.geoTlrnc = JOB.GeometryTolerance.Value |
| | if self.geoTlrnc == 0.0: |
| | useDGT = True |
| | except AttributeError as ee: |
| | Path.Log.warning( |
| | "{}\nPlease set Job.GeometryTolerance to an acceptable value. Using Path.Preferences.defaultGeometryTolerance().".format( |
| | ee |
| | ) |
| | ) |
| | useDGT = True |
| | if useDGT: |
| | self.geoTlrnc = Path.Preferences.defaultGeometryTolerance() |
| |
|
| | |
| | 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 m in range(0, len(JOB.Model.Group)): |
| | mNm = JOB.Model.Group[m].Name |
| | modelVisibility.append(FreeCADGui.ActiveDocument.getObject(mNm).Visibility) |
| |
|
| | |
| | for m in range(0, len(JOB.Model.Group)): |
| | M = JOB.Model.Group[m] |
| | self.modelSTLs.append(False) |
| | self.safeSTLs.append(False) |
| | self.profileShapes.append(False) |
| | |
| | if obj.BoundBox == "BaseBoundBox": |
| | if M.TypeId.startswith("Mesh"): |
| | self.modelTypes.append("M") |
| | self.boundBoxes.append(M.Mesh.BoundBox) |
| | else: |
| | self.modelTypes.append("S") |
| | self.boundBoxes.append(M.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 is False: |
| | Path.Log.error("Unable to pre-process obj.Base.") |
| | else: |
| | (FACES, VOIDS) = pPM |
| | self.modelSTLs = PSF.modelSTLs |
| | self.profileShapes = PSF.profileShapes |
| |
|
| | for m in range(0, len(JOB.Model.Group)): |
| | |
| | if obj.Algorithm == "OCL Dropcutter" or obj.Algorithm == "OCL Adaptive": |
| | PathSurfaceSupport._prepareModelSTLs(self, JOB, obj, m, ocl) |
| |
|
| | Mdl = JOB.Model.Group[m] |
| | if FACES[m] is False: |
| | Path.Log.error("No data for model base: {}".format(JOB.Model.Group[m].Label)) |
| | else: |
| | if m > 0: |
| | |
| | CMDS.append(Path.Command("N (Transition to base: {}.)".format(Mdl.Label))) |
| | CMDS.append( |
| | Path.Command( |
| | "G0", |
| | {"Z": obj.ClearanceHeight.Value, "F": self.vertRapid}, |
| | ) |
| | ) |
| | Path.Log.info("Working on Model.Group[{}]: {}".format(m, Mdl.Label)) |
| | |
| | if obj.Algorithm == "OCL Dropcutter" or obj.Algorithm == "OCL Adaptive": |
| | PathSurfaceSupport._makeSafeSTL(self, JOB, obj, m, FACES[m], VOIDS[m], ocl) |
| | |
| | CMDS.extend(self._processWaterlineAreas(JOB, obj, m, FACES[m], VOIDS[m])) |
| |
|
| | |
| | self.commandlist.extend(CMDS) |
| |
|
| | |
| |
|
| | |
| | |
| | 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 = list() |
| | 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 |
| | msg = translate("PathWaterline", "operation time is") |
| | Path.Log.info("Waterline " + msg + " {} sec.".format(execTime)) |
| |
|
| | return True |
| |
|
| | |
| | def _processWaterlineAreas(self, JOB, obj, mdlIdx, FCS, VDS): |
| | """_processWaterlineAreas(JOB, obj, mdlIdx, FCS, VDS)... |
| | This method applies any avoided faces or regions to the selected faces. |
| | It then calls the correct method.""" |
| | Path.Log.debug("_processWaterlineAreas()") |
| |
|
| | final = list() |
| |
|
| | |
| | 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 |
| |
|
| | final.append(Path.Command("G0", {"Z": obj.SafeHeight.Value, "F": self.vertRapid})) |
| | if obj.Algorithm == "OCL Dropcutter" or obj.Algorithm == "OCL Adaptive": |
| | final.extend( |
| | self._oclWaterlineOp(JOB, obj, mdlIdx, COMP) |
| | ) |
| | else: |
| | final.extend( |
| | self._experimentalWaterlineOp(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 |
| |
|
| | final.append(Path.Command("G0", {"Z": obj.SafeHeight.Value, "F": self.vertRapid})) |
| | if obj.Algorithm == "OCL Dropcutter" or obj.Algorithm == "OCL Adaptive": |
| | final.extend( |
| | self._oclWaterlineOp(JOB, obj, mdlIdx, COMP) |
| | ) |
| | else: |
| | final.extend( |
| | self._experimentalWaterlineOp(JOB, obj, mdlIdx, COMP) |
| | ) |
| | COMP = None |
| | |
| |
|
| | return final |
| |
|
| | def _getExperimentalWaterlinePaths(self, PNTSET, csHght, cutPattern): |
| | """_getExperimentalWaterlinePaths(PNTSET, csHght, cutPattern)... |
| | Switching function for calling the appropriate path-geometry to OCL points conversion function |
| | for the various cut patterns.""" |
| | Path.Log.debug("_getExperimentalWaterlinePaths()") |
| | SCANS = list() |
| |
|
| | |
| | if cutPattern in ["Line", "Spiral", "ZigZag"]: |
| | stpOvr = list() |
| | for STEP in PNTSET: |
| | for SEG in STEP: |
| | if SEG == "BRK": |
| | stpOvr.append(SEG) |
| | else: |
| | (A, B) = SEG |
| | P1 = FreeCAD.Vector(A[0], A[1], csHght) |
| | P2 = FreeCAD.Vector(B[0], B[1], csHght) |
| | stpOvr.append((P1, P2)) |
| | SCANS.append(stpOvr) |
| | stpOvr = list() |
| | elif cutPattern in ["Circular", "CircularZigZag"]: |
| | |
| | for so in range(0, len(PNTSET)): |
| | stpOvr = list() |
| | 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: |
| | (sp, ep, cp) = Arc |
| | S = FreeCAD.Vector(sp[0], sp[1], csHght) |
| | E = FreeCAD.Vector(ep[0], ep[1], csHght) |
| | C = FreeCAD.Vector(cp[0], cp[1], csHght) |
| | scan = (S, E, C, cMode) |
| | if scan is False: |
| | erFlg = True |
| | else: |
| | stpOvr.append(scan) |
| | if erFlg is False: |
| | SCANS.append(stpOvr) |
| |
|
| | return SCANS |
| |
|
| | |
| | def _stepTransitionCmds(self, obj, cutPattern, lstPnt, first, minSTH, tolrnc): |
| | cmds = list() |
| | rtpd = False |
| | horizGC = "G0" |
| | hSpeed = self.horizRapid |
| | height = obj.SafeHeight.Value |
| |
|
| | if cutPattern in ["Line", "Circular", "Spiral"]: |
| | if obj.OptimizeStepOverTransitions is True: |
| | height = minSTH + 2.0 |
| | elif cutPattern in ["ZigZag", "CircularZigZag"]: |
| | if obj.OptimizeStepOverTransitions is True: |
| | zChng = first.z - lstPnt.z |
| | if abs(zChng) < tolrnc: |
| | if (minSTH - first.z) > tolrnc: |
| | height = minSTH + 2.0 |
| | else: |
| | horizGC = "G1" |
| | height = first.z |
| | elif (minSTH + (2.0 * tolrnc)) >= max(first.z, lstPnt.z): |
| | height = False |
| |
|
| | |
| | if height is not False: |
| | cmds.append(Path.Command("G0", {"Z": height, "F": self.vertRapid})) |
| | cmds.append(Path.Command(horizGC, {"X": first.x, "Y": first.y, "F": hSpeed})) |
| | if rtpd is not False: |
| | cmds.append(Path.Command("G0", {"Z": rtpd, "F": self.vertRapid})) |
| |
|
| | return cmds |
| |
|
| | def _breakCmds(self, obj, cutPattern, lstPnt, first, minSTH, tolrnc): |
| | cmds = list() |
| | rtpd = False |
| | horizGC = "G0" |
| | hSpeed = self.horizRapid |
| | height = obj.SafeHeight.Value |
| |
|
| | if cutPattern in ["Line", "Circular", "Spiral"]: |
| | if obj.OptimizeStepOverTransitions is True: |
| | height = minSTH + 2.0 |
| | elif cutPattern in ["ZigZag", "CircularZigZag"]: |
| | if obj.OptimizeStepOverTransitions is True: |
| | zChng = first.z - lstPnt.z |
| | if abs(zChng) < tolrnc: |
| | if (minSTH - first.z) > tolrnc: |
| | height = minSTH + 2.0 |
| | else: |
| | height = first.z + 2.0 |
| |
|
| | cmds.append(Path.Command("G0", {"Z": height, "F": self.vertRapid})) |
| | cmds.append(Path.Command(horizGC, {"X": first.x, "Y": first.y, "F": hSpeed})) |
| | if rtpd is not False: |
| | cmds.append(Path.Command("G0", {"Z": rtpd, "F": self.vertRapid})) |
| |
|
| | return cmds |
| |
|
| | 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 _oclWaterlineOp(self, JOB, obj, mdlIdx, subShp=None): |
| | """_oclWaterlineOp(obj, base) ... Main waterline function to perform waterline extraction from model.""" |
| | commands = [] |
| |
|
| | base = JOB.Model.Group[mdlIdx] |
| | bb = self.boundBoxes[mdlIdx] |
| | stl = self.modelSTLs[mdlIdx] |
| | depOfst = obj.DepthOffset.Value |
| |
|
| | |
| | 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) |
| |
|
| | smplInt = obj.SampleInterval.Value |
| | minSmplInt = obj.MinSampleInterval.Value |
| | if minSmplInt > smplInt: |
| | minSmplInt = smplInt |
| |
|
| | |
| | if obj.LayerMode == "Single-pass": |
| | depthparams = [obj.FinalDepth.Value] |
| | else: |
| | depthparams = [dp for dp in self.depthParams] |
| | lenDP = len(depthparams) |
| |
|
| | |
| | if obj.Algorithm == "OCL Adaptive": |
| | |
| | BS = JOB.Stock |
| | stock_bb = BS.Shape.BoundBox |
| |
|
| | |
| | s_xmin = stock_bb.XMin |
| | s_xmax = stock_bb.XMax |
| | s_ymin = stock_bb.YMin |
| | s_ymax = stock_bb.YMax |
| |
|
| | |
| | path_min_x = stl.bb.minpt.x - self.radius |
| | path_min_y = stl.bb.minpt.y - self.radius |
| | path_max_x = stl.bb.maxpt.x + self.radius |
| | path_max_y = stl.bb.maxpt.y + self.radius |
| |
|
| | |
| | tol = 0.001 |
| | if ( |
| | (path_min_x < s_xmin - tol) |
| | or (path_min_y < s_ymin - tol) |
| | or (path_max_x > s_xmax + tol) |
| | or (path_max_y > s_ymax + tol) |
| | ): |
| |
|
| | newPropMsg = translate( |
| | "PathWaterline", |
| | "The toolpath has exceeded the stock bounding box limits. Consider using a Boundary Dressup.", |
| | ) |
| | FreeCAD.Console.PrintWarning(newPropMsg + "\n") |
| |
|
| | |
| | scanLines = self._waterlineAdaptiveScan(stl, smplInt, minSmplInt, depthparams, depOfst) |
| |
|
| | |
| | layTime = time.time() |
| | for loop in scanLines: |
| | |
| | cmds = self._loopToGcode(obj, 0.0, loop) |
| | commands.extend(cmds) |
| |
|
| | Path.Log.debug("--Adaptive generation took " + str(time.time() - layTime) + " s") |
| |
|
| | else: |
| | |
| | if subShp is None: |
| | |
| | if obj.BoundBox == "Stock": |
| | BS = JOB.Stock |
| | bb = BS.Shape.BoundBox |
| | elif obj.BoundBox == "BaseBoundBox": |
| | BS = base |
| | bb = BS.Shape.BoundBox |
| |
|
| | xmin = bb.XMin |
| | xmax = bb.XMax |
| | ymin = bb.YMin |
| | ymax = bb.YMax |
| | else: |
| | xmin = subShp.BoundBox.XMin |
| | xmax = subShp.BoundBox.XMax |
| | ymin = subShp.BoundBox.YMin |
| | ymax = subShp.BoundBox.YMax |
| |
|
| | |
| | bbLength = math.fabs(ymax - ymin) |
| | numScanLines = int(math.ceil(bbLength / smplInt) + 1) |
| |
|
| | |
| | fd = depthparams[-1] |
| | oclScan = self._waterlineDropCutScan(stl, smplInt, xmin, xmax, ymin, fd, numScanLines) |
| | oclScan = [FreeCAD.Vector(P.x, P.y, P.z + depOfst) for P in oclScan] |
| |
|
| | |
| | lenOS = len(oclScan) |
| | ptPrLn = int(lenOS / numScanLines) |
| | scanLines = [] |
| | for L in range(0, numScanLines): |
| | scanLines.append([]) |
| | for P in range(0, ptPrLn): |
| | pi = L * ptPrLn + P |
| | scanLines[L].append(oclScan[pi]) |
| |
|
| | |
| | lenSL = len(scanLines) |
| | pntsPerLine = len(scanLines[0]) |
| | msg = "--OCL scan: " + str(lenSL * pntsPerLine) + " points, with " |
| | msg += str(numScanLines) + " lines and " + str(pntsPerLine) + " pts/line" |
| | Path.Log.debug(msg) |
| |
|
| | lyr = 0 |
| | cmds = [] |
| | layTime = time.time() |
| | self.topoMap = [] |
| | for layDep in depthparams: |
| | cmds = self._getWaterline(obj, scanLines, layDep, lyr, lenSL, pntsPerLine) |
| | commands.extend(cmds) |
| | lyr += 1 |
| | Path.Log.debug("--All layer scans combined took " + str(time.time() - layTime) + " s") |
| |
|
| | return commands |
| |
|
| | def _waterlineDropCutScan(self, stl, smplInt, xmin, xmax, ymin, fd, numScanLines): |
| | """_waterlineDropCutScan(stl, smplInt, xmin, xmax, ymin, fd, numScanLines) ... |
| | Perform OCL scan for waterline purpose.""" |
| | pdc = ocl.PathDropCutter() |
| | pdc.setSTL(stl) |
| | pdc.setCutter(self.cutter) |
| | pdc.setZ(fd) |
| | pdc.setSampling(smplInt) |
| |
|
| | |
| | path = ocl.Path() |
| | for nSL in range(0, numScanLines): |
| | yVal = ymin + (nSL * smplInt) |
| | p1 = ocl.Point(xmin, yVal, fd) |
| | p2 = ocl.Point(xmax, yVal, fd) |
| | path.append(ocl.Line(p1, p2)) |
| | |
| | pdc.setPath(path) |
| | pdc.run() |
| |
|
| | |
| | return pdc.getCLPoints() |
| |
|
| | def _waterlineAdaptiveScan(self, stl, smplInt, minSmplInt, zheights, depOfst): |
| | """Perform OCL Adaptive scan for waterline purpose.""" |
| |
|
| | msg = translate( |
| | "Waterline", ": Steps below the model's top Face will be the only ones processed." |
| | ) |
| | Path.Log.info("Waterline " + msg) |
| |
|
| | |
| | awl = ocl.AdaptiveWaterline() |
| | awl.setSTL(stl) |
| | awl.setCutter(self.cutter) |
| | awl.setSampling(smplInt) |
| | awl.setMinSampling(minSmplInt) |
| |
|
| | adapt_loops = [] |
| |
|
| | |
| | for zh in zheights: |
| | awl.setZ(zh) |
| | awl.run() |
| |
|
| | |
| | |
| | temp_loops = awl.getLoops() |
| |
|
| | if not temp_loops: |
| | |
| | newPropMsg = translate("PathWaterline", "Step Down above model. Skipping height : ") |
| | newPropMsg += "{} mm".format(zh) |
| | FreeCAD.Console.PrintWarning(newPropMsg + "\n") |
| | continue |
| |
|
| | |
| | |
| | for loop in temp_loops: |
| | |
| | fc_loop = [FreeCAD.Vector(P.x, P.y, P.z + depOfst) for P in loop] |
| | adapt_loops.append(fc_loop) |
| |
|
| | return adapt_loops |
| |
|
| | def _getWaterline(self, obj, scanLines, layDep, lyr, lenSL, pntsPerLine): |
| | """_getWaterline(obj, scanLines, layDep, lyr, lenSL, pntsPerLine) ... Get waterline.""" |
| | commands = [] |
| | cmds = [] |
| | loopList = [] |
| | self.topoMap = [] |
| | if obj.Algorithm == "OCL Adaptive": |
| | loopList = scanLines |
| | else: |
| | |
| | self.topoMap = self._createTopoMap(scanLines, layDep, lenSL, pntsPerLine) |
| | |
| | self._bufferTopoMap(lenSL, pntsPerLine) |
| | |
| | self._highlightWaterline(4, 9) |
| | |
| | loopList = self._extractWaterlines(obj, scanLines, lyr, layDep) |
| |
|
| | |
| | for loop in loopList: |
| | cmds = self._loopToGcode(obj, layDep, loop) |
| | commands.extend(cmds) |
| | return commands |
| |
|
| | def _createTopoMap(self, scanLines, layDep, lenSL, pntsPerLine): |
| | """_createTopoMap(scanLines, layDep, lenSL, pntsPerLine) ... Create topo map version of OCL scan data.""" |
| | topoMap = [] |
| | for L in range(0, lenSL): |
| | topoMap.append([]) |
| | for P in range(0, pntsPerLine): |
| | if scanLines[L][P].z > layDep: |
| | topoMap[L].append(2) |
| | else: |
| | topoMap[L].append(0) |
| | return topoMap |
| |
|
| | def _bufferTopoMap(self, lenSL, pntsPerLine): |
| | """_bufferTopoMap(lenSL, pntsPerLine) ... Add buffer boarder of zeros to all sides to topoMap data.""" |
| | pre = [0, 0] |
| | post = [0, 0] |
| | for p in range(0, pntsPerLine): |
| | pre.append(0) |
| | post.append(0) |
| | for i in range(0, lenSL): |
| | self.topoMap[i].insert(0, 0) |
| | self.topoMap[i].append(0) |
| | self.topoMap.insert(0, pre) |
| | self.topoMap.append(post) |
| | return True |
| |
|
| | def _highlightWaterline(self, extraMaterial, insCorn): |
| | """_highlightWaterline(extraMaterial, insCorn) ... Highlight the waterline data, separating from extra material.""" |
| | TM = self.topoMap |
| | lastPnt = len(TM[1]) - 1 |
| | lastLn = len(TM) - 1 |
| | highFlag = 0 |
| |
|
| | |
| | for lin in range(1, lastLn): |
| | for pt in range(1, lastPnt): |
| | if TM[lin][pt] == 0: |
| | if TM[lin][pt + 1] == 2: |
| | TM[lin][pt] = 1 |
| | if TM[lin][pt - 1] == 2: |
| | TM[lin][pt] = 1 |
| |
|
| | |
| | for pt in range(1, lastPnt): |
| | for lin in range(1, lastLn): |
| | if TM[lin][pt] == 0: |
| | highFlag = 0 |
| | if TM[lin + 1][pt] == 2: |
| | TM[lin][pt] = 1 |
| | if TM[lin - 1][pt] == 2: |
| | TM[lin][pt] = 1 |
| | elif TM[lin][pt] == 2: |
| | highFlag += 1 |
| | if highFlag == 3: |
| | if TM[lin - 1][pt - 1] < 2 or TM[lin - 1][pt + 1] < 2: |
| | highFlag = 2 |
| | else: |
| | TM[lin - 1][pt] = extraMaterial |
| | highFlag = 2 |
| |
|
| | |
| | for pt in range(1, lastPnt): |
| | for lin in range(1, lastLn): |
| | if TM[lin][pt] == 1: |
| | cont = True |
| | if TM[lin + 1][pt] == 0: |
| | if TM[lin + 1][pt - 1] == 1: |
| | if TM[lin][pt - 1] == 2: |
| | TM[lin + 1][pt] = 1 |
| | cont = False |
| |
|
| | if cont is True and TM[lin + 1][pt + 1] == 1: |
| | if TM[lin][pt + 1] == 2: |
| | TM[lin + 1][pt] = 1 |
| | cont = True |
| |
|
| | if TM[lin - 1][pt] == 0: |
| | if TM[lin - 1][pt - 1] == 1: |
| | if TM[lin][pt - 1] == 2: |
| | TM[lin - 1][pt] = 1 |
| | cont = False |
| |
|
| | if cont is True and TM[lin - 1][pt + 1] == 1: |
| | if TM[lin][pt + 1] == 2: |
| | TM[lin - 1][pt] = 1 |
| |
|
| | |
| | for pt in range(1, lastPnt): |
| | for lin in range(1, lastLn): |
| | if TM[lin][pt] == 1: |
| | if TM[lin][pt + 1] == 1: |
| | if TM[lin - 1][pt + 1] == 1 or TM[lin + 1][pt + 1] == 1: |
| | TM[lin][pt + 1] = insCorn |
| | elif TM[lin][pt - 1] == 1: |
| | if TM[lin - 1][pt - 1] == 1 or TM[lin + 1][pt - 1] == 1: |
| | TM[lin][pt - 1] = insCorn |
| |
|
| | return True |
| |
|
| | def _extractWaterlines(self, obj, oclScan, lyr, layDep): |
| | """_extractWaterlines(obj, oclScan, lyr, layDep) ... Extract water lines from OCL scan data.""" |
| | srch = True |
| | lastPnt = len(self.topoMap[0]) - 1 |
| | lastLn = len(self.topoMap) - 1 |
| | maxSrchs = 5 |
| | srchCnt = 1 |
| | loopList = [] |
| | loop = [] |
| | loopNum = 0 |
| |
|
| | if self.CutClimb is True: |
| | lC = [ |
| | -1, |
| | -1, |
| | -1, |
| | 0, |
| | 1, |
| | 1, |
| | 1, |
| | 0, |
| | -1, |
| | -1, |
| | -1, |
| | 0, |
| | 1, |
| | 1, |
| | 1, |
| | 0, |
| | -1, |
| | -1, |
| | -1, |
| | 0, |
| | 1, |
| | 1, |
| | 1, |
| | 0, |
| | ] |
| | pC = [ |
| | -1, |
| | 0, |
| | 1, |
| | 1, |
| | 1, |
| | 0, |
| | -1, |
| | -1, |
| | -1, |
| | 0, |
| | 1, |
| | 1, |
| | 1, |
| | 0, |
| | -1, |
| | -1, |
| | -1, |
| | 0, |
| | 1, |
| | 1, |
| | 1, |
| | 0, |
| | -1, |
| | -1, |
| | ] |
| | else: |
| | lC = [ |
| | 1, |
| | 1, |
| | 1, |
| | 0, |
| | -1, |
| | -1, |
| | -1, |
| | 0, |
| | 1, |
| | 1, |
| | 1, |
| | 0, |
| | -1, |
| | -1, |
| | -1, |
| | 0, |
| | 1, |
| | 1, |
| | 1, |
| | 0, |
| | -1, |
| | -1, |
| | -1, |
| | 0, |
| | ] |
| | pC = [ |
| | -1, |
| | 0, |
| | 1, |
| | 1, |
| | 1, |
| | 0, |
| | -1, |
| | -1, |
| | -1, |
| | 0, |
| | 1, |
| | 1, |
| | 1, |
| | 0, |
| | -1, |
| | -1, |
| | -1, |
| | 0, |
| | 1, |
| | 1, |
| | 1, |
| | 0, |
| | -1, |
| | -1, |
| | ] |
| |
|
| | while srch is True: |
| | srch = False |
| | if srchCnt > maxSrchs: |
| | Path.Log.debug( |
| | "Max search scans, " |
| | + str(maxSrchs) |
| | + " reached\nPossible incomplete waterline result!" |
| | ) |
| | break |
| | for L in range(1, lastLn): |
| | for P in range(1, lastPnt): |
| | if self.topoMap[L][P] == 1: |
| | |
| | srch = True |
| | loopNum += 1 |
| | loop = self._trackLoop(oclScan, lC, pC, L, P, loopNum) |
| | self.topoMap[L][P] = 0 |
| | loopList.append(loop) |
| | srchCnt += 1 |
| | Path.Log.debug( |
| | "Search count for layer " |
| | + str(lyr) |
| | + " is " |
| | + str(srchCnt) |
| | + ", with " |
| | + str(loopNum) |
| | + " loops." |
| | ) |
| | return loopList |
| |
|
| | def _trackLoop(self, oclScan, lC, pC, L, P, loopNum): |
| | """_trackLoop(oclScan, lC, pC, L, P, loopNum) ... Track the loop direction.""" |
| | loop = [oclScan[L - 1][P - 1]] |
| | cur = [L, P, 1] |
| | prv = [L, P - 1, 1] |
| | nxt = [L, P + 1, 1] |
| | follow = True |
| | ptc = 0 |
| | ptLmt = 200000 |
| | while follow is True: |
| | ptc += 1 |
| | if ptc > ptLmt: |
| | Path.Log.debug( |
| | "Loop number " |
| | + str(loopNum) |
| | + " at [" |
| | + str(nxt[0]) |
| | + ", " |
| | + str(nxt[1]) |
| | + "] pnt count exceeds, " |
| | + str(ptLmt) |
| | + ". Stopped following loop." |
| | ) |
| | break |
| | nxt = self._findNextWlPoint(lC, pC, cur[0], cur[1], prv[0], prv[1]) |
| | loop.append(oclScan[nxt[0] - 1][nxt[1] - 1]) |
| | self.topoMap[nxt[0]][nxt[1]] = nxt[2] |
| | if nxt[0] == L and nxt[1] == P: |
| | follow = False |
| | elif nxt[0] == cur[0] and nxt[1] == cur[1]: |
| | follow = False |
| | prv = cur |
| | cur = nxt |
| | return loop |
| |
|
| | def _findNextWlPoint(self, lC, pC, cl, cp, pl, pp): |
| | """_findNextWlPoint(lC, pC, cl, cp, pl, pp) ... |
| | Find the next waterline point in the point cloud layer provided.""" |
| | dl = cl - pl |
| | dp = cp - pp |
| | num = 0 |
| | i = 3 |
| | s = 0 |
| | mtch = 0 |
| | found = False |
| | while mtch < 8: |
| | if lC[i] == dl: |
| | if pC[i] == dp: |
| | s = i - 3 |
| | found = True |
| | |
| | for y in range(1, mtch): |
| | if lC[i + y] == dl: |
| | if pC[i + y] == dp: |
| | num = 1 |
| | break |
| | break |
| | i += 1 |
| | mtch += 1 |
| | if found is False: |
| | |
| | return [cl, cp, num] |
| |
|
| | for r in range(0, 8): |
| | l = cl + lC[s + r] |
| | p = cp + pC[s + r] |
| | if self.topoMap[l][p] == 1: |
| | return [l, p, num] |
| |
|
| | |
| | return [cl, cp, num] |
| |
|
| | def _loopToGcode(self, obj, layDep, loop): |
| | """_loopToGcode(obj, layDep, loop) ... Convert set of loop points to Gcode.""" |
| | |
| | output = [] |
| |
|
| | |
| | if not loop: |
| | return output |
| |
|
| | nxt = FreeCAD.Vector(0.0, 0.0, 0.0) |
| |
|
| | |
| | if obj.Algorithm == "OCL Adaptive": |
| | if obj.CutMode == "Climb": |
| | |
| | loop.reverse() |
| | pnt = pnt1 = FreeCAD.Vector(loop[0].x, loop[0].y, loop[0].z) |
| | else: |
| | pnt = FreeCAD.Vector(loop[0].x, loop[0].y, layDep) |
| |
|
| | |
| | if self.layerEndPnt.x == 0 and self.layerEndPnt.y == 0: |
| | output.append(Path.Command("G0", {"Z": obj.ClearanceHeight.Value, "F": self.vertRapid})) |
| | else: |
| | output.append(Path.Command("G0", {"Z": obj.SafeHeight.Value, "F": self.vertRapid})) |
| |
|
| | 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})) |
| |
|
| | lenCLP = len(loop) |
| | lastIdx = lenCLP - 1 |
| | |
| | for i in range(0, lenCLP): |
| | if i < lastIdx: |
| | nxt.x = loop[i + 1].x |
| | nxt.y = loop[i + 1].y |
| | if obj.Algorithm == "OCL Adaptive": |
| | nxt.z = loop[i + 1].z |
| | else: |
| | nxt.z = layDep |
| | output.append(Path.Command("G1", {"X": pnt.x, "Y": pnt.y, "F": self.horizFeed})) |
| |
|
| | |
| | pnt = nxt |
| |
|
| | |
| | if obj.Algorithm == "OCL Adaptive": |
| | output.append(Path.Command("G1", {"X": pnt1.x, "Y": pnt1.y, "F": self.horizFeed})) |
| |
|
| | |
| | self.layerEndPnt = pnt |
| |
|
| | return output |
| |
|
| | |
| | def _experimentalWaterlineOp(self, JOB, obj, mdlIdx, subShp=None): |
| | """_waterlineOp(JOB, obj, mdlIdx, subShp=None) ... |
| | Main waterline function to perform waterline extraction from model.""" |
| | Path.Log.debug("_experimentalWaterlineOp()") |
| |
|
| | commands = [] |
| | base = JOB.Model.Group[mdlIdx] |
| | |
| | self.endVector = None |
| |
|
| | finDep = obj.FinalDepth.Value + (self.geoTlrnc / 10.0) |
| | depthParams = PathUtils.depth_params( |
| | obj.ClearanceHeight.Value, |
| | obj.SafeHeight.Value, |
| | obj.StartDepth.Value, |
| | obj.StepDown.Value, |
| | 0.0, |
| | finDep, |
| | ) |
| |
|
| | |
| | if obj.LayerMode == "Single-pass": |
| | depthparams = [finDep] |
| | else: |
| | depthparams = [dp for dp in depthParams] |
| | Path.Log.debug("Experimental Waterline depthparams:\n{}".format(depthparams)) |
| |
|
| | |
| | |
| |
|
| | buffer = self.cutter.getDiameter() * 10.0 |
| | borderFace = Part.Face(self._makeExtendedBoundBox(JOB.Stock.Shape.BoundBox, buffer, 0.0)) |
| |
|
| | |
| | if obj.BoundBox == "Stock": |
| | stockEnv = PathSurfaceSupport.getShapeEnvelope(JOB.Stock.Shape) |
| | bbFace = PathSurfaceSupport.getCrossSection(stockEnv) |
| | elif obj.BoundBox == "BaseBoundBox": |
| | baseEnv = PathSurfaceSupport.getShapeEnvelope(base.Shape) |
| | bbFace = PathSurfaceSupport.getCrossSection(baseEnv) |
| |
|
| | trimFace = borderFace.cut(bbFace) |
| | self.showDebugObject(trimFace, "TrimFace") |
| |
|
| | |
| | CUTAREAS = self._getCutAreas(base.Shape, depthparams, bbFace, trimFace, borderFace) |
| | if not CUTAREAS: |
| | Path.Log.error("No cross-section cut areas identified.") |
| | return commands |
| |
|
| | caCnt = 0 |
| | ofst = obj.BoundaryAdjustment.Value |
| | ofst -= self.radius |
| | caLen = len(CUTAREAS) |
| | lastCA = caLen - 1 |
| | lastClearArea = None |
| | lastCsHght = None |
| | clearLastLayer = True |
| | for ca in range(0, caLen): |
| | area = CUTAREAS[ca] |
| | csHght = area.BoundBox.ZMin |
| | csHght += obj.DepthOffset.Value |
| | cont = False |
| | caCnt += 1 |
| | if area.Area > 0.0: |
| | cont = True |
| | self.showDebugObject(area, "CutArea_{}".format(caCnt)) |
| | else: |
| | data = FreeCAD.Units.Quantity(csHght, FreeCAD.Units.Length).UserString |
| | Path.Log.debug("Cut area at {} is zero.".format(data)) |
| |
|
| | |
| | if cont: |
| | area.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - area.BoundBox.ZMin)) |
| | activeArea = area.cut(trimFace) |
| | self.showDebugObject(activeArea, "ActiveArea_{}".format(caCnt)) |
| | ofstArea = PathUtils.getOffsetArea(activeArea, ofst, self.wpc) |
| | if not ofstArea: |
| | data = FreeCAD.Units.Quantity(csHght, FreeCAD.Units.Length).UserString |
| | Path.Log.debug("No offset area returned for cut area depth at {}.".format(data)) |
| | cont = False |
| |
|
| | if cont: |
| | |
| | if obj.CutPattern == "Offset" or obj.CutPattern == "None": |
| | ofstSolidFacesList = self._getSolidAreasFromPlanarFaces(ofstArea.Faces) |
| | if ofstSolidFacesList: |
| | clearArea = Part.makeCompound(ofstSolidFacesList) |
| | self.showDebugObject(clearArea, "ClearArea_{}".format(caCnt)) |
| | else: |
| | cont = False |
| | data = FreeCAD.Units.Quantity(csHght, FreeCAD.Units.Length).UserString |
| | Path.Log.error("Could not determine solid faces at {}.".format(data)) |
| | else: |
| | clearArea = activeArea |
| |
|
| | if cont: |
| | data = FreeCAD.Units.Quantity(csHght, FreeCAD.Units.Length).UserString |
| | Path.Log.debug("... Clearning area at {}.".format(data)) |
| | |
| | commands.extend(self._wiresToWaterlinePath(obj, clearArea, csHght)) |
| | clearArea.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - clearArea.BoundBox.ZMin)) |
| | lastClearArea = clearArea |
| | lastCsHght = csHght |
| |
|
| | |
| | (clrLyr, clearLastLayer) = self._clearLayer(obj, ca, lastCA, clearLastLayer) |
| | if clrLyr == "Offset": |
| | commands.extend(self._makeOffsetLayerPaths(obj, clearArea, csHght)) |
| | elif clrLyr: |
| | cutPattern = obj.CutPattern |
| | if clearLastLayer is False: |
| | cutPattern = obj.ClearLastLayer |
| | commands.extend( |
| | self._makeCutPatternLayerPaths(JOB, obj, clearArea, csHght, cutPattern) |
| | ) |
| | |
| |
|
| | if clearLastLayer and obj.ClearLastLayer != "Off": |
| | Path.Log.debug("... Clearning last layer") |
| | (clrLyr, cLL) = self._clearLayer(obj, 1, 1, False) |
| | lastClearArea.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - lastClearArea.BoundBox.ZMin)) |
| | if clrLyr == "Offset": |
| | commands.extend(self._makeOffsetLayerPaths(obj, lastClearArea, lastCsHght)) |
| | elif clrLyr: |
| | commands.extend( |
| | self._makeCutPatternLayerPaths( |
| | JOB, obj, lastClearArea, lastCsHght, obj.ClearLastLayer |
| | ) |
| | ) |
| |
|
| | return commands |
| |
|
| | def _getCutAreas(self, shape, depthparams, bbFace, trimFace, borderFace): |
| | """_getCutAreas(JOB, shape, depthparams, bbFace, borderFace) ... |
| | Takes shape, depthparams and base-envelope-cross-section, and |
| | returns a list of cut areas - one for each depth.""" |
| | Path.Log.debug("_getCutAreas()") |
| |
|
| | CUTAREAS = list() |
| | isFirst = True |
| | lenDP = len(depthparams) |
| |
|
| | |
| | for dp in range(0, lenDP): |
| | csHght = depthparams[dp] |
| | |
| |
|
| | |
| | csFaces = self._getModelCrossSection(shape, csHght) |
| | if csFaces: |
| | if len(csFaces) > 0: |
| | useFaces = self._getSolidAreasFromPlanarFaces(csFaces) |
| | else: |
| | useFaces = False |
| |
|
| | if useFaces: |
| | compAdjFaces = Part.makeCompound(useFaces) |
| | self.showDebugObject(compAdjFaces, "Solids_{}".format(dp + 1)) |
| | if isFirst: |
| | allPrevComp = compAdjFaces |
| | cutArea = borderFace.cut(compAdjFaces) |
| | else: |
| | preCutArea = borderFace.cut(compAdjFaces) |
| | cutArea = preCutArea.cut( |
| | allPrevComp |
| | ) |
| | allPrevComp = allPrevComp.fuse(compAdjFaces) |
| | cutArea.translate(FreeCAD.Vector(0.0, 0.0, csHght - cutArea.BoundBox.ZMin)) |
| | CUTAREAS.append(cutArea) |
| | isFirst = False |
| | else: |
| | Path.Log.error("No waterline at depth: {} mm.".format(csHght)) |
| | |
| |
|
| | if len(CUTAREAS) > 0: |
| | return CUTAREAS |
| |
|
| | return False |
| |
|
| | def _wiresToWaterlinePath(self, obj, ofstPlnrShp, csHght): |
| | Path.Log.debug("_wiresToWaterlinePath()") |
| | commands = list() |
| |
|
| | |
| | ofstPlnrShp.translate(FreeCAD.Vector(0.0, 0.0, csHght - ofstPlnrShp.BoundBox.ZMin)) |
| | self.showDebugObject(ofstPlnrShp, "WaterlinePathArea_{}".format(round(csHght, 2))) |
| |
|
| | commands.append(Path.Command("N (Cut Area {}.)".format(round(csHght, 2)))) |
| | start = 1 |
| | if csHght < obj.IgnoreOuterAbove: |
| | start = 0 |
| | for w in range(start, len(ofstPlnrShp.Wires)): |
| | wire = ofstPlnrShp.Wires[w] |
| | V = wire.Vertexes |
| | if obj.CutMode == "Climb": |
| | lv = len(V) - 1 |
| | startVect = FreeCAD.Vector(V[lv].X, V[lv].Y, V[lv].Z) |
| | else: |
| | startVect = FreeCAD.Vector(V[0].X, V[0].Y, V[0].Z) |
| |
|
| | commands.append(Path.Command("N (Wire {}.)".format(w))) |
| |
|
| | |
| | |
| | commands.append( |
| | Path.Command("G0", {"X": startVect.x, "Y": startVect.y, "F": self.horizRapid}) |
| | ) |
| | (cmds, endVect) = self._wireToPath(obj, wire, startVect) |
| | commands.extend(cmds) |
| | commands.append(Path.Command("G0", {"Z": obj.SafeHeight.Value, "F": self.vertRapid})) |
| |
|
| | return commands |
| |
|
| | def _makeCutPatternLayerPaths(self, JOB, obj, clrAreaShp, csHght, cutPattern): |
| | Path.Log.debug("_makeCutPatternLayerPaths()") |
| | commands = [] |
| |
|
| | clrAreaShp.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - clrAreaShp.BoundBox.ZMin)) |
| |
|
| | |
| | if cutPattern == "Offset": |
| | commands.extend(self._makeOffsetLayerPaths(obj, clrAreaShp, csHght)) |
| | else: |
| | |
| | PGG = PathSurfaceSupport.PathGeometryGenerator(obj, clrAreaShp, cutPattern) |
| | if self.showDebugObjects: |
| | PGG.setDebugObjectsGroup(self.tempGroup) |
| | self.tmpCOM = PGG.getCenterOfPattern() |
| | pathGeom = PGG.generatePathGeometry() |
| | if not pathGeom: |
| | Path.Log.warning("No path geometry generated.") |
| | return commands |
| | pathGeom.translate(FreeCAD.Vector(0.0, 0.0, csHght - pathGeom.BoundBox.ZMin)) |
| |
|
| | self.showDebugObject(pathGeom, "PathGeom_{}".format(round(csHght, 2))) |
| |
|
| | if cutPattern == "Line": |
| | |
| | pntSet = PathSurfaceSupport.pathGeomToLinesPointSet(self, obj, pathGeom) |
| | elif cutPattern == "ZigZag": |
| | |
| | pntSet = PathSurfaceSupport.pathGeomToZigzagPointSet(self, obj, pathGeom) |
| | elif cutPattern in ["Circular", "CircularZigZag"]: |
| | |
| | pntSet = PathSurfaceSupport.pathGeomToCircularPointSet(self, obj, pathGeom) |
| | elif cutPattern == "Spiral": |
| | pntSet = PathSurfaceSupport.pathGeomToSpiralPointSet(obj, pathGeom) |
| |
|
| | stpOVRS = self._getExperimentalWaterlinePaths(pntSet, csHght, cutPattern) |
| | safePDC = False |
| | cmds = self._clearGeomToPaths(JOB, obj, safePDC, stpOVRS, cutPattern) |
| | commands.extend(cmds) |
| |
|
| | return commands |
| |
|
| | def _makeOffsetLayerPaths(self, obj, clrAreaShp, csHght): |
| | Path.Log.debug("_makeOffsetLayerPaths()") |
| | cmds = list() |
| | ofst = 0.0 - self.cutOut |
| | shape = clrAreaShp |
| | cont = True |
| | cnt = 0 |
| | while cont: |
| | ofstArea = PathUtils.getOffsetArea(shape, ofst, self.wpc) |
| | if not ofstArea: |
| | break |
| | for F in ofstArea.Faces: |
| | cmds.extend(self._wiresToWaterlinePath(obj, F, csHght)) |
| | shape = ofstArea |
| | if cnt == 0: |
| | ofst = 0.0 - self.cutOut |
| | cnt += 1 |
| | Path.Log.debug(" -Offset path count: {} at height: {}".format(cnt, round(csHght, 2))) |
| |
|
| | return cmds |
| |
|
| | def _clearGeomToPaths(self, JOB, obj, safePDC, stpOVRS, cutPattern): |
| | Path.Log.debug("_clearGeomToPaths()") |
| |
|
| | GCODE = [Path.Command("N (Beginning of Single-pass layer.)", {})] |
| | tolrnc = JOB.GeometryTolerance.Value |
| | lenstpOVRS = len(stpOVRS) |
| | |
| | |
| | gDIR = ["G3", "G2"] |
| |
|
| | if self.CutClimb is True: |
| | gDIR = ["G2", "G3"] |
| |
|
| | |
| | first = stpOVRS[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, lenstpOVRS): |
| | cmds = list() |
| | PRTS = stpOVRS[so] |
| | lenPRTS = len(PRTS) |
| | first = PRTS[0][0] |
| | last = None |
| | cmds.append(Path.Command("N (Begin step {}.)".format(so), {})) |
| |
|
| | if so > 0: |
| | if cutPattern == "CircularZigZag": |
| | if odd: |
| | odd = False |
| | else: |
| | odd = True |
| | |
| | minTrnsHght = obj.SafeHeight.Value |
| | |
| | cmds.extend( |
| | self._stepTransitionCmds(obj, cutPattern, lstStpEnd, first, minTrnsHght, tolrnc) |
| | ) |
| |
|
| | |
| | for i in range(0, lenPRTS): |
| | prt = PRTS[i] |
| | |
| | if prt == "BRK": |
| | nxtStart = PRTS[i + 1][0] |
| | |
| | minSTH = obj.SafeHeight.Value |
| | cmds.append(Path.Command("N (Break)", {})) |
| | cmds.extend(self._breakCmds(obj, cutPattern, last, nxtStart, minSTH, tolrnc)) |
| | else: |
| | cmds.append(Path.Command("N (part {}.)".format(i + 1), {})) |
| | if cutPattern in ["Line", "ZigZag", "Spiral"]: |
| | start, last = prt |
| | cmds.append( |
| | Path.Command( |
| | "G1", |
| | { |
| | "X": start.x, |
| | "Y": start.y, |
| | "Z": start.z, |
| | "F": self.horizFeed, |
| | }, |
| | ) |
| | ) |
| | cmds.append( |
| | Path.Command("G1", {"X": last.x, "Y": last.y, "F": self.horizFeed}) |
| | ) |
| | elif cutPattern in ["Circular", "CircularZigZag"]: |
| | |
| | isZigZag = True if cutPattern == "CircularZigZag" else False |
| | Path.Log.debug( |
| | "so, isZigZag, odd, cMode: {}, {}, {}, {}".format( |
| | so, isZigZag, odd, prt[3] |
| | ) |
| | ) |
| | gcode = self._makeGcodeArc(prt, gDIR, odd, isZigZag) |
| | cmds.extend(gcode) |
| | cmds.append(Path.Command("N (End of step {}.)".format(so), {})) |
| | GCODE.extend(cmds) |
| | lstStpEnd = last |
| | |
| |
|
| | |
| | GCODE.append(Path.Command("G0", {"Z": obj.SafeHeight.Value, "F": self.vertRapid})) |
| |
|
| | return GCODE |
| |
|
| | def _getSolidAreasFromPlanarFaces(self, csFaces): |
| | Path.Log.debug("_getSolidAreasFromPlanarFaces()") |
| | holds = list() |
| | useFaces = list() |
| | lenCsF = len(csFaces) |
| | Path.Log.debug("lenCsF: {}".format(lenCsF)) |
| |
|
| | if lenCsF == 1: |
| | useFaces = csFaces |
| | else: |
| | fIds = list() |
| | aIds = list() |
| | pIds = list() |
| | cIds = list() |
| |
|
| | for af in range(0, lenCsF): |
| | fIds.append(af) |
| | aIds.append(af) |
| | pIds.append(-1) |
| | cIds.append(False) |
| | holds.append(False) |
| |
|
| | while len(fIds) > 0: |
| | li = fIds.pop() |
| | low = csFaces[li] |
| | pIds = self._idInternalFeature(csFaces, fIds, pIds, li, low) |
| |
|
| | for af in range(lenCsF - 1, -1, -1): |
| | prnt = pIds[af] |
| | if prnt == -1: |
| | stack = -1 |
| | else: |
| | stack = [af] |
| | |
| | stack.insert(0, prnt) |
| | nxtPrnt = pIds[prnt] |
| | |
| | while nxtPrnt != -1: |
| | stack.insert(0, nxtPrnt) |
| | nxtPrnt = pIds[nxtPrnt] |
| | cIds[af] = stack |
| |
|
| | for af in range(0, lenCsF): |
| | pFc = cIds[af] |
| | if pFc == -1: |
| | |
| | holds[af] = csFaces[af] |
| | else: |
| | |
| | cnt = len(pFc) |
| | if cnt % 2.0 == 0.0: |
| | |
| | inr = pFc[cnt - 1] |
| | otr = pFc[cnt - 2] |
| | holds[otr] = holds[otr].cut(csFaces[inr]) |
| | else: |
| | |
| | holds[af] = csFaces[af] |
| |
|
| | for af in range(0, lenCsF): |
| | if holds[af]: |
| | useFaces.append(holds[af]) |
| | |
| |
|
| | if len(useFaces) > 0: |
| | return useFaces |
| |
|
| | return False |
| |
|
| | def _getModelCrossSection(self, shape, csHght): |
| | Path.Log.debug("_getModelCrossSection()") |
| | wires = list() |
| |
|
| | def byArea(fc): |
| | return fc.Area |
| |
|
| | for i in shape.slice(FreeCAD.Vector(0, 0, 1), csHght): |
| | wires.append(i) |
| |
|
| | if len(wires) > 0: |
| | for w in wires: |
| | if w.isClosed() is False: |
| | return False |
| | FCS = list() |
| | for w in wires: |
| | w.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - w.BoundBox.ZMin)) |
| | FCS.append(Part.Face(w)) |
| | FCS.sort(key=byArea, reverse=True) |
| | return FCS |
| | else: |
| | Path.Log.debug(" -No wires from .slice() method") |
| |
|
| | return False |
| |
|
| | def _isInBoundBox(self, outShp, inShp): |
| | obb = outShp.BoundBox |
| | ibb = inShp.BoundBox |
| |
|
| | if obb.XMin < ibb.XMin: |
| | if obb.XMax > ibb.XMax: |
| | if obb.YMin < ibb.YMin: |
| | if obb.YMax > ibb.YMax: |
| | return True |
| | return False |
| |
|
| | def _idInternalFeature(self, csFaces, fIds, pIds, li, low): |
| | Ids = list() |
| | for i in fIds: |
| | Ids.append(i) |
| | while len(Ids) > 0: |
| | hi = Ids.pop() |
| | high = csFaces[hi] |
| | if self._isInBoundBox(high, low): |
| | cmn = high.common(low) |
| | if cmn.Area > 0.0: |
| | pIds[li] = hi |
| | break |
| |
|
| | return pIds |
| |
|
| | def _wireToPath(self, obj, wire, startVect): |
| | """_wireToPath(obj, wire, startVect) ... wire to path.""" |
| | Path.Log.track() |
| |
|
| | paths = [] |
| | pathParams = {} |
| |
|
| | pathParams["shapes"] = [wire] |
| | pathParams["feedrate"] = self.horizFeed |
| | pathParams["feedrate_v"] = self.vertFeed |
| | pathParams["verbose"] = True |
| | pathParams["retraction"] = obj.SafeHeight.Value |
| | pathParams["return_end"] = True |
| | |
| | pathParams["preamble"] = False |
| | pathParams["start"] = startVect |
| |
|
| | (pp, end_vector) = Path.fromShapes(**pathParams) |
| | paths.extend(pp.Commands) |
| |
|
| | self.endVector = end_vector |
| |
|
| | return (paths, end_vector) |
| |
|
| | def _makeExtendedBoundBox(self, wBB, bbBfr, zDep): |
| | pl = FreeCAD.Placement() |
| | pl.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), 0) |
| | pl.Base = FreeCAD.Vector(0, 0, 0) |
| |
|
| | p1 = FreeCAD.Vector(wBB.XMin - bbBfr, wBB.YMin - bbBfr, zDep) |
| | p2 = FreeCAD.Vector(wBB.XMax + bbBfr, wBB.YMin - bbBfr, zDep) |
| | p3 = FreeCAD.Vector(wBB.XMax + bbBfr, wBB.YMax + bbBfr, zDep) |
| | p4 = FreeCAD.Vector(wBB.XMin - bbBfr, wBB.YMax + bbBfr, zDep) |
| | bb = Part.makePolygon([p1, p2, p3, p4, p1]) |
| |
|
| | return bb |
| |
|
| | def _makeGcodeArc(self, prt, gDIR, odd, isZigZag): |
| | cmds = list() |
| | strtPnt, endPnt, cntrPnt, cMode = prt |
| | gdi = 0 |
| | if odd: |
| | gdi = 1 |
| | else: |
| | if not cMode and isZigZag: |
| | gdi = 1 |
| | gCmd = gDIR[gdi] |
| |
|
| | |
| | |
| | ijk = cntrPnt.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( |
| | gCmd, |
| | { |
| | "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 cmds |
| |
|
| | def _clearLayer(self, obj, ca, lastCA, clearLastLayer): |
| | Path.Log.debug("_clearLayer()") |
| | clrLyr = False |
| |
|
| | if obj.ClearLastLayer == "Off": |
| | if obj.CutPattern != "None": |
| | clrLyr = obj.CutPattern |
| | else: |
| | obj.CutPattern = "None" |
| | if ca == lastCA: |
| | Path.Log.debug("... Clearing bottom layer.") |
| | clrLyr = obj.ClearLastLayer |
| | clearLastLayer = False |
| |
|
| | return (clrLyr, clearLastLayer) |
| |
|
| | |
| | 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 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 ObjectWaterline.opPropertyDefinitions(False)] |
| |
|
| |
|
| | def Create(name, obj=None, parentJob=None): |
| | """Create(name) ... Creates and returns a Waterline operation.""" |
| | if obj is None: |
| | obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name) |
| | obj.Proxy = ObjectWaterline(obj, name, parentJob) |
| | return obj |
| |
|