From 2b094f01600c8ee92976abe692e77b6dff474a29 Mon Sep 17 00:00:00 2001 From: Russell Johnson <47639332+Russ4262@users.noreply.github.com> Date: Sun, 15 Mar 2020 23:39:06 -0500 Subject: [PATCH] PathSurface: Add select-face(s) feature and other improvements New feature: Limit 3D Surface op to selected faces. Disabled the IgnoreWaste feature, until it can be made compatible with new features. New feature: Face avoidance using new `AvoidLastXFaces` property. New cut patterns: `Circular` and `CircularZigZag`. New patterns include customizable center point and optional optimizations. Implemented G2/G3 gcode commands. New feature: Safe travel for transitional paths. New methods to allow safe travel over stock, cut area, and avoided features. New feature: Start point for operation. Choose a custom XY start point for the operation. Restructure code to improve management and prepare for separation of the `Waterline` algorithm into an independent operation in the PathWB. New feature: `ProfileEdges`. Allows the user to profile the edges(boundary) of the selected face(s) with or without cutting the entire face area. Add my name to the `Credits` tab in the `About FreeCAD` documentation. Added new feature and property: `CutPatternReversed`. This will make Circular, Line, and ZigZag patterns work in reverse order - outside to inside. PathSurface: Add select-face(s) feature and other improvements New feature: Limit 3D Surface op to selected faces. Disabled the IgnoreWaste feature, until it can be made compatible with new features. New feature: Face avoidance using new `AvoidLastXFaces` property. New cut patterns: `Circular` and `CircularZigZag`. New patterns include customizable center point and optional optimizations. Implemented G2/G3 gcode commands. New feature: Safe travel for transitional paths. New methods to allow safe travel over stock, cut area, and avoided features. New feature: Start point for operation. Choose a custom XY start point for the operation. Restructure code to improve management and prepare for separation of the `Waterline` algorithm into an independent operation in the PathWB. New feature: `ProfileEdges`. Allows the user to profile the edges(boundary) of the selected face(s) with or without cutting the entire face area. Add my name to the `Credits` tab in the `About FreeCAD` documentation. --- src/Gui/AboutApplication.ui | 1 + src/Mod/Path/PathScripts/PathSelection.py | 7 +- src/Mod/Path/PathScripts/PathSurface.py | 4008 +++++++++++++++----- src/Mod/Path/PathScripts/PathSurfaceGui.py | 6 +- 4 files changed, 3158 insertions(+), 864 deletions(-) diff --git a/src/Gui/AboutApplication.ui b/src/Gui/AboutApplication.ui index 50507683c9..5f39aa7ca9 100644 --- a/src/Gui/AboutApplication.ui +++ b/src/Gui/AboutApplication.ui @@ -462,6 +462,7 @@ p, li { white-space: pre-wrap; } <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Rentlau</p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">R. Kuster</p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">† Roland Frank (1970-2017) Thanks for everything, Roland!</p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Russell Johnson (russ4262)</p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Ryan Pavlik</p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Sabin Iacob</p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Saso Badovinac</p> diff --git a/src/Mod/Path/PathScripts/PathSelection.py b/src/Mod/Path/PathScripts/PathSelection.py index 4cadfd8926..acb9c473e2 100644 --- a/src/Mod/Path/PathScripts/PathSelection.py +++ b/src/Mod/Path/PathScripts/PathSelection.py @@ -207,7 +207,12 @@ def adaptiveselect(): FreeCAD.Console.PrintWarning("Adaptive Select Mode\n") def surfaceselect(): - FreeCADGui.Selection.addSelectionGate(MESHGate()) + if(MESHGate() is True or PROFILEGate() is True): + FreeCADGui.Selection.addSelectionGate(True) + else: + FreeCADGui.Selection.addSelectionGate(False) + # FreeCADGui.Selection.addSelectionGate(MESHGate()) + # FreeCADGui.Selection.addSelectionGate(PROFILEGate()) # Added for face selection FreeCAD.Console.PrintWarning("Surfacing Select Mode\n") def select(op): diff --git a/src/Mod/Path/PathScripts/PathSurface.py b/src/Mod/Path/PathScripts/PathSurface.py index 0b83a57079..1f597cde04 100644 --- a/src/Mod/Path/PathScripts/PathSurface.py +++ b/src/Mod/Path/PathScripts/PathSurface.py @@ -23,36 +23,9 @@ # *************************************************************************** # * * # * Additional modifications and contributions beginning 2019 * -# * by Russell Johnson * -# * Version: Rev. 3t Usable * +# * by Russell Johnson 2020-03-14 13:15 CST * # * * # *************************************************************************** -# Revision Notes -# - Continue implementation of cut patterns: zigzag, line (line is used to force cut modes: climb, conventional) -# - Continue implementation of ignore waste feature for planar scans -# - Planar Op: move scan result(self.CLP) to local scope(CLP) -# - Implemented @sliptonic's meshFromShape() parameter improvements for better, faster mesh creation - -# RELEASE NOTES -# Only G0 and G1 gcode commands are used throughout. -# CutMode: Climb, Conventional is only functional for some operations, like waterline and rotational scans -# CutPattern: only functional for some operations -# IgnoreWaste: not implemented yet - target op is for planar dropcutter -# High density/resolution, mult-layer scans require significantly more processing time. -# Rotational scans are very processor intensive. -# Rotational scans take a longer time to complete. -# Multi-pass rotational scans require a very long time to complete, -# even at larger SampleInterval values - ex: 1mm, 0.5mm -# Remember, the larger the model, the more time to complete an operation! -# Rotational require even more time. Multi-pass require much, much more time. -# This release is NOT bug-free. -# After changing an operational value in the Properties list, press the ENTER key. -# Change all desired values, one at a time, pressing ENTER key after each change. -# When all property changes complete, click the blue recompute icon under user menus. -# If you have an existing 3D Surface Op in your Job(s), you will need to delete it and recreate -# with this updated script installed because it may contain additional properties not -# created with the original version. - from __future__ import print_function @@ -66,19 +39,19 @@ import PathScripts.PathOp as PathOp from PySide import QtCore import time import math +import Part +import Draft +if FreeCAD.GuiUp: + import FreeCADGui __title__ = "Path Surface Operation" __author__ = "sliptonic (Brad Collette)" __url__ = "http://www.freecadweb.org" __doc__ = "Class and implementation of Mill Facing operation." -__contributors__ = "roivai[FreeCAD], russ4262 (Russell Johnson)" -__created__ = "2016" -__scriptVersion__ = "3t Usable" -__lastModified__ = "2019-05-10 10:37 CST" PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) -#PathLog.trackModule(PathLog.thisModule()) +# PathLog.trackModule(PathLog.thisModule()) # Qt translation handling @@ -99,12 +72,6 @@ except ImportError: class ObjectSurface(PathOp.ObjectOp): '''Proxy object for Surfacing operation.''' - # These are static while document is open, if it contains a 3D Surface Op - initFinalDepth = None - initOpFinalDepth = None - initOpStartDepth = None - docRestored = False - def baseObject(self): '''baseObject() ... returns super of receiver Used to call base implementation in overwritten functions.''' @@ -112,106 +79,206 @@ class ObjectSurface(PathOp.ObjectOp): def opFeatures(self, obj): '''opFeatures(obj) ... return all standard features and edges based geomtries''' - return PathOp.FeatureTool | PathOp.FeatureDepths | PathOp.FeatureHeights | PathOp.FeatureStepDown | PathOp.FeatureCoolant + return PathOp.FeatureTool | PathOp.FeatureDepths | PathOp.FeatureHeights | PathOp.FeatureStepDown | PathOp.FeatureCoolant | PathOp.FeatureBaseFaces def initOperation(self, obj): '''initPocketOp(obj) ... create facing specific properties''' obj.addProperty("App::PropertyEnumeration", "Algorithm", "Algorithm", QtCore.QT_TRANSLATE_NOOP("App::Property", "The library to use to generate the path")) - obj.addProperty("App::PropertyEnumeration", "DropCutterDir", "Algorithm", QtCore.QT_TRANSLATE_NOOP("App::Property", "The direction along which dropcutter lines are created")) obj.addProperty("App::PropertyEnumeration", "BoundBox", "Algorithm", QtCore.QT_TRANSLATE_NOOP("App::Property", "Should the operation be limited by the stock object or by the bounding box of the base object")) + obj.addProperty("App::PropertyEnumeration", "DropCutterDir", "Algorithm", QtCore.QT_TRANSLATE_NOOP("App::Property", "The direction along which dropcutter lines are created")) obj.addProperty("App::PropertyVectorDistance", "DropCutterExtraOffset", "Algorithm", QtCore.QT_TRANSLATE_NOOP("App::Property", "Additional offset to the selected bounding box")) - obj.addProperty("App::PropertyEnumeration", "ScanType", "Algorithm", QtCore.QT_TRANSLATE_NOOP("App::Property", "Planar: Flat, 3D surface scan. Rotational: 4th-axis rotational scan.")) obj.addProperty("App::PropertyEnumeration", "LayerMode", "Algorithm", QtCore.QT_TRANSLATE_NOOP("App::Property", "The completion mode for the operation: single or multi-pass")) - obj.addProperty("App::PropertyEnumeration", "CutMode", "Path", QtCore.QT_TRANSLATE_NOOP("App::Property", "The direction that the toolpath should go around the part: Climb(ClockWise) or Conventional(CounterClockWise)")) - obj.addProperty("App::PropertyEnumeration", "CutPattern", "Path", QtCore.QT_TRANSLATE_NOOP("App::Property", "Clearing pattern to use")) + obj.addProperty("App::PropertyEnumeration", "ScanType", "Algorithm", QtCore.QT_TRANSLATE_NOOP("App::Property", "Planar: Flat, 3D surface scan. Rotational: 4th-axis rotational scan.")) + + obj.addProperty("App::PropertyFloat", "CutterTilt", "Rotational", QtCore.QT_TRANSLATE_NOOP("App::Property", "Stop index(angle) for rotational scan")) obj.addProperty("App::PropertyEnumeration", "RotationAxis", "Rotational", QtCore.QT_TRANSLATE_NOOP("App::Property", "The model will be rotated around this axis.")) obj.addProperty("App::PropertyFloat", "StartIndex", "Rotational", QtCore.QT_TRANSLATE_NOOP("App::Property", "Start index(angle) for rotational scan")) obj.addProperty("App::PropertyFloat", "StopIndex", "Rotational", QtCore.QT_TRANSLATE_NOOP("App::Property", "Stop index(angle) for rotational scan")) - obj.addProperty("App::PropertyFloat", "CutterTilt", "Rotational", QtCore.QT_TRANSLATE_NOOP("App::Property", "Stop index(angle) for rotational scan")) - obj.addProperty("App::PropertyPercent", "StepOver", "Surface", QtCore.QT_TRANSLATE_NOOP("App::Property", "Step over percentage of the drop cutter path")) + + obj.addProperty("App::PropertyInteger", "AvoidLastX_Faces", "Surface", QtCore.QT_TRANSLATE_NOOP("App::Property", "Avoid cutting the last 'N' faces in the Base Geometry list of selected faces.")) + obj.addProperty("App::PropertyBool", "AvoidLastX_InternalFeatures", "Surface", QtCore.QT_TRANSLATE_NOOP("App::Property", "Do not cut internal features on avoided faces.")) + obj.addProperty("App::PropertyDistance", "BoundaryAdjustment", "Surface", QtCore.QT_TRANSLATE_NOOP("App::Property", "Positive values push the cutter toward, or beyond, the boundary. Negative values retract the cutter away from the boundary.")) + obj.addProperty("App::PropertyBool", "BoundaryEnforcement", "Surface", QtCore.QT_TRANSLATE_NOOP("App::Property", "If true, the cutter will remain inside the boundaries of the model or selected face(s).")) obj.addProperty("App::PropertyDistance", "DepthOffset", "Surface", QtCore.QT_TRANSLATE_NOOP("App::Property", "Z-axis offset from the surface of the object")) - # obj.addProperty("App::PropertyFloatConstraint", "SampleInterval", "Surface", QtCore.QT_TRANSLATE_NOOP("App::Property", "The Sample Interval. Small values cause long wait times")) - obj.addProperty("App::PropertyFloat", "SampleInterval", "Surface", QtCore.QT_TRANSLATE_NOOP("App::Property", "The Sample Interval. Small values cause long wait times")) - obj.addProperty("App::PropertyBool", "Optimize", "Surface", QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable optimization which removes unnecessary points from G-Code output")) + obj.addProperty("App::PropertyEnumeration", "HandleMultipleFeatures", "Surface", QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose how to process multiple Base Geometry features.")) + obj.addProperty("App::PropertyDistance", "InternalFeaturesAdjustment", "Surface", QtCore.QT_TRANSLATE_NOOP("App::Property", "Positive values push the cutter toward, or into, the feature. Negative values retract the cutter away from the feature.")) + obj.addProperty("App::PropertyBool", "InternalFeaturesCut", "Surface", QtCore.QT_TRANSLATE_NOOP("App::Property", "Ignore internal feature areas within a larger selected face.")) + obj.addProperty("App::PropertyEnumeration", "ProfileEdges", "Surface", QtCore.QT_TRANSLATE_NOOP("App::Property", "Profile the edges of the selection.")) + obj.addProperty("App::PropertyDistance", "SampleInterval", "Surface", QtCore.QT_TRANSLATE_NOOP("App::Property", "The Sample Interval. Small values cause long wait times")) + obj.addProperty("App::PropertyPercent", "StepOver", "Surface", QtCore.QT_TRANSLATE_NOOP("App::Property", "Step over percentage of the drop cutter path")) + + obj.addProperty("App::PropertyVectorDistance", "CircularCenterCustom", "Surface Cut Options", QtCore.QT_TRANSLATE_NOOP("PathOp", "The start point of this path")) + obj.addProperty("App::PropertyEnumeration", "CircularCenterAt", "Surface Cut Options", QtCore.QT_TRANSLATE_NOOP("PathOp", "Choose what point to start the ciruclar pattern: Center Of Mass, Center Of Boundbox, Xmin Ymin of boundbox, Custom.")) + obj.addProperty("App::PropertyEnumeration", "CutMode", "Surface Cut Options", QtCore.QT_TRANSLATE_NOOP("App::Property", "The direction that the toolpath should go around the part: Climb(ClockWise) or Conventional(CounterClockWise)")) + obj.addProperty("App::PropertyEnumeration", "CutPattern", "Surface Cut Options", QtCore.QT_TRANSLATE_NOOP("App::Property", "Clearing pattern to use")) + obj.addProperty("App::PropertyFloat", "CutPatternAngle", "Surface Cut Options", QtCore.QT_TRANSLATE_NOOP("App::Property", "Yaw angle for certain clearing patterns")) + obj.addProperty("App::PropertyBool", "CutPatternReversed", "Surface Cut Options", QtCore.QT_TRANSLATE_NOOP("App::Property", "If true, the order of the step-overs will be reversed; the operation will begin cutting the outer most line/arc, and work toward the inner most line/arc.")) + + obj.addProperty("App::PropertyBool", "OptimizeLinearPaths", "Surface Optimization", QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable optimization of linear paths (co-linear points). Removes unnecessary co-linear points from G-Code output.")) + obj.addProperty("App::PropertyBool", "OptimizeStepOverTransitions", "Surface Optimization", QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable separate optimization of transitions between, and breaks within, each step over path.")) + obj.addProperty("App::PropertyBool", "CircularUseG2G3", "Surface Optimization", QtCore.QT_TRANSLATE_NOOP("App::Property", "Convert co-planar arcs to G2/G3 gcode commands for `Circular` and `CircularZigZag` cut patterns.")) + obj.addProperty("App::PropertyDistance", "GapThreshold", "Surface Optimization", QtCore.QT_TRANSLATE_NOOP("App::Property", "Collinear and co-radial artifact gaps that are smaller than this threshold are closed in the path.")) + obj.addProperty("App::PropertyString", "GapSizes", "Surface Optimization", QtCore.QT_TRANSLATE_NOOP("App::Property", "Feedback: three smallest gaps identified in the path geometry.")) + obj.addProperty("App::PropertyBool", "IgnoreWaste", "Waste", QtCore.QT_TRANSLATE_NOOP("App::Property", "Ignore areas that proceed below specified depth.")) obj.addProperty("App::PropertyFloat", "IgnoreWasteDepth", "Waste", QtCore.QT_TRANSLATE_NOOP("App::Property", "Depth used to identify waste areas to ignore.")) obj.addProperty("App::PropertyBool", "ReleaseFromWaste", "Waste", QtCore.QT_TRANSLATE_NOOP("App::Property", "Cut through waste to depth at model edge, releasing the model.")) - obj.CutMode = ['Conventional', 'Climb'] - obj.BoundBox = ['BaseBoundBox', 'Stock'] - obj.DropCutterDir = ['X', 'Y'] + obj.addProperty("App::PropertyVectorDistance", "StartPoint", "Start Point", QtCore.QT_TRANSLATE_NOOP("PathOp", "The start point of this path")) + obj.addProperty("App::PropertyBool", "UseStartPoint", "Start Point", QtCore.QT_TRANSLATE_NOOP("PathOp", "Make True, if specifying a Start Point")) + + # For debugging + obj.addProperty('App::PropertyString', 'AreaParams', 'Debugging') + obj.setEditorMode('AreaParams', 2) # hide + obj.addProperty("App::PropertyBool", "ShowTempObjects", "Debug", QtCore.QT_TRANSLATE_NOOP("App::Property", "If true, the temporary path construction objects will be shown.")) + if PathLog.getLevel(PathLog.thisModule()) != 4: + obj.setEditorMode('ShowTempObjects', 2) # hide + obj.Algorithm = ['OCL Dropcutter', 'OCL Waterline'] - # obj.SampleInterval = (0.04, 0.01, 1.0, 0.01) + obj.BoundBox = ['BaseBoundBox', 'Stock'] + obj.CircularCenterAt = ['CenterOfMass', 'CenterOfBoundBox', 'XminYmin', 'Custom'] + obj.CutMode = ['Conventional', 'Climb'] + obj.CutPattern = ['Line', 'ZigZag', 'Circular', 'CircularZigZag'] # Additional goals ['Offset', 'Spiral', 'ZigZagOffset', 'Grid', 'Triangle'] + obj.DropCutterDir = ['X', 'Y'] + obj.HandleMultipleFeatures = ['Collectively', 'Individually'] obj.LayerMode = ['Single-pass', 'Multi-pass'] - obj.ScanType = ['Planar', 'Rotational'] + obj.ProfileEdges = ['None', 'Only', 'First', 'Last'] obj.RotationAxis = ['X', 'Y'] - # obj.CutPattern = ['ZigZag', 'Offset', 'Spiral', 'ZigZagOffset', 'Line', 'Grid', 'Triangle'] - obj.CutPattern = ['ZigZag', 'Line'] + obj.ScanType = ['Planar', 'Rotational'] if not hasattr(obj, 'DoNotSetDefaultValues'): self.setEditorProperties(obj) + self.addedAllProperties = True + def setEditorProperties(self, obj): # Used to hide inputs in properties list + if obj.Algorithm == 'OCL Dropcutter': - obj.setEditorMode('DropCutterDir', 0) - # obj.setEditorMode('DropCutterExtraOffset', 0) - else: + obj.setEditorMode('CutPattern', 0) + obj.setEditorMode('HandleMultipleFeatures', 0) + obj.setEditorMode('CircularCenterAt', 0) + obj.setEditorMode('CircularCenterCustom', 0) + obj.setEditorMode('CutPatternAngle', 0) + # obj.setEditorMode('BoundaryEnforcement', 0) + + if obj.ScanType == 'Planar': + obj.setEditorMode('DropCutterDir', 2) + obj.setEditorMode('DropCutterExtraOffset', 2) + obj.setEditorMode('RotationAxis', 2) # 2=hidden + obj.setEditorMode('StartIndex', 2) + obj.setEditorMode('StopIndex', 2) + obj.setEditorMode('CutterTilt', 2) + if obj.CutPattern == 'Circular' or obj.CutPattern == 'CircularZigZag': + obj.setEditorMode('CutPatternAngle', 2) + else: # if obj.CutPattern == 'Line' or obj.CutPattern == 'ZigZag': + obj.setEditorMode('CircularCenterAt', 2) + obj.setEditorMode('CircularCenterCustom', 2) + elif obj.ScanType == 'Rotational': + obj.setEditorMode('DropCutterDir', 0) + obj.setEditorMode('DropCutterExtraOffset', 0) + obj.setEditorMode('RotationAxis', 0) # 0=show & editable + obj.setEditorMode('StartIndex', 0) + obj.setEditorMode('StopIndex', 0) + obj.setEditorMode('CutterTilt', 0) + + elif obj.Algorithm == 'OCL Waterline': + obj.setEditorMode('DropCutterExtraOffset', 2) obj.setEditorMode('DropCutterDir', 2) - # obj.setEditorMode('DropCutterExtraOffset', 2) + obj.setEditorMode('HandleMultipleFeatures', 2) + obj.setEditorMode('CutPattern', 2) + obj.setEditorMode('CutPatternAngle', 2) + # obj.setEditorMode('BoundaryEnforcement', 2) + + # Disable IgnoreWaste feature + obj.setEditorMode('IgnoreWaste', 2) + obj.setEditorMode('IgnoreWasteDepth', 2) + obj.setEditorMode('ReleaseFromWaste', 2) def onChanged(self, obj, prop): - if prop == "Algorithm": - self.setEditorProperties(obj) + if hasattr(self, 'addedAllProperties'): + if self.addedAllProperties is True: + if prop == 'Algorithm': + self.setEditorProperties(obj) + if prop == 'ScanType': + self.setEditorProperties(obj) + if prop == 'CutPattern': + self.setEditorProperties(obj) def opOnDocumentRestored(self, obj): + if PathLog.getLevel(PathLog.thisModule()) != 4: + obj.setEditorMode('ShowTempObjects', 2) # hide + else: + obj.setEditorMode('ShowTempObjects', 0) # show + self.addedAllProperties = True self.setEditorProperties(obj) - # Import FinalDepth from existing operation for use in recompute() operations - self.initFinalDepth = obj.FinalDepth.Value - self.initOpFinalDepth = obj.OpFinalDepth.Value - self.docRestored = True - PathLog.debug("Imported existing OpFinalDepth of " + str(self.initOpFinalDepth) + " for recompute() purposes.") - PathLog.debug("Imported existing FinalDepth of " + str(self.initFinalDepth) + " for recompute() purposes.") - def opExecute(self, obj): - '''opExecute(obj) ... process surface operation''' - PathLog.track() - initIdx = 0.0 + def opSetDefaultValues(self, obj, job): + '''opSetDefaultValues(obj, job) ... initialize defaults''' + job = PathUtils.findParentJob(obj) - # Instantiate additional class operation variables - self.resetOpVariables() + obj.OptimizeLinearPaths = True + obj.IgnoreWaste = False + obj.ReleaseFromWaste = False + obj.InternalFeaturesCut = True + obj.OptimizeStepOverTransitions = False + obj.CircularUseG2G3 = False + obj.BoundaryEnforcement = True + obj.UseStartPoint = False + obj.AvoidLastX_InternalFeatures = True + obj.CutPatternReversed = False + obj.StartPoint.x = 0.0 + obj.StartPoint.y = 0.0 + obj.StartPoint.z = obj.ClearanceHeight.Value + obj.ProfileEdges = 'None' + obj.LayerMode = 'Single-pass' + obj.ScanType = 'Planar' + obj.RotationAxis = 'X' + obj.CutMode = 'Conventional' + obj.CutPattern = 'Line' + obj.HandleMultipleFeatures = 'Collectively' # 'Individually' + obj.CircularCenterAt = 'CenterOfMass' # 'CenterOfBoundBox', 'XminYmin', 'Custom' + obj.AreaParams = '' + obj.GapSizes = 'No gaps identified.' + obj.StepOver = 100 + obj.CutPatternAngle = 0.0 + obj.CutterTilt = 0.0 + obj.StartIndex = 0.0 + obj.StopIndex = 360.0 + obj.SampleInterval.Value = 1.0 + obj.BoundaryAdjustment.Value = 0.0 + obj.InternalFeaturesAdjustment.Value = 0.0 + obj.AvoidLastX_Faces = 0 + obj.CircularCenterCustom.x = 0.0 + obj.CircularCenterCustom.y = 0.0 + obj.CircularCenterCustom.z = 0.0 + obj.GapThreshold.Value = 0.005 + # For debugging + obj.ShowTempObjects = False - # mark beginning of operation - self.startTime = time.time() + # need to overwrite the default depth calculations for facing + d = None + if job: + if job.Stock: + d = PathUtils.guessDepths(job.Stock.Shape, None) + PathLog.debug("job.Stock exists") + else: + PathLog.debug("job.Stock NOT exist") + else: + PathLog.debug("job NOT exist") - # Set cutter for OCL based on tool controller properties - self.setOclCutter(obj) + 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 - self.reportThis("\n-----\n-----\nBegin 3D surface operation") - self.reportThis("Script version: " + __scriptVersion__ + " Lm: " + __lastModified__) - - output = "" - if obj.Comment != "": - output += '(' + str(obj.Comment) + ')\n' - - output += "(" + obj.Label + ")" - output += "(Compensated Tool Path. Diameter: " + str(obj.ToolController.Tool.Diameter) + ")" - - parentJob = PathUtils.findParentJob(obj) - if parentJob is None: - self.reportThis("No parentJob") - return - self.SafeHeightOffset = parentJob.SetupSheet.SafeHeightOffset.Value - self.ClearHeightOffset = parentJob.SetupSheet.ClearanceHeightOffset.Value - - # Import OpFinalDepth from pre-existing operation for recompute() scenarios - if obj.OpFinalDepth.Value != self.initOpFinalDepth: - if obj.OpFinalDepth.Value == obj.FinalDepth.Value: - obj.FinalDepth.Value = self.initOpFinalDepth - obj.OpFinalDepth.Value = self.initOpFinalDepth - if self.initOpFinalDepth is not None: - obj.OpFinalDepth.Value = self.initOpFinalDepth + PathLog.debug('Default OpFinalDepth: {}'.format(obj.OpFinalDepth.Value)) + PathLog.debug('Defualt OpStartDepth: {}'.format(obj.OpStartDepth.Value)) + def opApplyPropertyLimits(self, obj): + '''opApplyPropertyLimits(obj) ... Apply necessary limits to user input property values before performing main operation.''' # Limit start index if obj.StartIndex < 0.0: obj.StartIndex = 0.0 @@ -231,636 +298,2806 @@ class ObjectSurface(PathOp.ObjectOp): obj.CutterTilt = 90.0 # Limit sample interval - if obj.SampleInterval < 0.001: - obj.SampleInterval = 0.001 - if obj.SampleInterval > 25.4: - obj.SampleInterval = 25.4 + if obj.SampleInterval.Value < 0.001: + obj.SampleInterval.Value = 0.001 + PathLog.error(translate('PathSurface', 'Sample interval limits are 0.001 to 25.4 millimeters.')) + if obj.SampleInterval.Value > 25.4: + obj.SampleInterval.Value = 25.4 + PathLog.error(translate('PathSurface', 'Sample interval limits are 0.001 to 25.4 millimeters.')) + + # Limit cut pattern angle + if obj.CutPatternAngle < -360.0: + obj.CutPatternAngle = 0.0 + PathLog.error(translate('PathSurface', 'Cut pattern angle limits are +-360 degrees.')) + if obj.CutPatternAngle >= 360.0: + obj.CutPatternAngle = 0.0 + PathLog.error(translate('PathSurface', 'Cut pattern angle limits are +- 360 degrees.')) # Limit StepOver to natural number percentage - if obj.Algorithm == 'OCL Dropcutter': - if obj.StepOver > 100: - obj.StepOver = 100 - if obj.StepOver < 1: - obj.StepOver = 1 - self.cutOut = (self.cutter.getDiameter() * (float(obj.StepOver) / 100.0)) - self.reportThis("Cut out: " + str(self.cutOut) + " mm") + if obj.StepOver > 100: + obj.StepOver = 100 + if obj.StepOver < 1: + obj.StepOver = 1 - # Cycle through parts of model - for base in self.model: - self.reportThis("BASE object: " + str(base.Name)) + # Limit AvoidLastX_Faces to zero and positive values + if obj.AvoidLastX_Faces < 0: + obj.AvoidLastX_Faces = 0 + PathLog.error(translate('PathSurface', 'AvoidLastX_Faces: Only zero or positive values permitted.')) + if obj.AvoidLastX_Faces > 100: + obj.AvoidLastX_Faces = 100 + PathLog.error(translate('PathSurface', 'AvoidLastX_Faces: Avoid last X faces count limited to 100.')) - # Rotate model to initial index - if obj.ScanType == 'Rotational': - initIdx = obj.CutterTilt + obj.StartIndex - if initIdx != 0.0: - self.basePlacement = FreeCAD.ActiveDocument.getObject(base.Name).Placement - if obj.RotationAxis == 'X': - # FreeCAD.ActiveDocument.getObject(base.Name).Placement = FreeCAD.Placement(FreeCAD.Vector(0,0,0),FreeCAD.Rotation(FreeCAD.Vector(1,0,0), initIdx)) - base.Placement = FreeCAD.Placement(FreeCAD.Vector(0, 0, 0), FreeCAD.Rotation(FreeCAD.Vector(1, 0, 0), initIdx)) - else: - # FreeCAD.ActiveDocument.getObject(base.Name).Placement = FreeCAD.Placement(FreeCAD.Vector(0,0,0),FreeCAD.Rotation(FreeCAD.Vector(0,1,0), initIdx)) - base.Placement = FreeCAD.Placement(FreeCAD.Vector(0, 0, 0), FreeCAD.Rotation(FreeCAD.Vector(0, 1, 0), initIdx)) + def opExecute(self, obj): + '''opExecute(obj) ... process surface operation''' + PathLog.track() - if base.TypeId.startswith('Mesh'): - mesh = base.Mesh + 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.deflection = None + self.tempGroup = None + self.CutClimb = False + self.closedGap = False + self.gaps = [0.1, 0.2, 0.3] + CMDS = list() + modelVisibility = list() + FCAD = FreeCAD.ActiveDocument + + # Set debugging behavior + self.showDebugObjects = False # Set to true if you want a visual DocObjects created for some path construction objects + self.showDebugObjects = obj.ShowTempObjects + deleteTempsFlag = True # Set to False for debugging + if PathLog.getLevel(PathLog.thisModule()) == 4: + deleteTempsFlag = False + else: + self.showDebugObjects = False + + # mark beginning of operation and identify parent Job + PathLog.info('\nBegin 3D Surface operation...') + startTime = time.time() + + # Disable(ignore) ReleaseFromWaste option(input) + obj.ReleaseFromWaste = False + + # Identify parent Job + JOB = PathUtils.findParentJob(obj) + if JOB is None: + PathLog.error(translate('PathSurface', "No JOB")) + return + self.stockZMin = JOB.Stock.Shape.BoundBox.ZMin + + # set cut mode; reverse as needed + if obj.CutMode == 'Climb': + self.CutClimb = True + if obj.CutPatternReversed is True: + if self.CutClimb is True: + self.CutClimb = False else: - # try/except is for Path Jobs created before GeometryTolerance - try: - deflection = parentJob.GeometryTolerance - except AttributeError: - import PathScripts.PathPreferences as PathPreferences - deflection = PathPreferences.defaultGeometryTolerance() - # base.Shape.tessellate(0.05) # 0.5 original value - # mesh = MeshPart.meshFromShape(base.Shape, Deflection=deflection) - mesh = MeshPart.meshFromShape(Shape=base.Shape, LinearDeflection=deflection, AngularDeflection=0.5, Relative=False) + self.CutClimb = True + # Begin GCode for operation with basic information + # ... and move cutter to clearance height and startpoint + output = '' + if obj.Comment != '': + output += '(' + str(obj.Comment) + ')\n' + output += '(' + obj.Label + ')\n' + output += '(Tool type: ' + str(obj.ToolController.Tool.ToolType) + ')\n' + output += '(Compensated Tool Path. Diameter: ' + str(obj.ToolController.Tool.Diameter) + ')\n' + output += '(Sample interval: ' + str(obj.SampleInterval.Value) + ')\n' + output += '(Step over %: ' + str(obj.StepOver) + ')\n' + 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})) + + # Instantiate additional class operation variables + self.resetOpVariables() + + # Impose property limits + self.opApplyPropertyLimits(obj) + + # Create temporary group for temporary objects, removing existing + # if self.showDebugObjects is True: + tempGroupName = 'tempPathSurfaceGroup' + if FCAD.getObject(tempGroupName): + for to in FCAD.getObject(tempGroupName).Group: + FCAD.removeObject(to.Name) + FCAD.removeObject(tempGroupName) # remove temp directory if already exists + if FCAD.getObject(tempGroupName + '001'): + for to in FCAD.getObject(tempGroupName + '001').Group: + FCAD.removeObject(to.Name) + FCAD.removeObject(tempGroupName + '001') # remove temp directory if already exists + tempGroup = FCAD.addObject('App::DocumentObjectGroup', tempGroupName) + tempGroupName = tempGroup.Name + self.tempGroup = tempGroup + tempGroup.purgeTouched() + # Add temp object to temp group folder with following code: + # ... self.tempGroup.addObject(OBJ) + + # Setup cutter for OCL and cutout value for operation - based on tool controller properties + self.cutter = self.setOclCutter(obj) + self.safeCutter = self.setOclCutter(obj, safe=True) + if self.cutter is False or self.safeCutter is False: + PathLog.error(translate('PathSurface', "Canceling 3D Surface operation. Error creating OCL cutter.")) + return + toolDiam = self.cutter.getDiameter() + self.cutOut = (toolDiam * (float(obj.StepOver) / 100.0)) + self.radius = toolDiam / 2.0 + self.gaps = [toolDiam, toolDiam, toolDiam] + + # Get height offset values for later use + self.SafeHeightOffset = JOB.SetupSheet.SafeHeightOffset.Value + self.ClearHeightOffset = JOB.SetupSheet.ClearanceHeightOffset.Value + + # Calculate default depthparams for operation + 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 + + # make circle for workplane + self.wpc = Part.makeCircle(2.0) + + # Set deflection values for mesh generation + self.angularDeflection = 0.05 + try: # try/except is for Path Jobs created before GeometryTolerance + self.deflection = JOB.GeometryTolerance.Value + except AttributeError as ee: + PathLog.warning('Error setting Mesh deflection: {}. Using PathPreferences.defaultGeometryTolerance().'.format(ee)) + import PathScripts.PathPreferences as PathPreferences + self.deflection = PathPreferences.defaultGeometryTolerance() + + # Save model visibilities for restoration + 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) + + # Setup STL, model type, and bound box containers for each model in Job + 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) # Set bound box - if obj.BoundBox == "BaseBoundBox": - bb = mesh.BoundBox - else: - bb = parentJob.Stock.Shape.BoundBox - - # Objective is to remove material from surface in StepDown layers rather than one pass to FinalDepth - final = [] - if obj.Algorithm == 'OCL Waterline': - self.reportThis("--CutMode: " + str(obj.CutMode)) - stl = ocl.STLSurf() - for f in mesh.Facets: - p = f.Points[0] - q = f.Points[1] - r = f.Points[2] - t = ocl.Triangle(ocl.Point(p[0], p[1], p[2] + obj.DepthOffset.Value), - ocl.Point(q[0], q[1], q[2] + obj.DepthOffset.Value), - ocl.Point(r[0], r[1], r[2] + obj.DepthOffset.Value)) - stl.addTriangle(t) - final = self._waterlineOp(obj, stl, bb) - elif obj.Algorithm == 'OCL Dropcutter': - # Create stl object via OCL - stl = ocl.STLSurf() - for f in mesh.Facets: - p = f.Points[0] - q = f.Points[1] - r = f.Points[2] - t = ocl.Triangle(ocl.Point(p[0], p[1], p[2]), - ocl.Point(q[0], q[1], q[2]), - ocl.Point(r[0], r[1], r[2])) - stl.addTriangle(t) - - # Rotate model back to original index - if obj.ScanType == 'Rotational': - if initIdx != 0.0: - initIdx = 0.0 - base.Placement = self.basePlacement - # if obj.RotationAxis == 'X': - # FreeCAD.ActiveDocument.getObject(base.Name).Placement = FreeCAD.Placement(FreeCAD.Vector(0,0,0),FreeCAD.Rotation(FreeCAD.Vector(1,0,0), initIdx)) - # else: - # FreeCAD.ActiveDocument.getObject(base.Name).Placement = FreeCAD.Placement(FreeCAD.Vector(0,0,0),FreeCAD.Rotation(FreeCAD.Vector(0,1,0), initIdx)) - - # Prepare global holdpoint container - if self.holdPoint is None: - self.holdPoint = ocl.Point(float("inf"), float("inf"), float("inf")) - if self.layerEndPnt is None: - self.layerEndPnt = ocl.Point(float("inf"), float("inf"), float("inf")) - - if obj.ScanType == 'Rotational': - # Remove extended material from Stock and re-assign bb - if hasattr(parentJob.Stock, 'ExtXneg'): - parentJob.Stock.ExtXneg = 0 - parentJob.Stock.ExtXpos = 0 - parentJob.Stock.ExtYneg = 0 - parentJob.Stock.ExtYpos = 0 - parentJob.Stock.ExtZneg = 0 - parentJob.Stock.ExtZpos = 0 - - # Avoid division by zero in rotational scan calculations - if obj.FinalDepth.Value <= 0.0: - zero = obj.SampleInterval # 0.00001 - self.FinalDepth = zero - obj.FinalDepth.Value = 0.0 - else: - self.FinalDepth = obj.FinalDepth.Value - - # Determine boundbox radius based upon xzy limits data - if math.fabs(bb.ZMin) > math.fabs(bb.ZMax): - vlim = bb.ZMin - else: - vlim = bb.ZMax - if obj.RotationAxis == 'X': - # Rotation is around X-axis, cutter moves along same axis - if math.fabs(bb.YMin) > math.fabs(bb.YMax): - hlim = bb.YMin - else: - hlim = bb.YMax - else: - # Rotation is around Y-axis, cutter moves along same axis - if math.fabs(bb.XMin) > math.fabs(bb.XMax): - hlim = bb.XMin - else: - hlim = bb.XMax - - # Compute max radius of stock, as it rotates, and rotational clearance & safe heights - self.bbRadius = math.sqrt(hlim**2 + vlim**2) - self.clearHeight = self.bbRadius + parentJob.SetupSheet.ClearanceHeightOffset.Value - self.safeHeight = self.bbRadius + parentJob.SetupSheet.ClearanceHeightOffset.Value - - final = self._rotationalDropCutterOp(obj, stl, bb) - elif obj.ScanType == 'Planar': - final = self._planarDropCutOp(obj, stl, bb) - # End IF - # Send final list of commands to operation object - self.commandlist.extend(final) - # End IF - - self.endTime = time.time() - self.reportThis("OPERATION time: " + str(self.endTime - self.startTime) + " sec.") - - print(self.opReport) - - def _planarDropCutOp(self, obj, stl, bb): - # t_before = time.time() - pntsPerLine = 0 - ignoreWasteFlag = obj.IgnoreWaste - ignoreMap = [1] - - def createTopoMap(scanCLP, ignoreDepth): - topoMap = [] - for pt in scanCLP: - if pt.z < ignoreDepth: - topoMap.append(0) + if obj.BoundBox == 'BaseBoundBox': + if M.TypeId.startswith('Mesh'): + self.modelTypes.append('M') # Mesh + self.boundBoxes.append(M.Mesh.BoundBox) else: - topoMap.append(2) - return topoMap + self.modelTypes.append('S') # Solid + self.boundBoxes.append(M.Shape.BoundBox) + elif obj.BoundBox == 'Stock': + self.modelTypes.append('S') # Solid + self.boundBoxes.append(JOB.Stock.Shape.BoundBox) - # Convert linear list of points to multi-dimensional list - def listToMultiDimensional(points, nL, pPL): - multiDim = [] - for L in range(0, nL): - multiDim.append([]) - for P in range(0, pPL): - pi = L * pPL + P - multiDim[L].append(points[pi]) - return multiDim + # ###### MAIN COMMANDS FOR OPERATION ###### - # De-buffer multi dimensional list - def debufferMultiDimenList(multi): - multi.pop(0) - multi.pop() - for i in range(0, len(multi)): - multi[i].pop(0) - multi[i].pop() - return multi + # If algorithm is `Waterline`, force certain property values + # Save initial value for restoration later. + if obj.Algorithm == 'OCL Waterline': + preCP = obj.CutPattern + preCPA = obj.CutPatternAngle + preRB = obj.BoundaryEnforcement + obj.CutPattern = 'Line' + obj.CutPatternAngle = 0.0 + obj.BoundaryEnforcement = False - # Convert multi-dimensional list to linear list of points - def multiDimensionalToList(multi): - points = [] - for L in multi: - points.extend(L) - return points + # Begin processing obj.Base data and creating GCode + # Process selected faces, if available + pPM = self._preProcessModel(JOB, obj) + if pPM is False: + PathLog.error('Unable to pre-process obj.Base.') + else: + (FACES, VOIDS) = pPM - # Prepare global holdpoint container - if self.holdPoint is None: - self.holdPoint = ocl.Point(float("inf"), float("inf"), float("inf")) + # Create OCL.stl model objects + self._prepareModelSTLs(JOB, obj) - # Set crop cutter extra offset - cdeoX = obj.DropCutterExtraOffset.x - cdeoY = obj.DropCutterExtraOffset.y + for m in range(0, len(JOB.Model.Group)): + Mdl = JOB.Model.Group[m] + if FACES[m] is False: + PathLog.error('No data for model base: {}'.format(JOB.Model.Group[m].Label)) + else: + if m > 0: + # Raise to clearance between moddels + CMDS.append(Path.Command('N (Transition to base: {}.)'.format(Mdl.Label))) + CMDS.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid})) + PathLog.info('Working on Model.Group[{}]: {}'.format(m, Mdl.Label)) + # make stock-model-voidShapes STL model for avoidance detection on transitions + self._makeSafeSTL(JOB, obj, m, FACES[m], VOIDS[m]) + time.sleep(0.2) + # Process model/faces - OCL objects must be ready + CMDS.extend(self._processCutAreas(JOB, obj, m, FACES[m], VOIDS[m])) - # the max and min XY area of the operation - xmin = bb.XMin - cdeoX - xmax = bb.XMax + cdeoX - ymin = bb.YMin - cdeoY - ymax = bb.YMax + cdeoY + # Save gcode produced + self.commandlist.extend(CMDS) + + # If algorithm is `Waterline`, restore initial property values + if obj.Algorithm == 'OCL Waterline': + obj.CutPattern = preCP + obj.CutPatternAngle = preCPA + obj.BoundaryEnforcement = preRB + + # ###### CLOSING COMMANDS FOR OPERATION ###### + + # Delete temporary objects + # Restore model visibilities for restoration + 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() + + # Provide user feedback for gap sizes + gaps = list() + for g in self.gaps: + if g != 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.' + + # clean up class variables + 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 + self.wpc = None + self.angularDeflection = None + self.deflection = 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 + del self.wpc + del self.angularDeflection + del self.deflection + + execTime = time.time() - startTime + PathLog.info('Operation time: {} sec.'.format(execTime)) + + return True + + # Methods for constructing the cut area + def _preProcessModel(self, JOB, obj): + PathLog.debug('_preProcessModel()') + + FACES = list() + VOIDS = list() + fShapes = list() + vShapes = list() + preProcEr = translate('PathSurface', 'Error pre-processing Face') + warnFinDep = translate('PathSurface', 'Final Depth might need to be lower. Internal features detected in Face') + GRP = JOB.Model.Group + lenGRP = len(GRP) + + # Crete place holders for each base model in Job + for m in range(0, lenGRP): + FACES.append(False) + VOIDS.append(False) + fShapes.append(False) + vShapes.append(False) + + # The user has selected subobjects from the base. Pre-Process each. + if obj.Base and len(obj.Base) > 0: + PathLog.debug(' -obj.Base exists. Pre-processing for selected faces.') + + (FACES, VOIDS) = self._identifyFacesAndVoids(JOB, obj, FACES, VOIDS) + + # Cycle through each base model, processing faces for each + for m in range(0, lenGRP): + base = GRP[m] + (mFS, mVS, mPS) = self._preProcessFacesAndVoids(obj, base, m, FACES, VOIDS) + fShapes[m] = mFS + vShapes[m] = mVS + self.profileShapes[m] = mPS + else: + PathLog.debug(' -No obj.Base data.') + for m in range(0, lenGRP): + self.modelSTLs[m] = True + + # Process each model base, as a whole, as needed + # PathLog.debug(' -Pre-processing all models in Job.') + for m in range(0, lenGRP): + if fShapes[m] is False: + PathLog.debug(' -Pre-processing {} as a whole.'.format(GRP[m].Label)) + if obj.BoundBox == 'BaseBoundBox': + base = GRP[m] + elif obj.BoundBox == 'Stock': + base = JOB.Stock + + pPEB = self._preProcessEntireBase(obj, base, m) + if pPEB is False: + PathLog.error(' -Failed to pre-process base as a whole.') + else: + (fcShp, prflShp) = pPEB + if fcShp is not False: + if fcShp is True: + PathLog.debug(' -fcShp is True.') + fShapes[m] = True + else: + fShapes[m] = [fcShp] + if prflShp is not False: + if fcShp is not False: + PathLog.debug('vShapes[{}]: {}'.format(m, vShapes[m])) + if vShapes[m] is not False: + PathLog.debug(' -Cutting void from base profile shape.') + adjPS = prflShp.cut(vShapes[m][0]) + self.profileShapes[m] = [adjPS] + else: + PathLog.debug(' -vShapes[m] is False.') + self.profileShapes[m] = [prflShp] + else: + PathLog.debug(' -Saving base profile shape.') + self.profileShapes[m] = [prflShp] + PathLog.debug('self.profileShapes[{}]: {}'.format(m, self.profileShapes[m])) + # Efor + + return (fShapes, vShapes) + + def _identifyFacesAndVoids(self, JOB, obj, F, V): + TUPS = list() + GRP = JOB.Model.Group + lenGRP = len(GRP) + + # Separate selected faces into (base, face) tuples and flag model(s) for STL creation + for (bs, SBS) in obj.Base: + for sb in SBS: + # Flag model for STL creation + mdlIdx = None + for m in range(0, lenGRP): + if bs is GRP[m]: + self.modelSTLs[m] = True + mdlIdx = m + break + TUPS.append((mdlIdx, bs, sb)) # (model idx, base, sub) + + # Apply `AvoidXFaces` value + faceCnt = len(TUPS) + add = faceCnt - obj.AvoidLastX_Faces + for bst in range(0, faceCnt): + (m, base, sub) = TUPS[bst] + shape = getattr(base.Shape, sub) + if isinstance(shape, Part.Face): + faceIdx = int(sub[4:]) - 1 + if bst < add: + if F[m] is False: + F[m] = list() + F[m].append((shape, faceIdx)) + else: + if V[m] is False: + V[m] = list() + V[m].append((shape, faceIdx)) + return (F, V) + + def _preProcessFacesAndVoids(self, obj, base, m, FACES, VOIDS): + mFS = False + mVS = False + mPS = False + mIFS = list() + BB = base.Shape.BoundBox + + if FACES[m] is not False: + isHole = False + if obj.HandleMultipleFeatures == 'Collectively': + cont = True + fsL = list() # face shape list + ifL = list() # avoid shape list + outFCS = list() + + # Get collective envelope slice of selected faces + for (fcshp, fcIdx) in FACES[m]: + fNum = fcIdx + 1 + fsL.append(fcshp) + gFW = self._getFaceWires(base, fcshp, fcIdx) + if gFW is False: + PathLog.debug('Failed to get wires from Face{}'.format(fNum)) + elif gFW[0] is False: + PathLog.debug('Cannot process Face{}. Check that it has horizontal surface exposure.'.format(fNum)) + else: + ((otrFace, raised), intWires) = gFW + outFCS.append(otrFace) + if obj.InternalFeaturesCut is False: + if intWires is not False: + for (iFace, rsd) in intWires: + ifL.append(iFace) + + PathLog.debug('Attempting to get cross-section of collective faces.') + if len(outFCS) == 0: + PathLog.error('Cannot process selected faces. Check horizontal surface exposure.'.format(fNum)) + cont = False + else: + cfsL = Part.makeCompound(outFCS) + + # Handle profile edges request + if cont is True and obj.ProfileEdges != 'None': + ofstVal = self._calculateOffsetValue(obj, isHole) + psOfst = self._extractFaceOffset(obj, cfsL, ofstVal) + if psOfst is not False: + mPS = [psOfst] + if obj.ProfileEdges == 'Only': + mFS = True + cont = False + else: + PathLog.error(' -Failed to create profile geometry for selected faces.') + cont = False + + if cont is True: + if self.showDebugObjects is True: + T = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCollectiveShape') + T.Shape = cfsL + T.purgeTouched() + self.tempGroup.addObject(T) + + ofstVal = self._calculateOffsetValue(obj, isHole) + faceOfstShp = self._extractFaceOffset(obj, cfsL, ofstVal) + if faceOfstShp is False: + PathLog.error(' -Failed to create offset face.') + cont = False + + if cont is True: + lenIfL = len(ifL) + if obj.InternalFeaturesCut is False: + if lenIfL == 0: + PathLog.debug(' -No internal features saved.') + else: + if lenIfL == 1: + casL = ifL[0] + else: + casL = Part.makeCompound(ifL) + if self.showDebugObjects is True: + C = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCompoundIntFeat') + C.Shape = casL + C.purgeTouched() + self.tempGroup.addObject(C) + ofstVal = self._calculateOffsetValue(obj, isHole=True) + intOfstShp = self._extractFaceOffset(obj, casL, ofstVal) + mIFS.append(intOfstShp) + # faceOfstShp = faceOfstShp.cut(intOfstShp) + + mFS = [faceOfstShp] + # Eif + + elif obj.HandleMultipleFeatures == 'Individually': + for (fcshp, fcIdx) in FACES[m]: + cont = True + fsL = list() # face shape list + ifL = list() # avoid shape list + fNum = fcIdx + 1 + outerFace = False + + gFW = self._getFaceWires(base, fcshp, fcIdx) + if gFW is False: + PathLog.debug('Failed to get wires from Face{}'.format(fNum)) + cont = False + elif gFW[0] is False: + PathLog.debug('Cannot process Face{}. Check that it has horizontal surface exposure.'.format(fNum)) + cont = False + outerFace = False + else: + ((otrFace, raised), intWires) = gFW + outerFace = otrFace + if obj.InternalFeaturesCut is False: + if intWires is not False: + for (iFace, rsd) in intWires: + ifL.append(iFace) + + if outerFace is not False: + PathLog.debug('Attempting to create offset face of Face{}'.format(fNum)) + + if obj.ProfileEdges != 'None': + ofstVal = self._calculateOffsetValue(obj, isHole) + psOfst = self._extractFaceOffset(obj, outerFace, ofstVal) + if psOfst is not False: + if mPS is False: + mPS = list() + mPS.append(psOfst) + if obj.ProfileEdges == 'Only': + if mFS is False: + mFS = list() + mFS.append(True) + cont = False + else: + PathLog.error(' -Failed to create profile geometry for Face{}.'.format(fNum)) + cont = False + + if cont is True: + ofstVal = self._calculateOffsetValue(obj, isHole) + faceOfstShp = self._extractFaceOffset(obj, outerFace, ofstVal) + + lenIfl = len(ifL) + if obj.InternalFeaturesCut is False and lenIfl > 0: + if lenIfl == 1: + casL = ifL[0] + else: + casL = Part.makeCompound(ifL) + + ofstVal = self._calculateOffsetValue(obj, isHole=True) + intOfstShp = self._extractFaceOffset(obj, casL, ofstVal) + mIFS.append(intOfstShp) + # faceOfstShp = faceOfstShp.cut(intOfstShp) + + if mFS is False: + mFS = list() + mFS.append(faceOfstShp) + # Eif + # Efor + # Eif + # Eif + + if len(mIFS) > 0: + if mVS is False: + mVS = list() + for ifs in mIFS: + mVS.append(ifs) + + if VOIDS[m] is not False: + PathLog.debug('Processing avoid faces.') + cont = True + isHole = False + outFCS = list() + intFEAT = list() + + for (fcshp, fcIdx) in VOIDS[m]: + fNum = fcIdx + 1 + gFW = self._getFaceWires(base, fcshp, fcIdx) + if gFW is False: + PathLog.debug('Failed to get wires from avoid Face{}'.format(fNum)) + cont = False + else: + ((otrFace, raised), intWires) = gFW + outFCS.append(otrFace) + if obj.AvoidLastX_InternalFeatures is False: + if intWires is not False: + for (iFace, rsd) in intWires: + intFEAT.append(iFace) + + lenOtFcs = len(outFCS) + if lenOtFcs == 0: + cont = False + else: + if lenOtFcs == 1: + avoid = outFCS[0] + else: + avoid = Part.makeCompound(outFCS) + + if self.showDebugObjects is True: + PathLog.debug('*** tmpAvoidArea') + P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpVoidEnvelope') + P.Shape = avoid + # P.recompute() + P.purgeTouched() + self.tempGroup.addObject(P) + + if cont is True: + if self.showDebugObjects is True: + PathLog.debug('*** tmpVoidCompound') + P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpVoidCompound') + P.Shape = avoid + # P.recompute() + P.purgeTouched() + self.tempGroup.addObject(P) + ofstVal = self._calculateOffsetValue(obj, isHole, isVoid=True) + avdOfstShp = self._extractFaceOffset(obj, avoid, ofstVal) + if avdOfstShp is False: + PathLog.error('Failed to create collective offset avoid face.') + cont = False + + if cont is True: + avdShp = avdOfstShp + + if obj.AvoidLastX_InternalFeatures is False and len(intFEAT) > 0: + if len(intFEAT) > 1: + ifc = Part.makeCompound(intFEAT) + else: + ifc = intFEAT[0] + ofstVal = self._calculateOffsetValue(obj, isHole=True) + ifOfstShp = self._extractFaceOffset(obj, ifc, ofstVal) + if ifOfstShp is False: + PathLog.error('Failed to create collective offset avoid internal features.') + else: + avdShp = avdOfstShp.cut(ifOfstShp) + + if mVS is False: + mVS = list() + mVS.append(avdShp) + + + return (mFS, mVS, mPS) + + def _getFaceWires(self, base, fcshp, fcIdx): + outFace = False + INTFCS = list() + fNum = fcIdx + 1 + # preProcEr = translate('PathSurface', 'Error pre-processing Face') + warnFinDep = translate('PathSurface', 'Final Depth might need to be lower. Internal features detected in Face') + + PathLog.debug('_getFaceWires() from Face{}'.format(fNum)) + WIRES = self._extractWiresFromFace(base, fcshp) + if WIRES is False: + PathLog.error('Failed to extract wires from Face{}'.format(fNum)) + return False + + # Process remaining internal features, adding to FCS list + lenW = len(WIRES) + for w in range(0, lenW): + (wire, rsd) = WIRES[w] + PathLog.debug('Processing Wire{} in Face{}. isRaised: {}'.format(w + 1, fNum, rsd)) + if wire.isClosed() is False: + PathLog.debug(' -wire is not closed.') + else: + slc = self._flattenWireToFace(wire) + if slc is False: + PathLog.error('FAILED to identify horizontal exposure on Face{}.'.format(fNum)) + else: + if w == 0: + outFace = (slc, rsd) + else: + # add to VOIDS so cutter avoids area. + PathLog.warning(warnFinDep + str(fNum) + '.') + INTFCS.append((slc, rsd)) + if len(INTFCS) == 0: + return (outFace, False) + else: + return (outFace, INTFCS) + + def _preProcessEntireBase(self, obj, base, m): + cont = True + isHole = False + prflShp = False + # Create envelope, extract cross-section and make offset co-planar shape + # baseEnv = PathUtils.getEnvelope(base.Shape, subshape=None, depthparams=self.depthParams) + + try: + baseEnv = PathUtils.getEnvelope(partshape=base.Shape, subshape=None, depthparams=self.depthParams) # Produces .Shape + except Exception as ee: + PathLog.error(str(ee)) + shell = base.Shape.Shells[0] + solid = Part.makeSolid(shell) + try: + baseEnv = PathUtils.getEnvelope(partshape=solid, subshape=None, depthparams=self.depthParams) # Produces .Shape + except Exception as eee: + PathLog.error(str(eee)) + cont = False + time.sleep(0.2) + + if cont is True: + csFaceShape = self._getShapeSlice(baseEnv) + if csFaceShape is False: + PathLog.debug('_getShapeSlice(baseEnv) failed') + csFaceShape = self._getCrossSection(baseEnv) + if csFaceShape is False: + PathLog.debug('_getCrossSection(baseEnv) failed') + csFaceShape = self._getSliceFromEnvelope(baseEnv) + if csFaceShape is False: + PathLog.error('Failed to slice baseEnv shape.') + cont = False + + if cont is True and obj.ProfileEdges != 'None': + PathLog.debug(' -Attempting profile geometry for model base.') + ofstVal = self._calculateOffsetValue(obj, isHole) + psOfst = self._extractFaceOffset(obj, csFaceShape, ofstVal) + if psOfst is not False: + if obj.ProfileEdges == 'Only': + return (True, psOfst) + prflShp = psOfst + else: + PathLog.error(' -Failed to create profile geometry.') + cont = False + + if cont is True: + ofstVal = self._calculateOffsetValue(obj, isHole) + faceOffsetShape = self._extractFaceOffset(obj, csFaceShape, ofstVal) + if faceOffsetShape is False: + PathLog.error('_extractFaceOffset() failed.') + else: + faceOffsetShape.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - faceOffsetShape.BoundBox.ZMin)) + return (faceOffsetShape, prflShp) + return False + + def _extractWiresFromFace(self, base, fc): + '''_extractWiresFromFace(base, fc) ... + Attempts to return all closed wires within a parent face, including the outer most wire of the parent. + The wires are ordered by area. Each wire is also categorized as a pocket(False) or raised protrusion(True). + ''' + PathLog.debug('_extractWiresFromFace()') + + WIRES = list() + lenWrs = len(fc.Wires) + PathLog.debug(' -Wire count: {}'.format(lenWrs)) + + def index0(tup): + return tup[0] + + # Cycle through wires in face + for w in range(0, lenWrs): + PathLog.debug(' -Analyzing wire_{}'.format(w + 1)) + wire = fc.Wires[w] + checkEdges = False + cont = True + + # Check for closed edges (circles, ellipses, etc...) + for E in wire.Edges: + if E.isClosed() is True: + checkEdges = True + break + + if checkEdges is True: + PathLog.debug(' -checkEdges is True') + for e in range(0, len(wire.Edges)): + edge = wire.Edges[e] + if edge.isClosed() is True and edge.Mass > 0.01: + PathLog.debug(' -Found closed edge') + raised = False + ip = self._isPocket(base, fc, edge) + if ip is False: + raised = True + ebb = edge.BoundBox + eArea = ebb.XLength * ebb.YLength + F = Part.Face(Part.Wire([edge])) + WIRES.append((eArea, F.Wires[0], raised)) + cont = False + + if cont is True: + PathLog.debug(' -cont is True') + # If only one wire and not checkEdges, return first wire + if lenWrs == 1: + return [(wire, False)] + + raised = False + wbb = wire.BoundBox + wArea = wbb.XLength * wbb.YLength + if w > 0: + ip = self._isPocket(base, fc, wire) + if ip is False: + raised = True + WIRES.append((wArea, Part.Wire(wire.Edges), raised)) + + nf = len(WIRES) + if nf > 0: + PathLog.debug(' -number of wires found is {}'.format(nf)) + if nf == 1: + (area, W, raised) = WIRES[0] + return [(W, raised)] + else: + sortedWIRES = sorted(WIRES, key=index0, reverse=True) + return [(W, raised) for (area, W, raised) in sortedWIRES] # outer, then inner by area size + + return False + + def _calculateOffsetValue(self, obj, isHole, isVoid=False): + '''_calculateOffsetValue(obj, isHole, isVoid) ... internal function. + Calculate the offset for the Path.Area() function.''' + JOB = PathUtils.findParentJob(obj) + tolrnc = JOB.GeometryTolerance.Value + + if isVoid is False: + if isHole is True: + offset = -1 * obj.InternalFeaturesAdjustment.Value + offset += self.radius # (self.radius + (tolrnc / 10.0)) + else: + offset = -1 * obj.BoundaryAdjustment.Value + if obj.BoundaryEnforcement is True: + offset += self.radius # (self.radius + (tolrnc / 10.0)) + else: + offset -= self.radius # (self.radius + (tolrnc / 10.0)) + offset = 0.0 - offset + else: + offset = -1 * obj.BoundaryAdjustment.Value + offset += self.radius # (self.radius + (tolrnc / 10.0)) + + return offset + + def _extractFaceOffset(self, obj, fcShape, offset): + '''_extractFaceOffset(fcShape, offset) ... internal function. + Original _buildPathArea() version copied from PathAreaOp.py module. This version is modified. + Adjustments made based on notes by @sliptonic at this webpage: https://github.com/sliptonic/FreeCAD/wiki/PathArea-notes.''' + PathLog.debug('_extractFaceOffset()') + + if fcShape.BoundBox.ZMin != 0.0: + fcShape.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - fcShape.BoundBox.ZMin)) + + areaParams = {} + areaParams['Offset'] = offset + areaParams['Fill'] = 1 + areaParams['Coplanar'] = 0 + areaParams['SectionCount'] = 1 # -1 = full(all per depthparams??) sections + areaParams['Reorient'] = True + areaParams['OpenMode'] = 0 + areaParams['MaxArcPoints'] = 400 # 400 + areaParams['Project'] = True + + area = Path.Area() # Create instance of Area() class object + # area.setPlane(PathUtils.makeWorkplane(fcShape)) # Set working plane + area.setPlane(PathUtils.makeWorkplane(self.wpc)) # Set working plane to normal at Z=1 + area.add(fcShape) + area.setParams(**areaParams) # set parameters + + # Save parameters for debugging + # obj.AreaParams = str(area.getParams()) + # PathLog.debug("Area with params: {}".format(area.getParams())) + + offsetShape = area.getShape() + wCnt = len(offsetShape.Wires) + if wCnt == 0: + return False + elif wCnt == 1: + ofstFace = Part.Face(offsetShape.Wires[0]) + else: + W = list() + for wr in offsetShape.Wires: + W.append(Part.Face(wr)) + ofstFace = Part.makeCompound(W) + + return ofstFace # offsetShape + + def _isPocket(self, b, f, w): + '''_isPocket(b, f, w)... + Attempts to determing if the wire(w) in face(f) of base(b) is a pocket or raised protrusion. + Returns True if pocket, False if raised protrusion.''' + e = w.Edges[0] + for fi in range(0, len(b.Shape.Faces)): + face = b.Shape.Faces[fi] + for ei in range(0, len(face.Edges)): + edge = face.Edges[ei] + if e.isSame(edge) is True: + if f is face: + # Alternative: run loop to see if all edges are same + pass # same source face, look for another + else: + if face.CenterOfMass.z < f.CenterOfMass.z: + return True + return False + + def _flattenWireToFace(self, wire): + PathLog.debug('_flattenWireToFace()') + if wire.isClosed() is False: + PathLog.debug(' -wire.isClosed() is False') + return False + + # If wire is planar horizontal, convert to a face and return + if wire.BoundBox.ZLength == 0.0: + slc = Part.Face(wire) + return slc + + # Attempt to create a new wire for manipulation, if not, use original + newWire = Part.Wire(wire.Edges) + if newWire.isClosed() is True: + nWire = newWire + else: + PathLog.debug(' -newWire.isClosed() is False') + nWire = wire + + # Attempt extrusion, and then try a manual slice and then cross-section + ext = self._getExtrudedShape(nWire) + if ext is False: + PathLog.debug('_getExtrudedShape() failed') + else: + slc = self._getShapeSlice(ext) + if slc is not False: + return slc + cs = self._getCrossSection(ext, True) + if cs is not False: + return cs + + # Attempt creating an envelope, and then try a manual slice and then cross-section + env = self._getShapeEnvelope(nWire) + if env is False: + PathLog.debug('_getShapeEnvelope() failed') + else: + slc = self._getShapeSlice(env) + if slc is not False: + return slc + cs = self._getCrossSection(env, True) + if cs is not False: + return cs + + # Attempt creating a projection + slc = self._getProjectedFace(nWire) + if slc is False: + PathLog.debug('_getProjectedFace() failed') + else: + return slc + + return False + + def _getExtrudedShape(self, wire): + PathLog.debug('_getExtrudedShape()') + wBB = wire.BoundBox + extFwd = math.floor(2.0 * wBB.ZLength) + 10.0 + + try: + # slower, but renders collective faces correctly. Method 5 in TESTING + shell = wire.extrude(FreeCAD.Vector(0.0, 0.0, extFwd)) + except Exception as ee: + PathLog.error(' -extrude wire failed: \n{}'.format(ee)) + return False + + SHP = Part.makeSolid(shell) + return SHP + + def _getShapeSlice(self, shape): + PathLog.debug('_getShapeSlice()') + + bb = shape.BoundBox + mid = (bb.ZMin + bb.ZMax) / 2.0 + xmin = bb.XMin - 1.0 + xmax = bb.XMax + 1.0 + ymin = bb.YMin - 1.0 + ymax = bb.YMax + 1.0 + p1 = FreeCAD.Vector(xmin, ymin, mid) + p2 = FreeCAD.Vector(xmax, ymin, mid) + p3 = FreeCAD.Vector(xmax, ymax, mid) + p4 = FreeCAD.Vector(xmin, ymax, mid) + + e1 = Part.makeLine(p1, p2) + e2 = Part.makeLine(p2, p3) + e3 = Part.makeLine(p3, p4) + e4 = Part.makeLine(p4, p1) + face = Part.Face(Part.Wire([e1, e2, e3, e4])) + fArea = face.BoundBox.XLength * face.BoundBox.YLength # face.Wires[0].Area + sArea = shape.BoundBox.XLength * shape.BoundBox.YLength + midArea = (fArea + sArea) / 2.0 + + slcShp = shape.common(face) + slcArea = slcShp.BoundBox.XLength * slcShp.BoundBox.YLength + + if slcArea < midArea: + for W in slcShp.Wires: + if W.isClosed() is False: + PathLog.debug(' -wire.isClosed() is False') + return False + if len(slcShp.Wires) == 1: + wire = slcShp.Wires[0] + slc = Part.Face(wire) + slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin)) + return slc + else: + fL = list() + for W in slcShp.Wires: + slc = Part.Face(W) + slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin)) + fL.append(slc) + comp = Part.makeCompound(fL) + if self.showDebugObjects is True: + PathLog.debug('*** tmpSliceCompound') + P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpSliceCompound') + P.Shape = comp + # P.recompute() + P.purgeTouched() + self.tempGroup.addObject(P) + return comp + + PathLog.debug(' -slcArea !< midArea') + PathLog.debug(' -slcShp.Edges count: {}. Might be a vertically oriented face.'.format(len(slcShp.Edges))) + return False + + def _getProjectedFace(self, wire): + PathLog.debug('_getProjectedFace()') + F = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpProjectionWire') + F.Shape = wire + F.purgeTouched() + self.tempGroup.addObject(F) + try: + prj = Draft.makeShape2DView(F, FreeCAD.Vector(0, 0, 1)) + prj.recompute() + prj.purgeTouched() + self.tempGroup.addObject(prj) + except Exception as ee: + PathLog.error(str(ee)) + return False + else: + pWire = Part.Wire(prj.Shape.Edges) + if pWire.isClosed() is False: + # PathLog.debug(' -pWire.isClosed() is False') + return False + slc = Part.Face(pWire) + slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin)) + return slc + return False + + def _getCrossSection(self, shape, withExtrude=False): + PathLog.debug('_getCrossSection()') + wires = list() + bb = shape.BoundBox + mid = (bb.ZMin + bb.ZMax) / 2.0 + + for i in shape.slice(FreeCAD.Vector(0, 0, 1), mid): + wires.append(i) + + if len(wires) > 0: + comp = Part.Compound(wires) # produces correct cross-section wire ! + comp.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - comp.BoundBox.ZMin)) + csWire = comp.Wires[0] + if csWire.isClosed() is False: + PathLog.debug(' -comp.Wires[0] is not closed') + return False + if withExtrude is True: + ext = self._getExtrudedShape(csWire) + CS = self._getShapeSlice(ext) + else: + CS = Part.Face(csWire) + CS.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - CS.BoundBox.ZMin)) + return CS + else: + PathLog.debug(' -No wires from .slice() method') + + return False + + def _getShapeEnvelope(self, shape): + PathLog.debug('_getShapeEnvelope()') + + wBB = shape.BoundBox + extFwd = wBB.ZLength + 10.0 + minz = wBB.ZMin + maxz = wBB.ZMin + extFwd + stpDwn = (maxz - minz) / 4.0 + dep_par = PathUtils.depth_params(maxz + 5.0, maxz + 3.0, maxz, stpDwn, 0.0, minz) + + try: + env = PathUtils.getEnvelope(partshape=shape, depthparams=dep_par) # Produces .Shape + except Exception as ee: + PathLog.error('try: PathUtils.getEnvelope() failed.\n' + str(ee)) + return False + else: + return env + + return False + + def _getSliceFromEnvelope(self, env): + PathLog.debug('_getSliceFromEnvelope()') + eBB = env.BoundBox + extFwd = eBB.ZLength + 10.0 + maxz = eBB.ZMin + extFwd + + maxMax = env.Edges[0].BoundBox.ZMin + emax = math.floor(maxz - 1.0) + E = list() + for e in range(0, len(env.Edges)): + emin = env.Edges[e].BoundBox.ZMin + if emin > emax: + E.append(env.Edges[e]) + tf = Part.Face(Part.Wire(Part.__sortEdges__(E))) + tf.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - tf.BoundBox.ZMin)) + + return tf + + def _prepareModelSTLs(self, JOB, obj): + PathLog.debug('_prepareModelSTLs()') + for m in range(0, len(JOB.Model.Group)): + M = JOB.Model.Group[m] + + # PathLog.debug(f" -self.modelTypes[{m}] == 'M'") + if self.modelTypes[m] == 'M': + mesh = M.Mesh + else: + # base.Shape.tessellate(0.05) # 0.5 original value + # mesh = MeshPart.meshFromShape(base.Shape, Deflection=self.deflection) + mesh = MeshPart.meshFromShape(Shape=M.Shape, LinearDeflection=self.deflection, AngularDeflection=self.angularDeflection, Relative=False) + + if self.modelSTLs[m] is True: + stl = ocl.STLSurf() + if obj.Algorithm == 'OCL Dropcutter': + for f in mesh.Facets: + p = f.Points[0] + q = f.Points[1] + r = f.Points[2] + t = ocl.Triangle(ocl.Point(p[0], p[1], p[2]), + ocl.Point(q[0], q[1], q[2]), + ocl.Point(r[0], r[1], r[2])) + stl.addTriangle(t) + self.modelSTLs[m] = stl + elif obj.Algorithm == 'OCL Waterline': + for f in mesh.Facets: + p = f.Points[0] + q = f.Points[1] + r = f.Points[2] + t = ocl.Triangle(ocl.Point(p[0], p[1], p[2] + obj.DepthOffset.Value), + ocl.Point(q[0], q[1], q[2] + obj.DepthOffset.Value), + ocl.Point(r[0], r[1], r[2] + obj.DepthOffset.Value)) + stl.addTriangle(t) + self.modelSTLs[m] = stl + return + + def _makeSafeSTL(self, JOB, obj, mdlIdx, faceShapes, voidShapes): + '''_makeSafeSTL(JOB, obj, mdlIdx, faceShapes, voidShapes)... + Creates and OCL.stl object with combined data with waste stock, + model, and avoided faces. Travel lines can be checked against this + STL object to determine minimum travel height to clear stock and model.''' + PathLog.debug('_makeSafeSTL()') + + fuseShapes = list() + Mdl = JOB.Model.Group[mdlIdx] + FCAD = FreeCAD.ActiveDocument + mBB = Mdl.Shape.BoundBox + sBB = JOB.Stock.Shape.BoundBox + + # add Model shape to safeSTL shape + fuseShapes.append(Mdl.Shape) + + if obj.BoundBox == 'BaseBoundBox': + cont = False + extFwd = (sBB.ZLength) + zmin = mBB.ZMin + zmax = mBB.ZMin + extFwd + stpDwn = (zmax - zmin) / 4.0 + dep_par = PathUtils.depth_params(zmax + 5.0, zmax + 3.0, zmax, stpDwn, 0.0, zmin) + + try: + envBB = PathUtils.getEnvelope(partshape=Mdl.Shape, depthparams=dep_par) # Produces .Shape + cont = True + except Exception as ee: + PathLog.error(str(ee)) + shell = Mdl.Shape.Shells[0] + solid = Part.makeSolid(shell) + try: + envBB = PathUtils.getEnvelope(partshape=solid, depthparams=dep_par) # Produces .Shape + cont = True + except Exception as eee: + PathLog.error(str(eee)) + + if cont is True: + stckWst = JOB.Stock.Shape.cut(envBB) + if obj.BoundaryAdjustment > 0.0: + cmpndFS = Part.makeCompound(faceShapes) + baBB = PathUtils.getEnvelope(partshape=cmpndFS, depthparams=self.depthParams) # Produces .Shape + adjStckWst = stckWst.cut(baBB) + else: + adjStckWst = stckWst + fuseShapes.append(adjStckWst) + else: + PathLog.warning('Path transitions might not avoid the model. Verify paths.') + time.sleep(0.3) + + else: + # If boundbox is Job.Stock, add hidden pad under stock as base plate + toolDiam = self.cutter.getDiameter() + zMin = JOB.Stock.Shape.BoundBox.ZMin + xMin = JOB.Stock.Shape.BoundBox.XMin - toolDiam + yMin = JOB.Stock.Shape.BoundBox.YMin - toolDiam + bL = JOB.Stock.Shape.BoundBox.XLength + (2 * toolDiam) + bW = JOB.Stock.Shape.BoundBox.YLength + (2 * toolDiam) + bH = 1.0 + crnr = FreeCAD.Vector(xMin, yMin, zMin - 1.0) + B = Part.makeBox(bL, bW, bH, crnr, FreeCAD.Vector(0, 0, 1)) + fuseShapes.append(B) + + if voidShapes is not False: + voidComp = Part.makeCompound(voidShapes) + voidEnv = PathUtils.getEnvelope(partshape=voidComp, depthparams=self.depthParams) # Produces .Shape + fuseShapes.append(voidEnv) + + f0 = fuseShapes.pop(0) + if len(fuseShapes) > 0: + fused = f0.fuse(fuseShapes) + else: + fused = f0 + + if self.showDebugObjects is True: + T = FreeCAD.ActiveDocument.addObject('Part::Feature', 'safeSTLShape') + T.Shape = fused + T.purgeTouched() + self.tempGroup.addObject(T) + + # Extract mesh from fusion + meshFuse = MeshPart.meshFromShape(Shape=fused, LinearDeflection=(self.deflection / 2.0), AngularDeflection=self.angularDeflection, Relative=False) + time.sleep(0.2) + stl = ocl.STLSurf() + for f in meshFuse.Facets: + p = f.Points[0] + q = f.Points[1] + r = f.Points[2] + t = ocl.Triangle(ocl.Point(p[0], p[1], p[2]), + ocl.Point(q[0], q[1], q[2]), + ocl.Point(r[0], r[1], r[2])) + stl.addTriangle(t) + + self.safeSTLs[mdlIdx] = stl + + 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 Algorithm and ScanType properties.''' + PathLog.debug('_processCutAreas()') + + final = list() + base = JOB.Model.Group[mdlIdx] + + # Process faces Collectively or Individually + 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.Algorithm == 'OCL Waterline': + final.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})) + final.extend(self._waterlineOp(JOB, obj, mdlIdx, COMP)) # independent method set for Waterline + elif obj.ScanType == 'Planar': + final.extend(self._processPlanarOp(JOB, obj, mdlIdx, COMP, 0)) + elif obj.ScanType == 'Rotational': + final.extend(self._processRotationalOp(obj, base, COMP)) + + elif obj.HandleMultipleFeatures == 'Individually': + for fsi in range(0, len(FCS)): + fShp = FCS[fsi] + # self.deleteOpVariables(all=False) + 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.Algorithm == 'OCL Waterline': + final.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})) + final.extend(self._waterlineOp(JOB, obj, mdlIdx, COMP)) # independent method set for Waterline + elif 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 + # Eif + + return final + + # Methods for creating path geometry + 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. ''' + PathLog.debug('_processPlanarOp()') + final = list() + SCANDATA = list() + # base = JOB.Model.Group[mdlIdx] # Compute number and size of stepdowns, and final depth if obj.LayerMode == 'Single-pass': depthparams = [obj.FinalDepth.Value] - else: - dep_par = PathUtils.depth_params(obj.ClearanceHeight.Value, obj.SafeHeight.Value, obj.StartDepth.Value, obj.StepDown.Value, 0.0, obj.FinalDepth.Value) - depthparams = [i for i in dep_par] - # self.reportThis("--depthparams:" + str(depthparams)) + elif obj.LayerMode == 'Multi-pass': + depthparams = [i for i in self.depthParams] lenDP = len(depthparams) - prevDepth = depthparams[0] - # Determine bounding box length - bbLength = bb.YLength - if obj.DropCutterDir == 'Y': - bbLength = bb.XLength + # Prepare PathDropCutter objects with STL data + pdc = self._planarGetPDC(self.modelSTLs[mdlIdx], depthparams[lenDP - 1], obj.SampleInterval.Value) + safePDC = self._planarGetPDC(self.safeSTLs[mdlIdx], + depthparams[lenDP - 1], obj.SampleInterval.Value, useSafeCutter=False) - # Determine number of lines for OCL scan - if obj.DropCutterDir == 'X': - exOff = obj.DropCutterExtraOffset.y - else: - exOff = obj.DropCutterExtraOffset.x - numLines = int(math.ceil((bbLength + (2 * exOff)) / self.cutOut)) # Number of lines + profScan = list() + if obj.ProfileEdges != 'None': + prflShp = self.profileShapes[mdlIdx][fsi] + if prflShp is False: + PathLog.error('No profile shape is False.') + return list() + if self.showDebugObjects is True: + P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpNewProfileShape') + P.Shape = prflShp + # P.recompute() + P.purgeTouched() + self.tempGroup.addObject(P) + # get offset path geometry and perform OCL scan with that geometry + pathOffsetGeom = self._planarMakeProfileGeom(obj, prflShp) + if pathOffsetGeom is False: + PathLog.error('No profile geometry returned.') + return list() + profScan = [self._planarPerformOclScan(obj, pdc, pathOffsetGeom, offsetPoints=True)] - # Scan the piece to depth - scanCLP = self._planarDropCutScan(obj, stl, bbLength, xmin, ymin, xmax, ymax, depthparams[lenDP - 1], numLines, self.cutOut) + geoScan = list() + if obj.ProfileEdges != 'Only': + if self.showDebugObjects is True: + F = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCutArea') + F.Shape = cmpdShp + # F.recompute() + F.purgeTouched() + self.tempGroup.addObject(F) + # get internal path geometry and perform OCL scan with that geometry + pathGeom = self._planarMakePathGeom(obj, cmpdShp) + if pathGeom is False: + PathLog.error('No path geometry returned.') + return list() + geoScan = self._planarPerformOclScan(obj, pdc, pathGeom, offsetPoints=False) + + if obj.ProfileEdges == 'Only': # ['None', 'Only', 'First', 'Last'] + SCANDATA.extend(profScan) + if obj.ProfileEdges == 'None': + SCANDATA.extend(geoScan) + if obj.ProfileEdges == 'First': + SCANDATA.extend(profScan) + SCANDATA.extend(geoScan) + if obj.ProfileEdges == 'Last': + SCANDATA.extend(geoScan) + SCANDATA.extend(profScan) # Apply depth offset - if obj.DepthOffset.Value != 0: - self.reportThis("--Applying DepthOffset") - for pt in range(0, len(scanCLP)): - scanCLP[pt].z += obj.DepthOffset.Value + if obj.DepthOffset.Value != 0.0: + self._planarApplyDepthOffset(SCANDATA, obj.DepthOffset.Value) - numPts = len(scanCLP) - pntsPerLine = numPts / numLines - # self.reportThis("--points: " + str(numPts)) - # self.reportThis("--lines: " + str(numLines)) - # self.reportThis("--pntsPerLine: " + str(pntsPerLine)) - if math.ceil(pntsPerLine) != math.floor(pntsPerLine): - pntsPerLine = None + if len(SCANDATA) == 0: + PathLog.error('No scan data to convert to Gcode.') + return list() + + # If cut pattern is `Circular`, there are zero(almost zero) straight lines to optimize + # Store initial `OptimizeLinearPaths` value for later restoration + self.preOLP = obj.OptimizeLinearPaths + if obj.CutPattern in ['Circular', 'CircularZigZag']: + obj.OptimizeLinearPaths = False + + # Process OCL scan data + 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 cut pattern is `Circular`, restore initial OLP value + if obj.CutPattern in ['Circular', 'CircularZigZag']: + obj.OptimizeLinearPaths = self.preOLP + + # Raise to safe height between individual faces. + if obj.HandleMultipleFeatures == 'Individually': + final.insert(0, Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})) + + return final + + def _planarMakePathGeom(self, obj, faceShp): + '''_planarMakePathGeom(obj, faceShp)... + Creates the line/arc cut pattern geometry and returns the intersection with the received faceShp. + The resulting intersecting line/arc geometries are then converted to lines or arcs for OCL.''' + PathLog.debug('_planarMakePathGeom()') + GeoSet = list() + + # Apply drop cutter extra offset and set the max and min XY area of the operation + xmin = faceShp.BoundBox.XMin + xmax = faceShp.BoundBox.XMax + ymin = faceShp.BoundBox.YMin + ymax = faceShp.BoundBox.YMax + zmin = faceShp.BoundBox.ZMin + zmax = faceShp.BoundBox.ZMax + + # Compute weighted center of mass of all faces combined + fCnt = 0 + totArea = 0.0 + zeroCOM = FreeCAD.Vector(0.0, 0.0, 0.0) + for F in faceShp.Faces: + comF = F.CenterOfMass + areaF = F.Area + totArea += areaF + fCnt += 1 + zeroCOM = zeroCOM.add(FreeCAD.Vector(comF.x, comF.y, 0.0).multiply(areaF)) + if fCnt == 0: + PathLog.error(translate('PathSurface', 'Cannot calculate the Center Of Mass. Using Center of Boundbox.')) + zeroCOM = FreeCAD.Vector((xmin + xmax) / 2.0, (ymin + ymax) / 2.0, 0.0) else: - pntsPerLine = math.ceil(pntsPerLine) + avgArea = totArea / fCnt + zeroCOM.multiply(1 / fCnt) + zeroCOM.multiply(1 / avgArea) + COM = FreeCAD.Vector(zeroCOM.x, zeroCOM.y, 0.0) - # Create topo map for ignoring waste material - if ignoreWasteFlag is True and pntsPerLine is not None: - topoMap = createTopoMap(scanCLP, obj.IgnoreWasteDepth) - self.topoMap = listToMultiDimensional(topoMap, numLines, pntsPerLine) - self._bufferTopoMap(numLines, pntsPerLine) - self._highlightWaterline(4, 1) - self.topoMap = debufferMultiDimenList(self.topoMap) - ignoreMap = multiDimensionalToList(self.topoMap) + # get X, Y, Z spans; Compute center of rotation + deltaX = abs(xmax-xmin) + deltaY = abs(ymax-ymin) + deltaZ = abs(zmax-zmin) + deltaC = math.sqrt(deltaX**2 + deltaY**2) + lineLen = deltaC + (2.0 * self.cutter.getDiameter()) # Line length to span boundbox diag with 2x cutter diameter extra on each end + halfLL = math.ceil(lineLen / 2.0) + cutPasses = math.ceil(lineLen / self.cutOut) + 1 # Number of lines(passes) required to cover lineLen + halfPasses = math.ceil(cutPasses / 2.0) + bbC = faceShp.BoundBox.Center - # Extract layers per depthparams - for lyr in range(0, lenDP): - # Convert current layer data to gcode - self._planarScanToGcode(obj, lyr, prevDepth, depthparams[lyr], scanCLP, pntsPerLine, ignoreMap) - prevDepth = depthparams[lyr] + # Generate the Draft line/circle sets to be intersected with the cut-face-area + if obj.CutPattern in ['ZigZag', 'Line']: + MaxLC = -1 + centRot = FreeCAD.Vector(0.0, 0.0, 0.0) # Bottom left corner of face/selection/model + cAng = math.atan(deltaX / deltaY) # BoundaryBox angle - commands = self._processPlanarHolds(obj, scanCLP) - # self.reportThis("--Elapsed time after processing gcode holds is " + str(time.time() - t_before) + " s") # self.keepTime - return commands + # Determine end points and create top lines + x1 = centRot.x - halfLL + x2 = centRot.x + halfLL + diag = None + if obj.CutPatternAngle == 0 or obj.CutPatternAngle == 180: + MaxLC = math.floor(deltaY / self.cutOut) + diag = deltaY + elif obj.CutPatternAngle == 90 or obj.CutPatternAngle == 270: + MaxLC = math.floor(deltaX / self.cutOut) + diag = deltaX + else: + perpDist = math.cos(cAng - math.radians(obj.CutPatternAngle)) * deltaC + MaxLC = math.floor(perpDist / self.cutOut) + diag = perpDist + y1 = centRot.y + diag + # y2 = y1 - def _planarDropCutScan(self, obj, stl, bbLength, xmin, ymin, xmax, ymax, fd, Nl, cOut): - t_before = time.time() + p1 = FreeCAD.Vector(x1, y1, 0.0) + p2 = FreeCAD.Vector(x2, y1, 0.0) + topLineTuple = (p1, p2) + ny1 = centRot.y - diag + n1 = FreeCAD.Vector(x1, ny1, 0.0) + n2 = FreeCAD.Vector(x2, ny1, 0.0) + negTopLineTuple = (n1, n2) - def cutPatternLine(obj, n, p1, p2): - if obj.CutPattern == 'ZigZag': - if (n % 2 == 0.0): # even - lo = ocl.Line(p1, p2) # line-object - else: # odd - lo = ocl.Line(p2, p1) # line-object - elif obj.CutPattern == 'Line': - if obj.CutMode == 'Conventional': - lo = ocl.Line(p1, p2) # line-object - else: # odd - lo = ocl.Line(p2, p1) # line-object - return lo + # Create end points for set of lines to intersect with cross-section face + pntTuples = list() + for lc in range((-1 * (halfPasses - 1)), halfPasses + 1): + # if lc == (cutPasses - MaxLC - 1): + # pntTuples.append(negTopLineTuple) + # if lc == (MaxLC + 1): + # pntTuples.append(topLineTuple) + x1 = centRot.x - halfLL + x2 = centRot.x + halfLL + y1 = centRot.y + (lc * self.cutOut) + # y2 = y1 + p1 = FreeCAD.Vector(x1, y1, 0.0) + p2 = FreeCAD.Vector(x2, y1, 0.0) + pntTuples.append( (p1, p2) ) - pdc = ocl.PathDropCutter() # create a pdc - pdc.setSTL(stl) - pdc.setCutter(self.cutter) - pdc.setZ(fd) # set minimumZ (final / target depth value) - pdc.setSampling(obj.SampleInterval) + # Convert end points to lines + for (p1, p2) in pntTuples: + line = Part.makeLine(p1, p2) + GeoSet.append(line) + elif obj.CutPattern in ['Circular', 'CircularZigZag']: + zTgt = faceShp.BoundBox.ZMin + axisRot = FreeCAD.Vector(0.0, 0.0, 1.0) + cntr = FreeCAD.Placement() + cntr.Rotation = FreeCAD.Rotation(axisRot, 0.0) + if obj.CircularCenterAt == 'CenterOfMass': + cntr.Base = FreeCAD.Vector(COM.x, COM.y, zTgt) # COM # Use center of Mass + elif obj.CircularCenterAt == 'CenterOfBoundBox': + cent = faceShp.BoundBox.Center + cntr.Base = FreeCAD.Vector(cent.x, cent.y, zTgt) + elif obj.CircularCenterAt == 'XminYmin': + cntr.Base = FreeCAD.Vector(faceShp.BoundBox.XMin, faceShp.BoundBox.YMin, zTgt) + elif obj.CircularCenterAt == 'Custom': + newCent = FreeCAD.Vector(obj.CircularCenterCustom.x, obj.CircularCenterCustom.y, zTgt) + cntr.Base = newCent + + # recalculate cutPasses value, if need be + radialPasses = halfPasses + if obj.CircularCenterAt != 'CenterOfBoundBox': + # make 4 corners of boundbox in XY plane, find which is greatest distance to new circular center + EBB = faceShp.BoundBox + CORNERS = [ + FreeCAD.Vector(EBB.XMin, EBB.YMin, 0.0), + FreeCAD.Vector(EBB.XMin, EBB.YMax, 0.0), + FreeCAD.Vector(EBB.XMax, EBB.YMax, 0.0), + FreeCAD.Vector(EBB.XMax, EBB.YMin, 0.0), + ] + dMax = 0.0 + for c in range(0, 4): + dist = CORNERS[c].sub(cntr.Base).Length + if dist > dMax: + dMax = dist + lineLen = dMax + (2.0 * self.cutter.getDiameter()) # Line length to span boundbox diag with 2x cutter diameter extra on each end + radialPasses = math.ceil(lineLen / self.cutOut) + 1 # Number of lines(passes) required to cover lineLen + + # Update COM point and current CircularCenter + if obj.CircularCenterAt != 'Custom': + obj.CircularCenterCustom = cntr.Base + + minRad = self.cutter.getDiameter() * 0.45 + siX3 = 3 * obj.SampleInterval.Value + minRadSI = (siX3 / 2.0) / math.pi + if minRad < minRadSI: + minRad = minRadSI + + # Make small center circle to start pattern + if obj.StepOver > 50: + circle = Part.makeCircle(minRad, cntr.Base) + GeoSet.append(circle) + + for lc in range(1, radialPasses + 1): + rad = (lc * self.cutOut) + if rad >= minRad: + circle = Part.makeCircle(rad, cntr.Base) + GeoSet.append(circle) + # Efor + COM = cntr.Base + # Eif + + if obj.CutPatternReversed is True: + GeoSet.reverse() + + if faceShp.BoundBox.ZMin != 0.0: + faceShp.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - faceShp.BoundBox.ZMin)) + + # Create compound object to bind all lines in Lineset + geomShape = Part.makeCompound(GeoSet) + + # Position and rotate the Line and ZigZag geometry + if obj.CutPattern in ['Line', 'ZigZag']: + if obj.CutPatternAngle != 0.0: + geomShape.Placement.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), obj.CutPatternAngle) + geomShape.Placement.Base = FreeCAD.Vector(bbC.x, bbC.y, 0.0 - geomShape.BoundBox.ZMin) + + if self.showDebugObjects is True: + F = FreeCAD.ActiveDocument.addObject('Part::Feature','tmpGeometrySet') + F.Shape = geomShape + F.purgeTouched() + self.tempGroup.addObject(F) + + # Identify intersection of cross-section face and lineset + cmnShape = faceShp.common(geomShape) + + if self.showDebugObjects is True: + F = FreeCAD.ActiveDocument.addObject('Part::Feature','tmpPathGeometry') + F.Shape = cmnShape + F.purgeTouched() + self.tempGroup.addObject(F) + + self.tmpCOM = FreeCAD.Vector(COM.x, COM.y, faceShp.BoundBox.ZMin) + return cmnShape + + def _planarMakeProfileGeom(self, obj, subShp): + PathLog.debug('_planarMakeProfileGeom()') + + offsetLists = list() + dist = obj.SampleInterval.Value / 5.0 + defl = obj.SampleInterval.Value / 5.0 + + # Reference https://forum.freecadweb.org/viewtopic.php?t=28861#p234939 + for fc in subShp.Faces: + # Reverse order of wires in each face - inside to outside + for w in range(len(fc.Wires) - 1, -1, -1): + W = fc.Wires[w] + PNTS = W.discretize(Distance=dist) + # PNTS = W.discretize(Deflection=defl) + if self.CutClimb is True: + PNTS.reverse() + offsetLists.append(PNTS) + + return offsetLists + + def _planarPerformOclScan(self, obj, pdc, pathGeom, offsetPoints=False): + '''_planarPerformOclScan(obj, pdc, pathGeom, offsetPoints=False)... + Switching fuction for calling the appropriate path-geometry to OCL points conversion fucntion + for the various cut patterns.''' + PathLog.debug('_planarPerformOclScan()') + SCANS = list() + + if offsetPoints is True: + PNTSET = self._pathGeomToOffsetPointSet(obj, pathGeom) + for D in PNTSET: + stpOvr = list() + ofst = list() + for I in D: + if I == 'BRK': + stpOvr.append(ofst) + stpOvr.append(I) + ofst = list() + else: + # D format is ((p1, p2), (p3, p4)) + (A, B) = I + ofst.extend(self._planarDropCutScan(pdc, A, B)) + if len(ofst) > 0: + stpOvr.append(ofst) + SCANS.extend(stpOvr) + elif obj.CutPattern == 'Line': + stpOvr = list() + PNTSET = self._pathGeomToLinesPointSet(obj, pathGeom) + for D in PNTSET: + for I in D: + if I == 'BRK': + stpOvr.append(I) + else: + # D format is ((p1, p2), (p3, p4)) + (A, B) = I + stpOvr.append(self._planarDropCutScan(pdc, A, B)) + SCANS.append(stpOvr) + stpOvr = list() + elif obj.CutPattern == 'ZigZag': + stpOvr = list() + PNTSET = self._pathGeomToZigzagPointSet(obj, pathGeom) + for (dirFlg, LNS) in PNTSET: + for SEG in LNS: + if SEG == 'BRK': + stpOvr.append(SEG) + else: + # D format is ((p1, p2), (p3, p4)) + (A, B) = SEG + stpOvr.append(self._planarDropCutScan(pdc, A, B)) + SCANS.append(stpOvr) + stpOvr = list() + elif obj.CutPattern in ['Circular', 'CircularZigZag']: + # PNTSET is list, by stepover. + # Each stepover is a list containing arc/loop descriptions, (sp, ep, cp) + PNTSET = self._pathGeomToArcPointSet(obj, pathGeom) + + for so in range(0, len(PNTSET)): + stpOvr = list() + erFlg = False + (aTyp, dirFlg, ARCS) = PNTSET[so] + + if dirFlg == 1: # 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 _pathGeomToOffsetPointSet(self, obj, compGeoShp): + '''_pathGeomToOffsetPointSet(obj, compGeoShp)... + Convert a compound set of 3D profile segmented wires to 2D segments, applying linear optimization.''' + PathLog.debug('_pathGeomToOffsetPointSet()') + + LINES = list() + optimize = obj.OptimizeLinearPaths + ofstCnt = len(compGeoShp) + + # Cycle through offeset loops + for ei in range(0, ofstCnt): + OS = compGeoShp[ei] + lenOS = len(OS) + + if ei > 0: + LINES.append('BRK') + + fp = FreeCAD.Vector(OS[0].x, OS[0].y, OS[0].z) + OS.append(fp) + + # Cycle through points in each loop + prev = OS[0] + pnt = OS[1] + for v in range(1, lenOS): + nxt = OS[v + 1] + if optimize is True: + iPOL = self.isPointOnLine(prev, nxt, pnt) + if iPOL is True: + pnt = nxt + else: + tup = ((prev.x, prev.y), (pnt.x, pnt.y)) + LINES.append(tup) + prev = pnt + pnt = nxt + else: + tup = ((prev.x, prev.y), (pnt.x, pnt.y)) + LINES.append(tup) + prev = pnt + pnt = nxt + if iPOL is True: + tup = ((prev.x, prev.y), (pnt.x, pnt.y)) + LINES.append(tup) + # Efor + + return [LINES] + + def _pathGeomToLinesPointSet(self, obj, compGeoShp): + '''_pathGeomToLinesPointSet(obj, compGeoShp)... + Convert a compound set of sequential line segments to directionally-oriented collinear groupings.''' + PathLog.debug('_pathGeomToLinesPointSet()') + # Extract intersection line segments for return value as list() + LINES = list() + inLine = list() + chkGap = False + lnCnt = 0 + ec = len(compGeoShp.Edges) + cutClimb = self.CutClimb + toolDiam = 2.0 * self.radius + cpa = obj.CutPatternAngle + + edg0 = compGeoShp.Edges[0] + p1 = (edg0.Vertexes[0].X, edg0.Vertexes[0].Y) + p2 = (edg0.Vertexes[1].X, edg0.Vertexes[1].Y) + if cutClimb is True: + tup = (p2, p1) + lst = FreeCAD.Vector(p1[0], p1[1], 0.0) + else: + tup = (p1, p2) + lst = FreeCAD.Vector(p2[0], p2[1], 0.0) + inLine.append(tup) + sp = FreeCAD.Vector(p1[0], p1[1], 0.0) # start point + + for ei in range(1, ec): + chkGap = False + edg = compGeoShp.Edges[ei] # Get edge for vertexes + v1 = (edg.Vertexes[0].X, edg.Vertexes[0].Y) # vertex 0 + v2 = (edg.Vertexes[1].X, edg.Vertexes[1].Y) # vertex 1 + + ep = FreeCAD.Vector(v2[0], v2[1], 0.0) # end point + cp = FreeCAD.Vector(v1[0], v1[1], 0.0) # check point (first / middle point) + iC = self.isPointOnLine(sp, ep, cp) + if iC is True: + inLine.append('BRK') + chkGap = True + else: + if cutClimb is True: + inLine.reverse() + LINES.append(inLine) # Save inLine segments + lnCnt += 1 + inLine = list() # reset collinear container + if cutClimb is True: + sp = cp # FreeCAD.Vector(v1[0], v1[1], 0.0) + else: + sp = ep + + if cutClimb is True: + tup = (v2, v1) + if chkGap is True: + gap = abs(toolDiam - lst.sub(ep).Length) + lst = cp + else: + tup = (v1, v2) + if chkGap is True: + gap = abs(toolDiam - lst.sub(cp).Length) + lst = ep + + if chkGap is True: + if gap < obj.GapThreshold.Value: + b = inLine.pop() # pop off 'BRK' marker + (vA, vB) = inLine.pop() # pop off previous line segment for combining with current + tup = (vA, tup[1]) + self.closedGap = True + else: + # PathLog.debug('---- Gap: {} mm'.format(gap)) + gap = round(gap, 6) + if gap < self.gaps[0]: + self.gaps.insert(0, gap) + self.gaps.pop() + inLine.append(tup) + # Efor + lnCnt += 1 + if cutClimb is True: + inLine.reverse() + LINES.append(inLine) # Save inLine segments + + # Handle last inLine set, reversing it. + if obj.CutPatternReversed is True: + if cpa != 0.0 and cpa % 90.0 == 0.0: + F = LINES.pop(0) + rev = list() + for iL in F: + if iL == 'BRK': + rev.append(iL) + else: + (p1, p2) = iL + rev.append((p2, p1)) + rev.reverse() + LINES.insert(0, rev) + + isEven = lnCnt % 2 + if isEven == 0: + PathLog.debug('Line count is ODD.') + else: + PathLog.debug('Line count is even.') + + return LINES + + def _pathGeomToZigzagPointSet(self, obj, compGeoShp): + '''_pathGeomToZigzagPointSet(obj, compGeoShp)... + Convert a compound set of sequential line segments to directionally-oriented collinear groupings + with a ZigZag directional indicator included for each collinear group.''' + PathLog.debug('_pathGeomToZigzagPointSet()') + # Extract intersection line segments for return value as list() + LINES = list() + inLine = list() + lnCnt = 0 + chkGap = False + ec = len(compGeoShp.Edges) + toolDiam = 2.0 * self.radius + + if self.CutClimb is True: + dirFlg = -1 + else: + dirFlg = 1 + + edg0 = compGeoShp.Edges[0] + p1 = (edg0.Vertexes[0].X, edg0.Vertexes[0].Y) + p2 = (edg0.Vertexes[1].X, edg0.Vertexes[1].Y) + if dirFlg == 1: + tup = (p1, p2) + lst = FreeCAD.Vector(p2[0], p2[1], 0.0) + sp = FreeCAD.Vector(p1[0], p1[1], 0.0) # start point + else: + tup = (p2, p1) + lst = FreeCAD.Vector(p1[0], p1[1], 0.0) + sp = FreeCAD.Vector(p2[0], p2[1], 0.0) # start point + inLine.append(tup) + otr = lst + + for ei in range(1, ec): + edg = compGeoShp.Edges[ei] + v1 = (edg.Vertexes[0].X, edg.Vertexes[0].Y) + v2 = (edg.Vertexes[1].X, edg.Vertexes[1].Y) + + cp = FreeCAD.Vector(v1[0], v1[1], 0.0) # check point (start point of segment) + ep = FreeCAD.Vector(v2[0], v2[1], 0.0) # end point + iC = self.isPointOnLine(sp, ep, cp) + if iC is True: + inLine.append('BRK') + chkGap = True + gap = abs(toolDiam - lst.sub(cp).Length) + else: + chkGap = False + if dirFlg == -1: + inLine.reverse() + LINES.append((dirFlg, inLine)) + lnCnt += 1 + dirFlg = -1 * dirFlg # Change zig to zag + inLine = list() # reset collinear container + sp = cp # FreeCAD.Vector(v1[0], v1[1], 0.0) + otr = ep + + lst = ep + if dirFlg == 1: + tup = (v1, v2) + else: + tup = (v2, v1) + + if chkGap is True: + if gap < obj.GapThreshold.Value: + b = inLine.pop() # pop off 'BRK' marker + (vA, vB) = inLine.pop() # pop off previous line segment for combining with current + if dirFlg == 1: + tup = (vA, tup[1]) + else: + #tup = (vA, tup[1]) + #tup = (tup[1], vA) + tup = (tup[0], vB) + self.closedGap = True + else: + gap = round(gap, 6) + if gap < self.gaps[0]: + self.gaps.insert(0, gap) + self.gaps.pop() + inLine.append(tup) + # Efor + lnCnt += 1 + + # Fix directional issue with LAST line when line count is even + isEven = lnCnt % 2 + if isEven == 0: # Changed to != with 90 degree CutPatternAngle + PathLog.debug('Line count is even.') + else: + PathLog.debug('Line count is ODD.') + dirFlg = -1 * dirFlg + if obj.CutPatternReversed is False: + if self.CutClimb is True: + dirFlg = -1 * dirFlg + + if obj.CutPatternReversed is True: + dirFlg = -1 * dirFlg + + # Handle last inLine list + if dirFlg == 1: + rev = list() + for iL in inLine: + if iL == 'BRK': + rev.append(iL) + else: + (p1, p2) = iL + rev.append((p2, p1)) + + if obj.CutPatternReversed is False: + rev.reverse() + else: + rev2 = list() + for iL in rev: + if iL == 'BRK': + rev2.append(iL) + else: + (p1, p2) = iL + rev2.append((p2, p1)) + rev2.reverse() + rev = rev2 + + LINES.append((dirFlg, rev)) + else: + LINES.append((dirFlg, inLine)) + + return LINES + + def _pathGeomToArcPointSet(self, obj, compGeoShp): + '''_pathGeomToArcPointSet(obj, compGeoShp)... + Convert a compound set of arcs/circles to a set of directionally-oriented arc end points + and the corresponding center point.''' + # Extract intersection line segments for return value as list() + PathLog.debug('_pathGeomToArcPointSet()') + ARCS = list() + stpOvrEI = list() + segEI = list() + isSame = False + sameRad = None + COM = self.tmpCOM + toolDiam = 2.0 * self.radius + ec = len(compGeoShp.Edges) + + def gapDist(sp, ep): + X = (ep[0] - sp[0])**2 + Y = (ep[1] - sp[1])**2 + Z = (ep[2] - sp[2])**2 + # return math.sqrt(X + Y + Z) + return math.sqrt(X + Y) # the 'z' value is zero in both points + + # Separate arc data into Loops and Arcs + for ei in range(0, ec): + edg = compGeoShp.Edges[ei] + if edg.Closed is True: + stpOvrEI.append(('L', ei, False)) + else: + if isSame is False: + segEI.append(ei) + isSame = True + pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0) + sameRad = pnt.sub(COM).Length + else: + # Check if arc is co-radial to current SEGS + pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0) + if abs(sameRad - pnt.sub(COM).Length) > 0.00001: + isSame = False + + if isSame is True: + segEI.append(ei) + else: + # Move co-radial arc segments + stpOvrEI.append(['A', segEI, False]) + # Start new list of arc segments + segEI = [ei] + isSame = True + pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0) + sameRad = pnt.sub(COM).Length + # Process trailing `segEI` data, if available + if isSame is True: + stpOvrEI.append(['A', segEI, False]) + + # Identify adjacent arcs with y=0 start/end points that connect + for so in range(0, len(stpOvrEI)): + SO = stpOvrEI[so] + if SO[0] == 'A': + startOnAxis = list() + endOnAxis = list() + EI = SO[1] # list of corresponding compGeoShp.Edges indexes + + # Identify startOnAxis and endOnAxis arcs + for i in range(0, len(EI)): + ei = EI[i] # edge index + E = compGeoShp.Edges[ei] # edge object + if abs(COM.y - E.Vertexes[0].Y) < 0.00001: + startOnAxis.append((i, ei, E.Vertexes[0])) + elif abs(COM.y - E.Vertexes[1].Y) < 0.00001: + endOnAxis.append((i, ei, E.Vertexes[1])) + + # Look for connections between startOnAxis and endOnAxis arcs. Consolidate data when connected + lenSOA = len(startOnAxis) + lenEOA = len(endOnAxis) + if lenSOA > 0 and lenEOA > 0: + delIdxs = list() + lstFindIdx = 0 + for soa in range(0, lenSOA): + (iS, eiS, vS) = startOnAxis[soa] + for eoa in range(0, len(endOnAxis)): + (iE, eiE, vE) = endOnAxis[eoa] + dist = vE.X - vS.X + if abs(dist) < 0.00001: # They connect on axis at same radius + SO[2] = (eiE, eiS) + break + elif dist > 0: + break # stop searching + # Eif + # Eif + # Efor + + # Construct arc data tuples for OCL + dirFlg = 1 + # cutPat = obj.CutPattern + if self.CutClimb is False: # True yields Climb when set to Conventional + dirFlg = -1 + + # Cycle through stepOver data + for so in range(0, len(stpOvrEI)): + SO = stpOvrEI[so] + if SO[0] == 'L': # L = Loop/Ring/Circle + lei = SO[1] # loop Edges index + v1 = compGeoShp.Edges[lei].Vertexes[0] + + space = obj.SampleInterval.Value / 2.0 + + p1 = FreeCAD.Vector(v1.X, v1.Y, v1.Z) + sp = (v1.X, v1.Y, 0.0) + rad = p1.sub(COM).Length + tolrncAng = math.asin(space/rad) + X = COM.x + (rad * math.cos(tolrncAng)) + Y = v1.Y - space # rad * math.sin(tolrncAng) + + sp = (v1.X, v1.Y, 0.0) + ep = (X, Y, 0.0) + cp = (COM.x, COM.y, 0.0) + if dirFlg == 1: + arc = (sp, ep, cp) + else: + arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction)) + ARCS.append(('L', dirFlg, [arc])) + else: # SO[0] == 'A' A = Arc + PRTS = list() + EI = SO[1] # list of corresponding Edges indexes + CONN = SO[2] # list of corresponding connected edges tuples (iE, iS) + chkGap = False + lst = None + + if CONN is not False: + (iE, iS) = CONN + v1 = compGeoShp.Edges[iE].Vertexes[0] + v2 = compGeoShp.Edges[iS].Vertexes[1] + sp = (v1.X, v1.Y, 0.0) + ep = (v2.X, v2.Y, 0.0) + cp = (COM.x, COM.y, 0.0) + if dirFlg == 1: + arc = (sp, ep, cp) + lst = ep + else: + arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction)) + lst = sp + PRTS.append(arc) + # Pop connected edge index values from arc segments index list + iEi = EI.index(iE) + iSi = EI.index(iS) + EI.pop(iEi) + EI.pop(iSi) + if len(EI) > 0: + PRTS.append('BRK') + chkGap = True + cnt = 0 + for ei in EI: + if cnt > 0: + PRTS.append('BRK') + chkGap = True + v1 = compGeoShp.Edges[ei].Vertexes[0] + v2 = compGeoShp.Edges[ei].Vertexes[1] + sp = (v1.X, v1.Y, 0.0) + ep = (v2.X, v2.Y, 0.0) + cp = (COM.x, COM.y, 0.0) + if dirFlg == 1: + arc = (sp, ep, cp) + if chkGap is True: + gap = abs(toolDiam - gapDist(lst, sp)) # abs(toolDiam - lst.sub(sp).Length) + lst = ep + else: + arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction)) + if chkGap is True: + gap = abs(toolDiam - gapDist(lst, ep)) # abs(toolDiam - lst.sub(ep).Length) + lst = sp + if chkGap is True: + if gap < obj.GapThreshold.Value: + b = PRTS.pop() # pop off 'BRK' marker + (vA, vB, vC) = PRTS.pop() # pop off previous arc segment for combining with current + arc = (vA, arc[1], vC) + self.closedGap = True + else: + # PathLog.debug('---- Gap: {} mm'.format(gap)) + gap = round(gap, 6) + if gap < self.gaps[0]: + self.gaps.insert(0, gap) + self.gaps.pop() + PRTS.append(arc) + cnt += 1 + + if dirFlg == -1: + PRTS.reverse() + + ARCS.append(('A', dirFlg, PRTS)) + # Eif + if obj.CutPattern == 'CircularZigZag': + dirFlg = -1 * dirFlg + # Efor + + return ARCS + + def _planarDropCutScan(self, pdc, A, B): + PNTS = list() + (x1, y1) = A + (x2, y2) = B path = ocl.Path() # create an empty path object - - if obj.DropCutterDir == 'X': - # add Line objects to the path in this loop - for n in range(0, Nl): - if n == Nl - 1: - if obj.StepOver > 50: - cOut = (self.cutter.getDiameter() / 2) - y = ymax - cOut - else: - y = ymin - (self.cutter.getDiameter() / 2) + ((n + 1) * cOut) # all lines are offset by 1/2 cutter diameter - p1 = ocl.Point(xmin, y, 0) # start-point of line - p2 = ocl.Point(xmax, y, 0) # end-point of line - - lo = cutPatternLine(obj, n, p1, p2) - path.append(lo) # add the line to the path - else: - # add Line objects to the path in this loop - for n in range(0, Nl): - if n == Nl - 1: - if obj.StepOver > 50: - cOut = (self.cutter.getDiameter() / 2) - x = xmax - cOut - else: - x = xmin - (self.cutter.getDiameter() / 2) + ((n + 1) * cOut) # all lines are offset by 1/2 cutter diameter - p1 = ocl.Point(x, ymin, 0) # start-point of line - p2 = ocl.Point(x, ymax, 0) # end-point of line - - lo = cutPatternLine(obj, n, p1, p2) - path.append(lo) # add the line to the path - + p1 = ocl.Point(x1, y1, 0) # start-point of line + p2 = ocl.Point(x2, y2, 0) # end-point of line + lo = ocl.Line(p1, p2) # line-object + path.append(lo) # add the line to the path pdc.setPath(path) + pdc.run() # run dropcutter algorithm on path + CLP = pdc.getCLPoints() + for p in CLP: + PNTS.append(FreeCAD.Vector(p.x, p.y, p.z)) + return PNTS # pdc.getCLPoints() - # run drop-cutter on the path - pdc.run() - self.reportThis("--OCL scan took " + str(time.time() - t_before) + " s") + def _planarCircularDropCutScan(self, pdc, Arc, cMode): + PNTS = list() + path = ocl.Path() # create an empty path object + (sp, ep, cp) = Arc - clp = pdc.getCLPoints() - # return the list the points - return clp + # process list of segment tuples (vect, vect) + path = ocl.Path() # create an empty path object + p1 = ocl.Point(sp[0], sp[1], 0) # start point of arc + p2 = ocl.Point(ep[0], ep[1], 0) # end point of arc + C = ocl.Point(cp[0], cp[1], 0) # center point of arc + ao = ocl.Arc(p1, p2, C, cMode) # arc object + path.append(ao) # add the arc to the path + pdc.setPath(path) + pdc.run() # run dropcutter algorithm on path + CLP = pdc.getCLPoints() - def _planarScanToGcode(self, obj, lc, prvDep, layDep, CLP, pntsPerLine, ignoreMap): + # Convert OCL object data to FreeCAD vectors + for p in CLP: + PNTS.append(FreeCAD.Vector(p.x, p.y, p.z)) + + return PNTS + + # Main planar scan functions + def _planarDropCutSingle(self, JOB, obj, pdc, safePDC, depthparams, SCANDATA): + PathLog.debug('_planarDropCutSingle()') + + GCODE = [Path.Command('N (Beginning of Single-pass layer.)', {})] + tolrnc = JOB.GeometryTolerance.Value + prevDepth = obj.SafeHeight.Value + lenDP = len(depthparams) + lenSCANDATA = len(SCANDATA) + gDIR = ['G3', 'G2'] + + if self.CutClimb is True: + gDIR = ['G2', 'G3'] + + # Set `ProfileEdges` specific trigger indexes + peIdx = lenSCANDATA # off by default + if obj.ProfileEdges == 'Only': + peIdx = -1 + elif obj.ProfileEdges == 'First': + peIdx = 0 + elif obj.ProfileEdges == 'Last': + peIdx = lenSCANDATA - 1 + + # Send cutter to x,y position of first point on first line + first = SCANDATA[0][0][0] # [step][item][point] + GCODE.append(Path.Command('G0', {'X': first.x, 'Y': first.y, 'F': self.horizRapid})) + + # Cycle through step-over sections (line segments or arcs) + odd = True + lstStpEnd = None + prevDepth = obj.SafeHeight.Value # Not used for Single-pass + for so in range(0, lenSCANDATA): + cmds = list() + PRTS = SCANDATA[so] + lenPRTS = len(PRTS) + first = PRTS[0][0] # first point of arc/line stepover group + start = PRTS[0][0] # will change with each line/arc segment + last = None + cmds.append(Path.Command('N (Begin step {}.)'.format(so), {})) + + if so > 0: + if obj.CutPattern == 'CircularZigZag': + if odd is True: + odd = False + else: + odd = True + minTrnsHght = self._getMinSafeTravelHeight(safePDC, lstStpEnd, first) # Check safe travel height against fullSTL + # cmds.append(Path.Command('N (Transition: last, first: {}, {}: minSTH: {})'.format(lstStpEnd, first, minTrnsHght), {})) + cmds.extend(self._stepTransitionCmds(obj, lstStpEnd, first, minTrnsHght, tolrnc)) + + # Override default `OptimizeLinearPaths` behavior to allow `ProfileEdges` optimization + if so == peIdx or peIdx == -1: + obj.OptimizeLinearPaths = self.preOLP + + # Cycle through current step-over parts + for i in range(0, lenPRTS): + prt = PRTS[i] + lenPrt = len(prt) + if prt == 'BRK': + nxtStart = PRTS[i + 1][0] + minSTH = self._getMinSafeTravelHeight(safePDC, last, nxtStart) # Check safe travel height against fullSTL + cmds.append(Path.Command('N (Break)', {})) + cmds.extend(self._breakCmds(obj, last, nxtStart, minSTH, tolrnc)) + else: + cmds.append(Path.Command('N (part {}.)'.format(i + 1), {})) + start = prt[0] + 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 is True: + 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) # save line commands + lstStpEnd = last + + # Return `OptimizeLinearPaths` to disabled + if so == peIdx or peIdx == -1: + if obj.CutPattern in ['Circular', 'CircularZigZag']: + obj.OptimizeLinearPaths = False + # Efor + + return GCODE + + def _planarSinglepassProcess(self, obj, PNTS): output = [] - optimize = obj.Optimize - ignWF = obj.IgnoreWaste - lenCLP = len(CLP) - lastCLP = len(CLP) - 1 - holdStart = False - holdStop = False - holdCount = 0 - holdLine = 0 + optimize = obj.OptimizeLinearPaths + lenPNTS = len(PNTS) + lstIdx = lenPNTS - 1 + lop = None onLine = False - # lineOfTravel = "X" - pointsOnLine = 0 - zMin = prvDep - zMax = prvDep - prcs = False - prcsCnt = 0 - pntCount = 0 - rowCount = 1 - begLayCmds = ["aaa", "bbb"] - minIgnVal = 1 - def makePnt(pnt): - p = ocl.Point(float("inf"), float("inf"), float("inf")) - p.x = pnt.x - p.y = pnt.y - p.z = pnt.z - return p + # Initialize first three points + nxt = None + pnt = PNTS[0] + prev = FreeCAD.Vector(-442064564.6, 258539656553.27, 3538553425.847) - def beginLayerCommand(pnt, lc): - # Send cutter to starting position(first point) - begcmd = [] - begcmd.append(Path.Command('N (Beginning of layer ' + str(lc) + ')', {})) - begcmd.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid})) - begcmd.append(Path.Command('G0', {'X': pnt.x, 'Y': pnt.y, 'F': self.horizRapid})) - begcmd.append(Path.Command('G1', {'Z': pnt.z, 'F': self.vertFeed})) - begcmd.reverse() - return begcmd + # Add temp end point + PNTS.append(FreeCAD.Vector(-4895747464.6, -25855763553.2, 35865763425)) - # Create containers for x,y,z points - prev = ocl.Point(float("inf"), float("inf"), float("inf")) - nxt = ocl.Point(float("inf"), float("inf"), float("inf")) - pnt = ocl.Point(float("inf"), float("inf"), float("inf")) - travVect = ocl.Point(float("inf"), float("inf"), float("inf")) - - # Determine if releasing model from ignore waste areas - if obj.ReleaseFromWaste is True: - minIgnVal = 0 - - # Set values for first gcode point in layer - pnt.x = CLP[0].x - pnt.y = CLP[0].y - pnt.z = CLP[0].z - if CLP[0].z < layDep: - pnt.z = layDep - - # generate the path commands # Begin processing ocl points list into gcode - for i in range(0, lenCLP): - # Calculate next point for consideration of next point - if i < lastCLP: - nxt.x = CLP[i + 1].x - nxt.y = CLP[i + 1].y - if CLP[i + 1].z < layDep: - nxt.z = layDep - else: - nxt.z = CLP[i + 1].z - else: - optimize = False + for i in range(0, lenPNTS): + # Calculate next point for consideration with current point + nxt = PNTS[i + 1] - pntCount += 1 - if pntCount == 1: - # PathLog.debug("--Start row: " + str(rowCount)) - pass # nn = 1 - elif pntCount == pntsPerLine: - # PathLog.debug("--End row: " + str(rowCount)) - pntCount = 0 - rowCount += 1 - # Add rise to clear height before beginning next row in CutPattern: Line - # if obj.CutPattern == 'Line': - # output.append(Path.Command('G0', {'Z': self.clearHeight, 'F': self.vertRapid})) - - # determine vector direction for each axis - if nxt.x == pnt.x: - travVect.x = 0 - elif nxt.x < pnt.x: - travVect.x = -1 - else: - travVect.x = 1 - - if nxt.y == pnt.y: - travVect.y = 0 - elif nxt.y < pnt.y: - travVect.y = -1 - else: - travVect.y = 1 - - # Determine cutter line of travel - if travVect.x == 0 and travVect.y != 0: - lineOfTravel = "Y" - elif travVect.y == 0 and travVect.x != 0: - lineOfTravel = "X" - else: - lineOfTravel = "O" # used for turns - - # determine if lineOfTravel is same as obj.DropCutterDir line - if onLine is False: - if lineOfTravel == obj.DropCutterDir: + # Process point + if optimize is True: + iPOL = self.isPointOnLine(prev, nxt, pnt) + if iPOL is True: onLine = True - self.lineCNT += 1 # increment line count - pointsOnLine += 1 - else: - if lineOfTravel != obj.DropCutterDir: - if self.onHold is False: - zMax = prvDep + else: onLine = False - pointsOnLine = 0 - else: - pointsOnLine += 1 - - # Ignore waste operation triggers - if ignWF is False: - prcs = True - else: # ignWF is TRUE - if obj.LayerMode == 'Single-pass': - if ignoreMap[i] > minIgnVal: - if ignoreMap[i] == 1: - pnt.z = obj.IgnoreWasteDepth - if prcs is False: - # Move cutter to current xy position - output.append(Path.Command('G0', {'X': pnt.x, 'Y': pnt.y, 'F': self.horizRapid})) - prcs = True - else: - if prcs is True: - # Raise cutter to safe height - output.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})) - prcs = False - else: # Multi-pass mode - if ignoreMap[i] > minIgnVal: - if prcs is False: - # Move cutter to current xy position - output.append(Path.Command('G0', {'X': pnt.x, 'Y': pnt.y, 'F': self.horizRapid})) - prcs = True - else: - prcs = False - # End of ignWF - - if prcs is True: - prcsCnt += 1 - if prcsCnt == 1: - begLayCmds = beginLayerCommand(pnt, lc) # start layer at this point - - if obj.LayerMode == 'Multi-pass': - # if z travels above previous layer, start/continue hold high cycle - if pnt.z > prvDep: - if self.onHold is False: - holdStart = True - self.onHold = True - - # Update zMin and zMax values - if pnt.z < zMin: - zMin = pnt.z - if pnt.z > zMax: - zMax = pnt.z - - if self.onHold is True: - if holdStart is True: - # go to current coordinate - output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed})) - # Save holdStart coordinate and prvDep values - self.holdPoint.x = pnt.x - self.holdPoint.y = pnt.y - self.holdPoint.z = pnt.z - - holdCount += 1 # Increment hold count - holdLine = self.lineCNT # remember holdLine - self.holdStartPnts.append(makePnt(pnt)) - self.holdPrevLayerVals.append(prvDep) - holdStart = False # cancel holdStart - - # hold cutter high until Z value drops below prvDep - if pnt.z <= prvDep: - holdStop = True - # End of onHold - - if holdStop is True: - # Send hold and current points to - if holdLine == self.lineCNT: - # if start and stop points on same line, process has simple hold - self.holdStartPnts.pop() - self.holdPrevLayerVals.pop() - zMax += 2.0 - for cmd in self.holdStopCmds(obj, zMax, prvDep, pnt, "Hold Stop: in-line"): - output.append(cmd) - else: - # if start and stop points on different lines, process has complex hold - self.holdStopPnts.append(makePnt(pnt)) - self.holdZMaxVals.append(zMax) - self.holdStopTypes.append("Mid") - output.append("HD") # add placeholder for processing of hold - self.holdPntCnt += 1 - - # reset necessary hold related settings - zMax = prvDep - holdStop = False - self.onHold = False - self.holdPoint = ocl.Point(float("inf"), float("inf"), float("inf")) - # End of holdStop - - if self.onHold is False: - if not optimize or not self.isPointOnLine(FreeCAD.Vector(prev.x, prev.y, prev.z), FreeCAD.Vector(nxt.x, nxt.y, nxt.z), FreeCAD.Vector(pnt.x, pnt.y, pnt.z)): - output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed})) - # elif i == lastCLP: - # output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed})) - - # Rotate point data - prev.x = pnt.x - prev.y = pnt.y - prev.z = pnt.z - # End of prcs - pnt.x = nxt.x - pnt.y = nxt.y - pnt.z = nxt.z - - # save current layer end point - if self.onHold is True: - if holdLine == self.lineCNT: - self.holdStartPnts.pop() - self.holdPrevLayerVals.pop() - for cmd in self.holdStopCmds(obj, obj.SafeHeight.Value, obj.SafeHeight.Value, pnt, "Hold Stop: Layer endpoint online"): # zMax as prvDep removes drop to prvDep - output.append(cmd) + output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed})) else: - self.holdStopPnts.append(makePnt(pnt)) - self.holdZMaxVals.append(zMax) - output.append("HD") # add placeholder for processing of hold - self.holdStopTypes.append("End") # tag hold type - self.holdPntCnt += 1 - self.onHold = False + output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed})) - # save last point for insertion into next layer CLP as start point - endPnt = pnt - endPnt.z = obj.OpStockZMax.Value + obj.DepthOffset.Value - self.layerEndPnt = endPnt - # self.reportThis("----Points after linear optimization: " + str(len(output))) - # append layer commands to operation command list - for cmd in begLayCmds: - output.insert(0, cmd) - for o in output: - self.gcodeCmds.append(o) - self.gcodeCmds.append(Path.Command('N (End of layer ' + str(lc) + ')', {})) - # return output + # Rotate point data + if onLine is False: + prev = pnt + pnt = nxt + # Efor + + temp = PNTS.pop() # Remove temp end point - def _processPlanarHolds(self, obj, scanCLP): - # Process all HOLDs in gcode command list - self.keepTime = time.time() - hldcnt = 0 - hpCmds = [] - lenHP = len(self.holdStartPnts) - commands = [Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})] - self.reportThis("--Processing " + str(lenHP) + " HOLD optimizations---") - # cycle through hold points - if lenHP > 0: - hdCnt = 0 - lenGC = len(self.gcodeCmds) - for idx in range(0, lenGC): - if self.gcodeCmds[idx] == "HD": - hdCnt += 1 - # process hold here - p1 = self.holdStartPnts.pop(0) - p2 = self.holdStopPnts.pop(0) - pd = self.holdPrevLayerVals.pop(0) - hscType = self.holdStopTypes.pop(0) + return output - if hscType == "End": - # Create gcode commands to connect p1 and p2 - hpCmds = self.holdStopEndCmds(obj, p2, "Hold Stop: End point") - elif hscType == "Mid": - # Set the max and min XY boundaries of the HOLD connection operation - cutterClearance = self.cutter.getDiameter() / 1.25 - if p1.x < p2.x: - xmin = p1.x - cutterClearance - xmax = p2.x + cutterClearance + 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 is True: + gDIR = ['G2', 'G3'] + + # Set `ProfileEdges` specific trigger indexes + peIdx = lenSCANDATA # off by default + if obj.ProfileEdges == 'Only': + peIdx = -1 + elif obj.ProfileEdges == 'First': + peIdx = 0 + elif obj.ProfileEdges == 'Last': + peIdx = lenSCANDATA - 1 + + # Process each layer in depthparams + prvLyrFirst = None + prvLyrLast = None + actvLyrs = 0 + for lyr in range(0, lenDP): + odd = True # ZigZag directional switch + lyrHasCmds = False + lstStpEnd = None + actvSteps = 0 + LYR = list() + prvStpFirst = None + prvStpLast = None + lyrDep = depthparams[lyr] + + # Cycle through step-over sections (line segments or arcs) + for so in range(0, len(SCANDATA)): + SO = SCANDATA[so] + lenSO = len(SO) + + # Pre-process step-over parts for layer depth and holds + ADJPRTS = list() + LMAX = list() + soHasPnts = False + brkFlg = False + for i in range(0, lenSO): + prt = SO[i] + lenPrt = len(prt) + if prt == 'BRK': + if brkFlg is True: + 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) + # Efor + lenAdjPrts = len(ADJPRTS) + + # Process existing parts within current step over + prtsHasCmds = False + stepHasCmds = False + prtsCmds = list() + stpOvrCmds = list() + transCmds = list() + if soHasPnts is True: + first = ADJPRTS[0][0] # first point of arc/line stepover group + + # Manage step over transition and CircularZigZag direction + if so > 0: + # Control ZigZag direction + if obj.CutPattern == 'CircularZigZag': + if odd is True: + odd = False + else: + odd = True + # Control step over transition + minTrnsHght = self._getMinSafeTravelHeight(safePDC, prvStpLast, first, minDep=None) # Check safe travel height against fullSTL + transCmds.append(Path.Command('N (--Step {} transition)'.format(so), {})) + transCmds.extend(self._stepTransitionCmds(obj, prvStpLast, first, minTrnsHght, tolrnc)) + + # Override default `OptimizeLinearPaths` behavior to allow `ProfileEdges` optimization + if so == peIdx or peIdx == -1: + obj.OptimizeLinearPaths = self.preOLP + + # Cycle through current step-over parts + for i in range(0, lenAdjPrts): + prt = ADJPRTS[i] + lenPrt = len(prt) + if prt == 'BRK' and prtsHasCmds is True: + nxtStart = ADJPRTS[i + 1][0] + minSTH = self._getMinSafeTravelHeight(safePDC, last, nxtStart, minDep=None) # Check safe travel height against fullSTL + prtsCmds.append(Path.Command('N (--Break)', {})) + prtsCmds.extend(self._breakCmds(obj, last, nxtStart, minSTH, tolrnc)) else: - xmin = p2.x - cutterClearance - xmax = p1.x + cutterClearance + 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 p1.y < p2.y: - ymin = p1.y - cutterClearance - ymax = p2.y + cutterClearance - else: - ymin = p2.y - cutterClearance - ymax = p1.y + cutterClearance - # get focused list of points based on bound box with p1 and p2 as corners, with cutter diam. as additional buffer - subCLP = self.subsectionCLP(scanCLP, xmin, ymin, xmax, ymax) - # Determine max z height for clearance between p1 and p2 - zMax = self.getMaxHeight(self.targetDepth, p1, p2, self.cutter.getDiameter(), subCLP) - # Create gcode commands to connect p1 and p2 - hpCmds = self.holdStopCmds(obj, zMax, pd, p2, "Hold Stop: Group processed") - # Add commands to list - for cmd in hpCmds: - commands.append(cmd) - hldcnt += 1 + if segCmds is not False: + prtsCmds.extend(segCmds) + prtsHasCmds = True + prvStpLast = last + # Eif + # Efor + # Eif + + # Return `OptimizeLinearPaths` to disabled + if so == peIdx or peIdx == -1: + if obj.CutPattern in ['Circular', 'CircularZigZag']: + obj.OptimizeLinearPaths = False + + # Compile step over(prts) commands + if prtsHasCmds is True: + stepHasCmds = True + actvSteps += 1 + prvStpFirst = first + 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), {})) + + # Layer transition at first active step over in current layer + if actvSteps == 1: + prvLyrFirst = first + 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) + # Eif + + # Close layer, saving commands, if any + if lyrHasCmds is True: + prvLyrLast = last + GCODE.extend(LYR) # save line commands + GCODE.append(Path.Command('N (End of layer {})'.format(lyr), {})) + + # Set previous depth + prevDepth = lyrDep + # Efor + + PathLog.debug('Multi-pass op has {} layers (step downs).'.format(lyr + 1)) + + return GCODE + + def _planarMultipassPreProcess(self, obj, LN, prvDep, layDep): + ALL = list() + PTS = list() + brkFlg = False + optLinTrans = obj.OptimizeStepOverTransitions + safe = math.ceil(obj.SafeHeight.Value) + + if optLinTrans is True: + for P in LN: + ALL.append(P) + # Handle layer depth AND hold points + 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: - commands.append(self.gcodeCmds[idx]) + PTS.append(FreeCAD.Vector(P.x, P.y, P.z)) + # Efor else: - commands = self.gcodeCmds - return commands + for P in LN: + ALL.append(P) + # Handle layer depth only + if P.z <= layDep: + PTS.append(FreeCAD.Vector(P.x, P.y, layDep)) + else: + PTS.append(FreeCAD.Vector(P.x, P.y, P.z)) + # Efor + + if optLinTrans is True: + # Remove leading and trailing Hold Points + popList = list() + for i in range(0, len(PTS)): # identify leading string + if PTS[i].z == safe: + popList.append(i) + else: + break + popList.sort(reverse=True) + for p in popList: # Remove hold points + PTS.pop(p) + ALL.pop(p) + popList = list() + for i in range(len(PTS) - 1, -1, -1): # identify trailing string + if PTS[i].z == safe: + popList.append(i) + else: + break + popList.sort(reverse=True) + for p in popList: # Remove hold points + PTS.pop(p) + ALL.pop(p) + + # Determine max Z height for remaining points on line + 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 = list() + optimize = obj.OptimizeLinearPaths + safe = math.ceil(obj.SafeHeight.Value) + lenPNTS = len(PNTS) + lastPNTS = lenPNTS - 1 + prcs = True + onHold = False + onLine = False + clrScnLn = lMax + 2.0 + + # Initialize first three points + nxt = None + pnt = PNTS[0] + prev = FreeCAD.Vector(-442064564.6, 258539656553.27, 3538553425.847) + + # Add temp end point + PNTS.append(FreeCAD.Vector(-4895747464.6, -25855763553.2, 35865763425)) + + # Begin processing ocl points list into gcode + 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}) ) + + # Process point + if prcs is True: + if optimize is True: + iPOL = self.isPointOnLine(prev, nxt, pnt) + 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})) + + # Rotate point data + if onLine is False: + prev = pnt + pnt = nxt + # Efor + + temp = PNTS.pop() # Remove temp end point + + return output + + def _stepTransitionCmds(self, obj, lstPnt, first, minSTH, tolrnc): + cmds = list() + rtpd = False + horizGC = 'G0' + hSpeed = self.horizRapid + height = obj.SafeHeight.Value + + if obj.CutPattern in ['Line', 'Circular']: + if obj.OptimizeStepOverTransitions is True: + height = minSTH + 2.0 + # if obj.LayerMode == 'Multi-pass': + # rtpd = minSTH + elif obj.CutPattern in ['ZigZag', 'CircularZigZag']: + if obj.OptimizeStepOverTransitions is True: + zChng = first.z - lstPnt.z + # PathLog.debug('first.z: {}'.format(first.z)) + # PathLog.debug('lstPnt.z: {}'.format(lstPnt.z)) + # PathLog.debug('zChng: {}'.format(zChng)) + # PathLog.debug('minSTH: {}'.format(minSTH)) + if abs(zChng) < tolrnc: # transitions to same Z height + PathLog.debug('abs(zChng) < tolrnc') + if (minSTH - first.z) > tolrnc: + PathLog.debug('(minSTH - first.z) > tolrnc') + height = minSTH + 2.0 + else: + PathLog.debug('ELSE (minSTH - first.z) > tolrnc') + horizGC = 'G1' + height = first.z + elif (minSTH + (2.0 * tolrnc)) >= max(first.z, lstPnt.z): + height = False # allow end of Zig to cut to beginning of Zag + + + # Create raise, shift, and optional lower commands + 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: # ReturnToPreviousDepth + cmds.append(Path.Command('G0', {'Z': rtpd, 'F': self.vertRapid})) + + return cmds + + def _breakCmds(self, obj, lstPnt, first, minSTH, tolrnc): + cmds = list() + rtpd = False + horizGC = 'G0' + hSpeed = self.horizRapid + height = obj.SafeHeight.Value + + if obj.CutPattern in ['Line', 'Circular']: + if obj.OptimizeStepOverTransitions is True: + height = minSTH + 2.0 + elif obj.CutPattern in ['ZigZag', 'CircularZigZag']: + if obj.OptimizeStepOverTransitions is True: + zChng = first.z - lstPnt.z + if abs(zChng) < tolrnc: # transitions to same Z height + if (minSTH - first.z) > tolrnc: + height = minSTH + 2.0 + else: + height = first.z + 2.0 # first.z + + 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: # ReturnToPreviousDepth + cmds.append(Path.Command('G0', {'Z': rtpd, 'F': self.vertRapid})) + + return cmds + + def _arcsToG2G3(self, LN, numPts, odd, gDIR, tolrnc): + cmds = list() + strtPnt = LN[0] + endPnt = LN[numPts - 1] + strtHght = strtPnt.z + coPlanar = True + isCircle = False + inrPnt = None + gdi = 0 + if odd is True: + gdi = 1 + + # Test if pnt set is circle + 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: + # convert LN to G2/G3 arc, consolidating GCode + # https://wiki.shapeoko.com/index.php/G-Code#G2_-_clockwise_arc + # https://www.cnccookbook.com/cnc-g-code-arc-circle-g02-g03/ + # Dividing circle into two arcs allows for G2/G3 on inclined surfaces + + # ijk = self.tmpCOM - strtPnt # vector from start to center + ijk = self.tmpCOM - strtPnt # vector from start to center + xyz = self.tmpCOM.add(ijk) # end point + 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, # leave same xyz.z height + 'F': self.horizFeed})) + cmds.append(Path.Command('G1', {'X': xyz.x, 'Y': xyz.y, 'Z': xyz.z, 'F': self.horizFeed})) + ijk = self.tmpCOM - xyz # vector from start to center + rst = strtPnt # end point + cmds.append(Path.Command(gDIR[gdi], {'X': rst.x, 'Y': rst.y, 'Z': rst.z, + 'I': ijk.x, 'J': ijk.y, 'K': ijk.z, # leave same xyz.z height + '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: # test for horizontal coplanar + coPlanar = False + break + if coPlanar is True: + # ijk = self.tmpCOM - strtPnt + ijk = self.tmpCOM.sub(strtPnt) # vector from start to center + 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, # leave same xyz.z height + '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): + PathLog.debug('Applying DepthOffset value: {}'.format(DepthOffset)) + lenScans = len(SCANDATA) + for s in range(0, lenScans): + LN = SCANDATA[s] + numPts = len(LN) + for pt in range(0, numPts): + SCANDATA[s][pt].z += DepthOffset + + def _planarGetPDC(self, stl, finalDep, SampleInterval, useSafeCutter=False): + pdc = ocl.PathDropCutter() # create a pdc [PathDropCutter] object + pdc.setSTL(stl) # add stl model + if useSafeCutter is True: + pdc.setCutter(self.safeCutter) # add safeCutter + else: + pdc.setCutter(self.cutter) # add cutter + pdc.setZ(finalDep) # set minimumZ (final / target depth value) + pdc.setSampling(SampleInterval) # set sampling size + return pdc + + # Main rotational scan functions + def _processRotationalOp(self, JOB, obj, mdlIdx, compoundFaces=None): + PathLog.debug('_processRotationalOp(self, obj, mdlIdx, compoundFaces=None)') + initIdx = 0.0 + final = list() + + JOB = PathUtils.findParentJob(obj) + base = JOB.Model.Group[mdlIdx] + bb = self.boundBoxes[mdlIdx] + stl = self.modelSTLs[mdlIdx] + + # Rotate model to initial index + 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)) + + # Prepare global holdpoint container + if self.holdPoint is None: + self.holdPoint = ocl.Point(float("inf"), float("inf"), float("inf")) + if self.layerEndPnt is None: + self.layerEndPnt = ocl.Point(float("inf"), float("inf"), float("inf")) + + # Avoid division by zero in rotational scan calculations + if obj.FinalDepth.Value <= 0.0: + zero = obj.SampleInterval.Value # 0.00001 + self.FinalDepth = zero + obj.FinalDepth.Value = 0.0 + else: + self.FinalDepth = obj.FinalDepth.Value + + # Determine boundbox radius based upon xzy limits data + if math.fabs(bb.ZMin) > math.fabs(bb.ZMax): + vlim = bb.ZMin + else: + vlim = bb.ZMax + if obj.RotationAxis == 'X': + # Rotation is around X-axis, cutter moves along same axis + if math.fabs(bb.YMin) > math.fabs(bb.YMax): + hlim = bb.YMin + else: + hlim = bb.YMax + else: + # Rotation is around Y-axis, cutter moves along same axis + if math.fabs(bb.XMin) > math.fabs(bb.XMax): + hlim = bb.XMin + else: + hlim = bb.XMax + + # Compute max radius of stock, as it rotates, and rotational clearance & safe heights + self.bbRadius = math.sqrt(hlim**2 + vlim**2) + self.clearHeight = self.bbRadius + JOB.SetupSheet.ClearanceHeightOffset.Value + self.safeHeight = self.bbRadius + JOB.SetupSheet.ClearanceHeightOffset.Value + + final = self._rotationalDropCutterOp(obj, stl, bb) + + return final def _rotationalDropCutterOp(self, obj, stl, bb): self.resetTolerance = 0.0000001 # degrees @@ -890,10 +3127,10 @@ class ObjectSurface(PathOp.ObjectOp): def linesToPointRings(scanLines): rngs = [] - numPnts = len(scanLines[0]) # Number of points per line along axis, at obj.SampleInterval spacing + numPnts = len(scanLines[0]) # Number of points per line along axis, at obj.SampleInterval.Value spacing for line in scanLines: # extract circular set(ring) of points from scan lines if len(line) != numPnts: - PathLog.debug("Error: line lengths not equal") + PathLog.debug('Error: line lengths not equal') return rngs for num in range(0, numPnts): @@ -961,7 +3198,6 @@ class ObjectSurface(PathOp.ObjectOp): # Complete rotational scans at layer and translate into gcode for layDep in depthparams: t_before = time.time() - self.reportThis("--layDep " + str(layDep)) # Compute circumference and step angles for current layer layCircum = 2 * math.pi * layDep @@ -976,7 +3212,7 @@ class ObjectSurface(PathOp.ObjectOp): if obj.RotationAxis == obj.DropCutterDir: # Same == indexed stepDeg = (self.cutOut / layCircum) * 360.0 else: - stepDeg = (obj.SampleInterval / layCircum) * 360.0 + stepDeg = (obj.SampleInterval.Value / layCircum) * 360.0 # Limit step angle and determine rotational index angles [indexes]. if stepDeg > 120.0: @@ -985,7 +3221,7 @@ class ObjectSurface(PathOp.ObjectOp): # Perform rotational indexed scans to layer depth if obj.RotationAxis == obj.DropCutterDir: # Same == indexed OR parallel - sample = obj.SampleInterval + sample = obj.SampleInterval.Value else: sample = self.cutOut scanLines = self._indexedDropCutScan(obj, stl, advances, xmin, ymin, xmax, ymax, layDep, sample) @@ -1020,7 +3256,7 @@ class ObjectSurface(PathOp.ObjectOp): commands.append(Path.Command('G0', {'Z': self.clearHeight, 'F': self.vertRapid})) # Eol else: - if obj.CutMode == 'Conventional': + if self.CutClimb is False: advances = invertAdvances(advances) advances.reverse() scanLines.reverse() @@ -1050,7 +3286,7 @@ class ObjectSurface(PathOp.ObjectOp): prevDepth = layDep lCnt += 1 # increment layer count - self.reportThis("--Layer " + str(lCnt) + ": " + str(len(advances)) + " OCL scans and gcode in " + str(time.time() - t_before) + " s") + PathLog.debug("--Layer " + str(lCnt) + ": " + str(len(advances)) + " OCL scans and gcode in " + str(time.time() - t_before) + " s") time.sleep(0.2) # Eol return commands @@ -1070,8 +3306,8 @@ class ObjectSurface(PathOp.ObjectOp): # if self.useTiltCutter == True: if obj.CutterTilt != 0.0: - cutterOfst = layDep * math.sin(obj.CutterTilt * math.pi / 180.0) - self.reportThis("CutterTilt: cutterOfst is " + str(cutterOfst)) + cutterOfst = layDep * math.sin(math.radians(obj.CutterTilt)) + PathLog.debug("CutterTilt: cutterOfst is " + str(cutterOfst)) sumAdv = 0.0 for adv in advances: @@ -1103,10 +3339,10 @@ class ObjectSurface(PathOp.ObjectOp): else: # odd lo = ocl.Line(p2, p1) elif obj.CutPattern == 'Line': - if obj.CutMode == 'Conventional': - lo = ocl.Line(p1, p2) - else: + if self.CutClimb is True: lo = ocl.Line(p2, p1) + else: + lo = ocl.Line(p1, p2) else: lo = ocl.Line(p1, p2) # line-object @@ -1132,7 +3368,7 @@ class ObjectSurface(PathOp.ObjectOp): def _indexedScanToGcode(self, obj, li, CLP, idxAng, prvDep, layerDepth, numDeps): # generate the path commands output = [] - optimize = obj.Optimize + optimize = obj.OptimizeLinearPaths holdCount = 0 holdStart = False holdStop = False @@ -1222,7 +3458,6 @@ class ObjectSurface(PathOp.ObjectOp): pnt.x = nxt.x pnt.y = nxt.y pnt.z = nxt.z - # self.reportThis("points after optimization: " + str(len(output))) output.append(Path.Command('N (End index angle ' + str(round(idxAng, 4)) + ')', {})) # Save layer end point for use in transitioning to next layer @@ -1233,7 +3468,8 @@ class ObjectSurface(PathOp.ObjectOp): return output def _rotationalScanToGcode(self, obj, RNG, rN, prvDep, layDep, advances): - # generate the path commands + '''_rotationalScanToGcode(obj, RNG, rN, prvDep, layDep, advances) ... + Convert rotational scan data to gcode path commands.''' output = [] nxtAng = 0 zMax = 0.0 @@ -1288,13 +3524,11 @@ class ObjectSurface(PathOp.ObjectOp): pnt.y = nxt.y pnt.z = nxt.z ang = nxtAng - # self.reportThis("points after optimization: " + str(len(output))) # Save layer end point for use in transitioning to next layer self.layerEndPnt.x = RNG[0].x self.layerEndPnt.y = RNG[0].y self.layerEndPnt.z = RNG[0].z - self.layerEndIdx = ang self.layerEndzMax = zMax # Move cutter to final point @@ -1302,10 +3536,17 @@ class ObjectSurface(PathOp.ObjectOp): return output - def _waterlineOp(self, obj, stl, bb): - t_begin = time.time() # self.keepTime = time.time() + # Main waterline functions + def _waterlineOp(self, JOB, obj, mdlIdx, subShp=None): + '''_waterlineOp(obj, base) ... Main waterline function to perform waterline extraction from model.''' commands = [] + t_begin = time.time() + # JOB = PathUtils.findParentJob(obj) + base = JOB.Model.Group[mdlIdx] + bb = self.boundBoxes[mdlIdx] + stl = self.modelSTLs[mdlIdx] + # Prepare global holdpoint and layerEndPnt containers if self.holdPoint is None: self.holdPoint = ocl.Point(float("inf"), float("inf"), float("inf")) @@ -1316,16 +3557,36 @@ class ObjectSurface(PathOp.ObjectOp): # Need to make DropCutterExtraOffset available for waterline algorithm # cdeoX = obj.DropCutterExtraOffset.x # cdeoY = obj.DropCutterExtraOffset.y - cdeoX = 0.6 * self.cutter.getDiameter() - cdeoY = 0.6 * self.cutter.getDiameter() + toolDiam = self.cutter.getDiameter() + cdeoX = 0.6 * toolDiam + cdeoY = 0.6 * toolDiam - # the max and min XY area of the operation - xmin = bb.XMin - cdeoX - xmax = bb.XMax + cdeoX - ymin = bb.YMin - cdeoY - ymax = bb.YMax + cdeoY + if subShp is None: + # Get correct boundbox + if obj.BoundBox == 'Stock': + BS = JOB.Stock + bb = BS.Shape.BoundBox + elif obj.BoundBox == 'BaseBoundBox': + BS = base + bb = base.Shape.BoundBox - smplInt = obj.SampleInterval + env = PathUtils.getEnvelope(partshape=BS.Shape, depthparams=self.depthParams) # Produces .Shape + + xmin = bb.XMin + xmax = bb.XMax + ymin = bb.YMin + ymax = bb.YMax + zmin = bb.ZMin + zmax = bb.ZMax + else: + xmin = subShp.BoundBox.XMin + xmax = subShp.BoundBox.XMax + ymin = subShp.BoundBox.YMin + ymax = subShp.BoundBox.YMax + zmin = subShp.BoundBox.ZMin + zmax = subShp.BoundBox.ZMax + + smplInt = obj.SampleInterval.Value minSampInt = 0.001 # value is mm if smplInt < minSampInt: smplInt = minSampInt @@ -1338,13 +3599,17 @@ class ObjectSurface(PathOp.ObjectOp): if obj.LayerMode == 'Single-pass': depthparams = [obj.FinalDepth.Value] else: - dep_par = PathUtils.depth_params(obj.ClearanceHeight.Value, obj.SafeHeight.Value, obj.StartDepth.Value, obj.StepDown.Value, 0.0, obj.FinalDepth.Value) - depthparams = [dp for dp in dep_par] + depthparams = [dp for dp in self.depthParams] lenDP = len(depthparams) + # Prepare PathDropCutter objects with STL data + safePDC = self._planarGetPDC(self.safeSTLs[mdlIdx], + depthparams[lenDP - 1], obj.SampleInterval.Value, useSafeCutter=False) + # Scan the piece to depth at smplInt oclScan = [] oclScan = self._waterlineDropCutScan(stl, smplInt, xmin, xmax, ymin, depthparams[lenDP - 1], numScanLines) + # oclScan = SCANS lenOS = len(oclScan) ptPrLn = int(lenOS / numScanLines) @@ -1357,8 +3622,7 @@ class ObjectSurface(PathOp.ObjectOp): scanLines[L].append(oclScan[pi]) lenSL = len(scanLines) pntsPerLine = len(scanLines[0]) - self.reportThis("--OCL scan: " + str(lenSL * pntsPerLine) + " points, with " + str(numScanLines) + " lines and " + str(pntsPerLine) + " pts/line") - self.reportThis("--Setup, OCL scan, and scan conversion to multi-dimen. list took " + str(time.time() - t_begin) + " s") + PathLog.debug("--OCL scan: " + str(lenSL * pntsPerLine) + " points, with " + str(numScanLines) + " lines and " + str(pntsPerLine) + " pts/line") # Extract Wl layers per depthparams lyr = 0 @@ -1369,10 +3633,12 @@ class ObjectSurface(PathOp.ObjectOp): cmds = self._getWaterline(obj, scanLines, layDep, lyr, lenSL, pntsPerLine) commands.extend(cmds) lyr += 1 - self.reportThis("--All layer scans combined took " + str(time.time() - layTime) + " s") + PathLog.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() # create a pdc pdc.setSTL(stl) pdc.setCutter(self.cutter) @@ -1394,6 +3660,7 @@ class ObjectSurface(PathOp.ObjectOp): return pdc.getCLPoints() def _getWaterline(self, obj, scanLines, layDep, lyr, lenSL, pntsPerLine): + '''_getWaterline(obj, scanLines, layDep, lyr, lenSL, pntsPerLine) ... Get waterline.''' commands = [] cmds = [] loopList = [] @@ -1406,7 +3673,6 @@ class ObjectSurface(PathOp.ObjectOp): self._highlightWaterline(4, 9) # Extract waterline and convert to gcode loopList = self._extractWaterlines(obj, scanLines, lyr, layDep) - time.sleep(0.1) # save commands for loop in loopList: cmds = self._loopToGcode(obj, layDep, loop) @@ -1414,6 +3680,7 @@ class ObjectSurface(PathOp.ObjectOp): 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([]) @@ -1425,7 +3692,7 @@ class ObjectSurface(PathOp.ObjectOp): return topoMap def _bufferTopoMap(self, lenSL, pntsPerLine): - # add buffer boarder of zeros to all sides to topoMap data + '''_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): @@ -1439,12 +3706,13 @@ class ObjectSurface(PathOp.ObjectOp): 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 - # self.reportThis("--Convert parallel data to ridges") + # ("--Convert parallel data to ridges") for lin in range(1, lastLn): for pt in range(1, lastPnt): # Ignore first and last points if TM[lin][pt] == 0: @@ -1453,7 +3721,7 @@ class ObjectSurface(PathOp.ObjectOp): if TM[lin][pt - 1] == 2: # step down TM[lin][pt] = 1 - # self.reportThis("--Convert perpendicular data to ridges and highlight ridges") + # ("--Convert perpendicular data to ridges and highlight ridges") for pt in range(1, lastPnt): # Ignore first and last points for lin in range(1, lastLn): if TM[lin][pt] == 0: @@ -1471,8 +3739,7 @@ class ObjectSurface(PathOp.ObjectOp): TM[lin - 1][pt] = extraMaterial highFlag = 2 - # Square corners - # self.reportThis("--Square corners") + # ("--Square corners") for pt in range(1, lastPnt): for lin in range(1, lastLn): if TM[lin][pt] == 1: # point == 1 @@ -1499,7 +3766,6 @@ class ObjectSurface(PathOp.ObjectOp): TM[lin - 1][pt] = 1 # square the corner # remove inside corners - # self.reportThis("--Remove inside corners") for pt in range(1, lastPnt): for lin in range(1, lastLn): if TM[lin][pt] == 1: # point == 1 @@ -1510,12 +3776,10 @@ class ObjectSurface(PathOp.ObjectOp): if TM[lin - 1][pt - 1] == 1 or TM[lin + 1][pt - 1] == 1: TM[lin][pt - 1] = insCorn - # PathLog.debug("\n-------------") - # for li in TM: - # PathLog.debug("Line: " + str(li)) 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 @@ -1525,17 +3789,17 @@ class ObjectSurface(PathOp.ObjectOp): loop = [] loopNum = 0 - if obj.CutMode == 'Conventional': - 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] + 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] + 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: - self.reportThis("Max search scans, " + str(maxSrchs) + " reached\nPossible incomplete waterline result!") + PathLog.debug("Max search scans, " + str(maxSrchs) + " reached\nPossible incomplete waterline result!") break for L in range(1, lastLn): for P in range(1, lastPnt): @@ -1547,10 +3811,11 @@ class ObjectSurface(PathOp.ObjectOp): self.topoMap[L][P] = 0 # Mute the starting point loopList.append(loop) srchCnt += 1 - # self.reportThis("Search count for layer " + str(lyr) + " is " + str(srchCnt) + ", with " + str(loopNum) + " loops.") + PathLog.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]] # Start loop point list cur = [L, P, 1] prv = [L, P - 1, 1] @@ -1561,7 +3826,7 @@ class ObjectSurface(PathOp.ObjectOp): while follow is True: ptc += 1 if ptc > ptLmt: - self.reportThis("Loop number " + str(loopNum) + " at [" + str(nxt[0]) + ", " + str(nxt[1]) + "] pnt count exceeds, " + str(ptLmt) + ". Stopped following loop.") + PathLog.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]) # get next point loop.append(oclScan[nxt[0] - 1][nxt[1] - 1]) # add it to loop point list @@ -1575,6 +3840,8 @@ class ObjectSurface(PathOp.ObjectOp): 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 @@ -1597,7 +3864,7 @@ class ObjectSurface(PathOp.ObjectOp): i += 1 mtch += 1 if found is False: - # self.reportThis("_findNext: No start point found.") + # ("_findNext: No start point found.") return [cl, cp, num] for r in range(0, 8): @@ -1606,13 +3873,14 @@ class ObjectSurface(PathOp.ObjectOp): if self.topoMap[l][p] == 1: return [l, p, num] - # self.reportThis("_findNext: No next pnt found") + # ("_findNext: No next pnt found") return [cl, cp, num] def _loopToGcode(self, obj, layDep, loop): + '''_loopToGcode(obj, layDep, loop) ... Convert set of loop points to Gcode.''' # generate the path commands output = [] - optimize = obj.Optimize + optimize = obj.OptimizeLinearPaths prev = ocl.Point(float("inf"), float("inf"), float("inf")) nxt = ocl.Point(float("inf"), float("inf"), float("inf")) @@ -1649,7 +3917,6 @@ class ObjectSurface(PathOp.ObjectOp): pnt.x = nxt.x pnt.y = nxt.y pnt.z = nxt.z - # self.reportThis("points after optimization: " + str(len(output))) # Save layer end point for use in transitioning to next layer self.layerEndPnt.x = pnt.x @@ -1658,10 +3925,12 @@ class ObjectSurface(PathOp.ObjectOp): return output - def isPointOnLine(self, lineA, lineB, pointP): + # Support functions for both dropcutter and waterline operations + def isPointOnLine(self, strtPnt, endPnt, pointP): + '''isPointOnLine(strtPnt, endPnt, pointP) ... Determine if a given point is on the line defined by start and end points.''' tolerance = 1e-6 - vectorAB = lineB - lineA - vectorAC = pointP - lineA + vectorAB = endPnt - strtPnt + vectorAC = pointP - strtPnt crossproduct = vectorAB.cross(vectorAC) dotproduct = vectorAB.dot(vectorAC) @@ -1677,6 +3946,7 @@ class ObjectSurface(PathOp.ObjectOp): return True 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, {})) # Raise cutter rapid to zMax in line of travel @@ -1688,6 +3958,7 @@ class ObjectSurface(PathOp.ObjectOp): return cmds def holdStopEndCmds(self, obj, p2, txt): + '''holdStopEndCmds(obj, p2, txt) ... Gcode commands to be executed at end of hold stop.''' cmds = [] msg = 'N (' + txt + ')' cmds.append(Path.Command(msg, {})) # Raise cutter rapid to zMax in line of travel @@ -1696,8 +3967,9 @@ class ObjectSurface(PathOp.ObjectOp): return cmds def subsectionCLP(self, CLP, xmin, ymin, xmax, ymax): - # This function returns a subsection of the CLP scan, limited to the min/max values supplied - section = [] + '''subsectionCLP(CLP, xmin, ymin, xmax, ymax) ... + This function returns a subsection of the CLP scan, limited to the min/max values supplied.''' + section = list() lenCLP = len(CLP) for i in range(0, lenCLP): if CLP[i].x < xmax: @@ -1707,14 +3979,16 @@ class ObjectSurface(PathOp.ObjectOp): section.append(CLP[i]) return section - def getMaxHeight(self, finalDepth, p1, p2, cutter, CLP): - # This function connects two HOLD points with line - # Each point within the subsection point list is tested to determinie if it is under cutter - # Points determined to be under the cutter on line are tested for z height - # The highest z point is the requirement for clearance between p1 and p2, and returned as zMax with 2 mm extra + def getMaxHeightBetweenPoints(self, finalDepth, p1, p2, cutter, CLP): + ''' getMaxHeightBetweenPoints(finalDepth, p1, p2, cutter, CLP) ... + This function connects two HOLD points with line. + Each point within the subsection point list is tested to determinie if it is under cutter. + Points determined to be under the cutter on line are tested for z height. + The highest z point is the requirement for clearance between p1 and p2, and returned as zMax with 2 mm extra. + ''' dx = (p2.x - p1.x) if dx == 0.0: - dx = 0.00001 + dx = 0.00001 # Need to employ a global tolerance here m = (p2.y - p1.y) / dx b = p1.y - (m * p1.x) @@ -1723,68 +3997,68 @@ class ObjectSurface(PathOp.ObjectOp): lenCLP = len(CLP) for i in range(0, lenCLP): mSqrd = m**2 - if mSqrd < 0.0000001: + if mSqrd < 0.0000001: # Need to employ a global tolerance here mSqrd = 0.0000001 perpDist = math.sqrt((CLP[i].y - (m * CLP[i].x) - b)**2 / (1 + 1 / (mSqrd))) if perpDist < avoidTool: # if point within cutter reach on line of travel, test z height and update as needed if CLP[i].z > zMax: zMax = CLP[i].z return zMax + 2.0 - - def holdStopPerpCmds(self, obj, zMax, pd, p2, aor, ang, txt): - cmds = [] - msg = 'N (' + txt + ')' - cmds.append(Path.Command(msg, {})) # Raise cutter rapid to zMax in line of travel - cmds.append(Path.Command('G0', {'Z': zMax, 'F': self.vertRapid})) # Raise cutter rapid to zMax in line of travel - cmds.append(Path.Command('G0', {'X': p2.x, 'Y': p2.y, 'F': self.horizRapid})) # horizontal rapid to current XY coordinate - if zMax != pd: - cmds.append(Path.Command('G0', {'Z': pd, 'F': self.vertRapid})) # drop cutter down rapidly to prevDepth depth - cmds.append(Path.Command('G0', {'Z': p2.z, aor: ang, 'F': self.vertFeed})) # drop cutter down to current Z depth, returning to normal cut path and speed - return cmds - - def reportThis(self, txt): - self.opReport += "\n" + txt - - def resetOpVariables(self): - # reset operation variables - self.opReport = "" - self.cutter = None + + def resetOpVariables(self, all=True): + '''resetOpVariables() ... Reset class variables used for instance of operation.''' self.holdPoint = None - self.stl = None self.layerEndPnt = None self.onHold = False - self.useTiltCutter = False - self.holdStartPnts = [] - self.holdStopPnts = [] - self.holdStopTypes = [] - self.holdZMaxVals = [] - self.holdPrevLayerVals = [] - self.gcodeCmds = [] - self.CLP = [] self.SafeHeightOffset = 2.0 self.ClearHeightOffset = 4.0 - self.layerEndIdx = 0.0 self.layerEndzMax = 0.0 self.resetTolerance = 0.0 self.holdPntCnt = 0 - self.startTime = 0.0 - self.endTime = 0.0 - self.lineCNT = 0 - self.keepTime = 0.0 - self.lineScanTime = 0.0 self.bbRadius = 0.0 - self.targetDepth = 0.0 - self.stepDeg = 0.0 - self.stepRads = 0.0 self.axialFeed = 0.0 self.axialRapid = 0.0 self.FinalDepth = 0.0 - self.cutOut = 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.radius = 0.0 + self.useTiltCutter = False return True - def setOclCutter(self, obj): + 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 setOclCutter(self, obj, safe=False): + ''' setOclCutter(obj) ... Translation function to convert FreeCAD tool definition to OCL formatted tool. ''' # Set cutter details # https://www.freecadweb.org/api/dd/dfe/classPath_1_1Tool.html#details diam_1 = float(obj.ToolController.Tool.Diameter) @@ -1793,123 +4067,137 @@ class ObjectSurface(PathOp.ObjectOp): CEH = obj.ToolController.Tool.CuttingEdgeHeight if hasattr(obj.ToolController.Tool, 'CuttingEdgeHeight') else 0 CEA = obj.ToolController.Tool.CuttingEdgeAngle if hasattr(obj.ToolController.Tool, 'CuttingEdgeAngle') else 0 + # Make safeCutter with 2 mm buffer around physical cutter + if safe is True: + diam_1 += 4.0 + if FR != 0.0: + FR += 2.0 + + PathLog.debug('ToolType: {}'.format(obj.ToolController.Tool.ToolType)) if obj.ToolController.Tool.ToolType == 'EndMill': # Standard End Mill - self.cutter = ocl.CylCutter(diam_1, (CEH + lenOfst)) + return ocl.CylCutter(diam_1, (CEH + lenOfst)) elif obj.ToolController.Tool.ToolType == 'BallEndMill' and FR == 0.0: # Standard Ball End Mill # OCL -> BallCutter::BallCutter(diameter, length) - self.cutter = ocl.BallCutter(diam_1, (diam_1 / 2 + lenOfst)) self.useTiltCutter = True + return ocl.BallCutter(diam_1, (diam_1 / 2 + lenOfst)) elif obj.ToolController.Tool.ToolType == 'BallEndMill' and FR > 0.0: # Bull Nose or Corner Radius cutter # Reference: https://www.fine-tools.com/halbstabfraeser.html # OCL -> BallCutter::BallCutter(diameter, length) - self.cutter = ocl.BullCutter(diam_1, FR, (CEH + lenOfst)) + return ocl.BullCutter(diam_1, FR, (CEH + lenOfst)) elif obj.ToolController.Tool.ToolType == 'Engraver' and FR > 0.0: # Bull Nose or Corner Radius cutter # Reference: https://www.fine-tools.com/halbstabfraeser.html # OCL -> ConeCutter::ConeCutter(diameter, angle, lengthOffset) - self.cutter = ocl.ConeCutter(diam_1, (CEA / 2), lenOfst) + return ocl.ConeCutter(diam_1, (CEA / 2), lenOfst) elif obj.ToolController.Tool.ToolType == 'ChamferMill': # Bull Nose or Corner Radius cutter # Reference: https://www.fine-tools.com/halbstabfraeser.html # OCL -> ConeCutter::ConeCutter(diameter, angle, lengthOffset) - self.cutter = ocl.ConeCutter(diam_1, (CEA / 2), lenOfst) + return ocl.ConeCutter(diam_1, (CEA / 2), lenOfst) else: # Default to standard end mill - self.cutter = ocl.CylCutter(diam_1, (CEH + lenOfst)) - PathLog.info("Defaulting cutter to standard end mill.") + PathLog.warning("Defaulting cutter to standard end mill.") + return ocl.CylCutter(diam_1, (CEH + lenOfst)) # http://www.carbidecutter.net/products/carbide-burr-cone-shape-sm.html ''' - return "Drill"; - return "CenterDrill"; - return "CounterSink"; - return "CounterBore"; - return "FlyCutter"; - return "Reamer"; - return "Tap"; - return "EndMill"; - return "SlotCutter"; - return "BallEndMill"; - return "ChamferMill"; - return "CornerRound"; - return "Engraver"; - return "Undefined"; + # Available FreeCAD cutter types - some still need translation to available OCL cutter classes. + Drill, CenterDrill, CounterSink, CounterBore, FlyCutter, Reamer, Tap, + EndMill, SlotCutter, BallEndMill, ChamferMill, CornerRound, Engraver ''' - return True + # Adittional problem is with new ToolBit user-defined cutter shapes. + # Some sort of translation/conversion will have to be defined to make compatible with OCL. + PathLog.error('Unable to set OCL cutter.') + return False - def pocketInvertExtraOffset(self): - return True - - def opSetDefaultValues(self, obj, job): - '''opSetDefaultValues(obj, job) ... initialize defaults''' - - obj.StepOver = 100 - obj.Optimize = True - obj.IgnoreWaste = False - obj.ReleaseFromWaste = False - obj.LayerMode = 'Single-pass' - obj.ScanType = 'Planar' - obj.RotationAxis = 'X' - obj.CutMode = 'Conventional' - obj.CutPattern = 'ZigZag' - obj.CutterTilt = 0.0 - obj.StartIndex = 0.0 - obj.StopIndex = 360.0 - obj.SampleInterval = 1.0 - - # need to overwrite the default depth calculations for facing - job = PathUtils.findParentJob(obj) - if job: - if job.Stock: - d = PathUtils.guessDepths(job.Stock.Shape, None) - PathLog.debug("job.Stock exists") - else: - PathLog.debug("job.Stock NOT exist") + def determineVectDirect(self, pnt, nxt, travVect): + if nxt.x == pnt.x: + travVect.x = 0 + elif nxt.x < pnt.x: + travVect.x = -1 else: - PathLog.debug("job NOT exist") + travVect.x = 1 - if self.docRestored is True: # This op is NOT the first in the Operations list - PathLog.debug("doc restored") - obj.FinalDepth.Value = obj.OpFinalDepth.Value + if nxt.y == pnt.y: + travVect.y = 0 + elif nxt.y < pnt.y: + travVect.y = -1 else: - PathLog.debug("new operation") - obj.OpFinalDepth.Value = d.final_depth - obj.OpStartDepth.Value = d.start_depth - if self.initOpFinalDepth is None and self.initFinalDepth is None: - self.initFinalDepth = d.final_depth - self.initOpFinalDepth = d.final_depth - else: - PathLog.debug("-initFinalDepth" + str(self.initFinalDepth)) - PathLog.debug("-initOpFinalDepth" + str(self.initOpFinalDepth)) - obj.IgnoreWasteDepth = obj.FinalDepth.Value + 0.001 + travVect.y = 1 + return travVect + + def determineLineOfTravel(self, travVect): + if travVect.x == 0 and travVect.y != 0: + lineOfTravel = "Y" + elif travVect.y == 0 and travVect.x != 0: + lineOfTravel = "X" + else: + lineOfTravel = "O" # used for turns + return lineOfTravel + + def _getMinSafeTravelHeight(self, pdc, p1, p2, minDep=None): + A = (p1.x, p1.y) + B = (p2.x, p2.y) + LINE = self._planarDropCutScan(pdc, A, B) + zMax = LINE[0].z + for p in LINE: + if p.z > zMax: + zMax = p.z + if minDep is not None: + if zMax < minDep: + zMax = minDep + return zMax def SetupProperties(): + ''' SetupProperties() ... Return list of properties required for operation.''' setup = [] - setup.append("Algorithm") - setup.append("DropCutterDir") - setup.append("BoundBox") - setup.append("StepOver") - setup.append("DepthOffset") - setup.append("LayerMode") - setup.append("ScanType") - setup.append("RotationAxis") - setup.append("CutMode") - setup.append("SampleInterval") - setup.append("StartIndex") - setup.append("StopIndex") - setup.append("CutterTilt") - setup.append("CutPattern") - setup.append("IgnoreWasteDepth") - setup.append("IgnoreWaste") - setup.append("ReleaseFromWaste") + setup.append('Algorithm') + setup.append('AvoidLastX_Faces') + setup.append('AvoidLastX_InternalFeatures') + setup.append('BoundBox') + setup.append('BoundaryAdjustment') + setup.append('CircularCenterAt') + setup.append('CircularCenterCustom') + setup.append('CircularUseG2G3') + setup.append('InternalFeaturesCut') + setup.append('InternalFeaturesAdjustment') + setup.append('CutMode') + setup.append('CutPattern') + setup.append('CutPatternAngle') + setup.append('CutPatternReversed') + setup.append('CutterTilt') + setup.append('DepthOffset') + setup.append('DropCutterDir') + setup.append('GapSizes') + setup.append('GapThreshold') + setup.append('HandleMultipleFeatures') + setup.append('LayerMode') + setup.append('OptimizeStepOverTransitions') + setup.append('ProfileEdges') + setup.append('BoundaryEnforcement') + setup.append('RotationAxis') + setup.append('SampleInterval') + setup.append('ScanType') + setup.append('StartIndex') + setup.append('StartPoint') + setup.append('StepOver') + setup.append('StopIndex') + setup.append('UseStartPoint') + # For debugging + setup.append('AreaParams') + setup.append('ShowTempObjects') + # Targeted for possible removal + setup.append('IgnoreWaste') + setup.append('IgnoreWasteDepth') + setup.append('ReleaseFromWaste') return setup diff --git a/src/Mod/Path/PathScripts/PathSurfaceGui.py b/src/Mod/Path/PathScripts/PathSurfaceGui.py index 0f69922998..40feff2c70 100644 --- a/src/Mod/Path/PathScripts/PathSurfaceGui.py +++ b/src/Mod/Path/PathScripts/PathSurfaceGui.py @@ -63,8 +63,8 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): obj.DropCutterExtraOffset.x = FreeCAD.Units.Quantity(self.form.boundBoxExtraOffsetX.text()).Value obj.DropCutterExtraOffset.y = FreeCAD.Units.Quantity(self.form.boundBoxExtraOffsetY.text()).Value - if obj.Optimize != self.form.optimizeEnabled.isChecked(): - obj.Optimize = self.form.optimizeEnabled.isChecked() + if obj.OptimizeLinearPaths != self.form.optimizeEnabled.isChecked(): + obj.OptimizeLinearPaths = self.form.optimizeEnabled.isChecked() self.updateToolController(obj, self.form.toolController) self.updateCoolant(obj, self.form.coolantController) @@ -81,7 +81,7 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): self.form.sampleInterval.setText(str(obj.SampleInterval)) self.form.stepOver.setValue(obj.StepOver) - if obj.Optimize: + if obj.OptimizeLinearPaths: self.form.optimizeEnabled.setCheckState(QtCore.Qt.Checked) else: self.form.optimizeEnabled.setCheckState(QtCore.Qt.Unchecked)