From c7ba4f4f3f8dee49e89eefa8adf3b0bd58f52cfb Mon Sep 17 00:00:00 2001
From: Russell Johnson <47639332+Russ4262@users.noreply.github.com>
Date: Sun, 22 Mar 2020 18:02:50 -0500
Subject: [PATCH 01/18] Path: Add `Waterline` icon file
---
.../Gui/Resources/icons/Path-Waterline.svg | 281 ++++++++++++++++++
1 file changed, 281 insertions(+)
create mode 100644 src/Mod/Path/Gui/Resources/icons/Path-Waterline.svg
diff --git a/src/Mod/Path/Gui/Resources/icons/Path-Waterline.svg b/src/Mod/Path/Gui/Resources/icons/Path-Waterline.svg
new file mode 100644
index 0000000000..86c07f4c62
--- /dev/null
+++ b/src/Mod/Path/Gui/Resources/icons/Path-Waterline.svg
@@ -0,0 +1,281 @@
+
+
From 312f09e81ebc5c70e079f9efa6162a190406e7c9 Mon Sep 17 00:00:00 2001
From: Russell Johnson <47639332+Russ4262@users.noreply.github.com>
Date: Sun, 22 Mar 2020 18:29:02 -0500
Subject: [PATCH 02/18] Path: Add `Waterline` modules to PathScripts
---
src/Mod/Path/PathScripts/PathWaterline.py | 2237 ++++++++++++++++++
src/Mod/Path/PathScripts/PathWaterlineGui.py | 154 ++
2 files changed, 2391 insertions(+)
create mode 100644 src/Mod/Path/PathScripts/PathWaterline.py
create mode 100644 src/Mod/Path/PathScripts/PathWaterlineGui.py
diff --git a/src/Mod/Path/PathScripts/PathWaterline.py b/src/Mod/Path/PathScripts/PathWaterline.py
new file mode 100644
index 0000000000..db2c2229bd
--- /dev/null
+++ b/src/Mod/Path/PathScripts/PathWaterline.py
@@ -0,0 +1,2237 @@
+# -*- coding: utf-8 -*-
+
+# ***************************************************************************
+# * *
+# * Copyright (c) 2016 sliptonic *
+# * *
+# * This program is free software; you can redistribute it and/or modify *
+# * it under the terms of the GNU Lesser General Public License (LGPL) *
+# * as published by the Free Software Foundation; either version 2 of *
+# * the License, or (at your option) any later version. *
+# * for detail see the LICENCE text file. *
+# * *
+# * This program is distributed in the hope that it will be useful, *
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+# * GNU Library General Public License for more details. *
+# * *
+# * You should have received a copy of the GNU Library General Public *
+# * License along with this program; if not, write to the Free Software *
+# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
+# * USA *
+# * *
+# ***************************************************************************
+# * *
+# * Additional modifications and contributions beginning 2019 *
+# * by Russell Johnson 2020-03-15 10:55 CST *
+# * *
+# ***************************************************************************
+
+from __future__ import print_function
+
+import FreeCAD
+import MeshPart
+import Path
+import PathScripts.PathLog as PathLog
+import PathScripts.PathUtils as PathUtils
+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."
+
+PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
+# PathLog.trackModule(PathLog.thisModule())
+
+
+# Qt translation handling
+def translate(context, text, disambig=None):
+ return QtCore.QCoreApplication.translate(context, text, disambig)
+
+
+# OCL must be installed
+try:
+ import ocl
+except ImportError:
+ FreeCAD.Console.PrintError(
+ translate("Path_Surface", "This operation requires OpenCamLib to be installed.") + "\n")
+ import sys
+ sys.exit(translate("Path_Surface", "This operation requires OpenCamLib to be installed."))
+
+
+class ObjectSurface(PathOp.ObjectOp):
+ '''Proxy object for Surfacing operation.'''
+
+ def baseObject(self):
+ '''baseObject() ... returns super of receiver
+ Used to call base implementation in overwritten functions.'''
+ return super(self.__class__, self)
+
+ 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 | PathOp.FeatureBaseFaces
+
+ def initOperation(self, obj):
+ '''initPocketOp(obj) ... create facing specific properties'''
+ obj.addProperty("App::PropertyEnumeration", "BoundBox", "Waterline", 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", "LayerMode", "Waterline", QtCore.QT_TRANSLATE_NOOP("App::Property", "The completion mode for the operation: single or multi-pass"))
+ obj.addProperty("App::PropertyEnumeration", "ScanType", "Waterline", 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::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::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.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.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.HandleMultipleFeatures = ['Collectively', 'Individually']
+ obj.LayerMode = ['Single-pass', 'Multi-pass']
+ obj.ProfileEdges = ['None', 'Only', 'First', 'Last']
+ obj.RotationAxis = ['X', 'Y']
+ 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
+
+ '''
+ 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('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('RotationAxis', 0) # 0=show & editable
+ obj.setEditorMode('StartIndex', 0)
+ obj.setEditorMode('StopIndex', 0)
+ obj.setEditorMode('CutterTilt', 0)
+ '''
+
+ 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 hasattr(self, 'addedAllProperties'):
+ if self.addedAllProperties is True:
+ 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)
+
+ def opSetDefaultValues(self, obj, job):
+ '''opSetDefaultValues(obj, job) ... initialize defaults'''
+ job = PathUtils.findParentJob(obj)
+
+ 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
+
+ # 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")
+
+ 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
+
+ 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
+ if obj.StartIndex > 360.0:
+ obj.StartIndex = 360.0
+
+ # Limit stop index
+ if obj.StopIndex > 360.0:
+ obj.StopIndex = 360.0
+ if obj.StopIndex < 0.0:
+ obj.StopIndex = 0.0
+
+ # Limit cutter tilt
+ if obj.CutterTilt < -90.0:
+ obj.CutterTilt = -90.0
+ if obj.CutterTilt > 90.0:
+ obj.CutterTilt = 90.0
+
+ # Limit sample interval
+ 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.StepOver > 100:
+ obj.StepOver = 100
+ if obj.StepOver < 1:
+ obj.StepOver = 1
+
+ # 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.'))
+
+ def opExecute(self, obj):
+ '''opExecute(obj) ... process surface operation'''
+ PathLog.track()
+
+ self.modelSTLs = list()
+ self.safeSTLs = list()
+ self.modelTypes = list()
+ self.boundBoxes = list()
+ self.profileShapes = list()
+ self.collectiveShapes = list()
+ self.individualShapes = list()
+ self.avoidShapes = list()
+ self.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:
+ 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':
+ if M.TypeId.startswith('Mesh'):
+ self.modelTypes.append('M') # Mesh
+ self.boundBoxes.append(M.Mesh.BoundBox)
+ else:
+ 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)
+
+ # ###### MAIN COMMANDS FOR OPERATION ######
+
+ # 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
+ '''
+
+ # 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
+
+ # Create OCL.stl model objects
+ self._prepareModelSTLs(JOB, obj)
+
+ 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]))
+
+ # 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, slc, 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()
+
+ 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 ScanType property.'''
+ 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
+
+ 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.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
+
+ 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
+ COMP = None
+ # Eif
+
+ return final
+
+ 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 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"))
+ if self.layerEndPnt is None:
+ self.layerEndPnt = ocl.Point(float("inf"), float("inf"), float("inf"))
+
+ # Set extra offset to diameter of cutter to allow cutter to move around perimeter of model
+ toolDiam = self.cutter.getDiameter()
+ cdeoX = 0.6 * toolDiam
+ cdeoY = 0.6 * toolDiam
+
+ 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
+
+ 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
+
+ # Determine bounding box length for the OCL scan
+ bbLength = math.fabs(ymax - ymin)
+ numScanLines = int(math.ceil(bbLength / smplInt) + 1) # Number of lines
+
+ # Compute number and size of stepdowns, and final depth
+ if obj.LayerMode == 'Single-pass':
+ depthparams = [obj.FinalDepth.Value]
+ else:
+ 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)
+
+ # Convert oclScan list of points to multi-dimensional list
+ scanLines = []
+ for L in range(0, numScanLines):
+ scanLines.append([])
+ for P in range(0, ptPrLn):
+ pi = L * ptPrLn + P
+ scanLines[L].append(oclScan[pi])
+ lenSL = len(scanLines)
+ pntsPerLine = len(scanLines[0])
+ PathLog.debug("--OCL scan: " + str(lenSL * pntsPerLine) + " points, with " + str(numScanLines) + " lines and " + str(pntsPerLine) + " pts/line")
+
+ # Extract Wl layers per depthparams
+ lyr = 0
+ cmds = []
+ layTime = time.time()
+ self.topoMap = []
+ for layDep in depthparams:
+ cmds = self._getWaterline(obj, scanLines, layDep, lyr, lenSL, pntsPerLine)
+ commands.extend(cmds)
+ lyr += 1
+ 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)
+ pdc.setZ(fd) # set minimumZ (final / target depth value)
+ pdc.setSampling(smplInt)
+
+ # Create line object as path
+ path = ocl.Path() # create an empty path object
+ for nSL in range(0, numScanLines):
+ yVal = ymin + (nSL * smplInt)
+ p1 = ocl.Point(xmin, yVal, fd) # start-point of line
+ p2 = ocl.Point(xmax, yVal, fd) # end-point of line
+ path.append(ocl.Line(p1, p2))
+ # path.append(l) # add the line to the path
+ pdc.setPath(path)
+ pdc.run() # run drop-cutter on the path
+
+ # return the list the points
+ return pdc.getCLPoints()
+
+ def _getWaterline(self, obj, scanLines, layDep, lyr, lenSL, pntsPerLine):
+ '''_getWaterline(obj, scanLines, layDep, lyr, lenSL, pntsPerLine) ... Get waterline.'''
+ commands = []
+ cmds = []
+ loopList = []
+ self.topoMap = []
+ # Create topo map from scanLines (highs and lows)
+ self.topoMap = self._createTopoMap(scanLines, layDep, lenSL, pntsPerLine)
+ # Add buffer lines and columns to topo map
+ self._bufferTopoMap(lenSL, pntsPerLine)
+ # Identify layer waterline from OCL scan
+ self._highlightWaterline(4, 9)
+ # Extract waterline and convert to gcode
+ loopList = self._extractWaterlines(obj, scanLines, lyr, layDep)
+ # save commands
+ for loop in loopList:
+ cmds = self._loopToGcode(obj, layDep, loop)
+ commands.extend(cmds)
+ return commands
+
+ def _createTopoMap(self, scanLines, layDep, lenSL, pntsPerLine):
+ '''_createTopoMap(scanLines, layDep, lenSL, pntsPerLine) ... Create topo map version of OCL scan data.'''
+ topoMap = []
+ for L in range(0, lenSL):
+ topoMap.append([])
+ for P in range(0, pntsPerLine):
+ if scanLines[L][P].z > layDep:
+ topoMap[L].append(2)
+ else:
+ topoMap[L].append(0)
+ return topoMap
+
+ def _bufferTopoMap(self, lenSL, pntsPerLine):
+ '''_bufferTopoMap(lenSL, pntsPerLine) ... Add buffer boarder of zeros to all sides to topoMap data.'''
+ pre = [0, 0]
+ post = [0, 0]
+ for p in range(0, pntsPerLine):
+ pre.append(0)
+ post.append(0)
+ for l in range(0, lenSL):
+ self.topoMap[l].insert(0, 0)
+ self.topoMap[l].append(0)
+ self.topoMap.insert(0, pre)
+ self.topoMap.append(post)
+ return True
+
+ def _highlightWaterline(self, extraMaterial, insCorn):
+ '''_highlightWaterline(extraMaterial, insCorn) ... Highlight the waterline data, separating from extra material.'''
+ TM = self.topoMap
+ lastPnt = len(TM[1]) - 1
+ lastLn = len(TM) - 1
+ highFlag = 0
+
+ # ("--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:
+ if TM[lin][pt + 1] == 2: # step up
+ TM[lin][pt] = 1
+ if TM[lin][pt - 1] == 2: # step down
+ TM[lin][pt] = 1
+
+ # ("--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:
+ highFlag = 0
+ if TM[lin + 1][pt] == 2: # step up
+ TM[lin][pt] = 1
+ if TM[lin - 1][pt] == 2: # step down
+ TM[lin][pt] = 1
+ elif TM[lin][pt] == 2:
+ highFlag += 1
+ if highFlag == 3:
+ if TM[lin - 1][pt - 1] < 2 or TM[lin - 1][pt + 1] < 2:
+ highFlag = 2
+ else:
+ TM[lin - 1][pt] = extraMaterial
+ highFlag = 2
+
+ # ("--Square corners")
+ for pt in range(1, lastPnt):
+ for lin in range(1, lastLn):
+ if TM[lin][pt] == 1: # point == 1
+ cont = True
+ if TM[lin + 1][pt] == 0: # forward == 0
+ if TM[lin + 1][pt - 1] == 1: # forward left == 1
+ if TM[lin][pt - 1] == 2: # left == 2
+ TM[lin + 1][pt] = 1 # square the corner
+ cont = False
+
+ if cont is True and TM[lin + 1][pt + 1] == 1: # forward right == 1
+ if TM[lin][pt + 1] == 2: # right == 2
+ TM[lin + 1][pt] = 1 # square the corner
+ cont = True
+
+ if TM[lin - 1][pt] == 0: # back == 0
+ if TM[lin - 1][pt - 1] == 1: # back left == 1
+ if TM[lin][pt - 1] == 2: # left == 2
+ TM[lin - 1][pt] = 1 # square the corner
+ cont = False
+
+ if cont is True and TM[lin - 1][pt + 1] == 1: # back right == 1
+ if TM[lin][pt + 1] == 2: # right == 2
+ TM[lin - 1][pt] = 1 # square the corner
+
+ # remove inside corners
+ for pt in range(1, lastPnt):
+ for lin in range(1, lastLn):
+ if TM[lin][pt] == 1: # point == 1
+ if TM[lin][pt + 1] == 1:
+ if TM[lin - 1][pt + 1] == 1 or TM[lin + 1][pt + 1] == 1:
+ TM[lin][pt + 1] = insCorn
+ elif TM[lin][pt - 1] == 1:
+ if TM[lin - 1][pt - 1] == 1 or TM[lin + 1][pt - 1] == 1:
+ TM[lin][pt - 1] = insCorn
+
+ return True
+
+ def _extractWaterlines(self, obj, oclScan, lyr, layDep):
+ '''_extractWaterlines(obj, oclScan, lyr, layDep) ... Extract water lines from OCL scan data.'''
+ srch = True
+ lastPnt = len(self.topoMap[0]) - 1
+ lastLn = len(self.topoMap) - 1
+ maxSrchs = 5
+ srchCnt = 1
+ loopList = []
+ loop = []
+ loopNum = 0
+
+ if self.CutClimb is True:
+ lC = [-1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0]
+ pC = [-1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1]
+ else:
+ lC = [1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0]
+ pC = [-1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1]
+
+ while srch is True:
+ srch = False
+ if srchCnt > maxSrchs:
+ 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):
+ if self.topoMap[L][P] == 1:
+ # start loop follow
+ srch = True
+ loopNum += 1
+ loop = self._trackLoop(oclScan, lC, pC, L, P, loopNum)
+ self.topoMap[L][P] = 0 # Mute the starting point
+ loopList.append(loop)
+ srchCnt += 1
+ 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]
+ nxt = [L, P + 1, 1]
+ follow = True
+ ptc = 0
+ ptLmt = 200000
+ while follow is True:
+ ptc += 1
+ if ptc > ptLmt:
+ 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
+ self.topoMap[nxt[0]][nxt[1]] = nxt[2] # Mute the point, if not Y stem
+ if nxt[0] == L and nxt[1] == P: # check if loop complete
+ follow = False
+ elif nxt[0] == cur[0] and nxt[1] == cur[1]: # check if line cannot be detected
+ follow = False
+ prv = cur
+ cur = nxt
+ return loop
+
+ def _findNextWlPoint(self, lC, pC, cl, cp, pl, pp):
+ '''_findNextWlPoint(lC, pC, cl, cp, pl, pp) ...
+ Find the next waterline point in the point cloud layer provided.'''
+ dl = cl - pl
+ dp = cp - pp
+ num = 0
+ i = 3
+ s = 0
+ mtch = 0
+ found = False
+ while mtch < 8: # check all 8 points around current point
+ if lC[i] == dl:
+ if pC[i] == dp:
+ s = i - 3
+ found = True
+ # Check for y branch where current point is connection between branches
+ for y in range(1, mtch):
+ if lC[i + y] == dl:
+ if pC[i + y] == dp:
+ num = 1
+ break
+ break
+ i += 1
+ mtch += 1
+ if found is False:
+ # ("_findNext: No start point found.")
+ return [cl, cp, num]
+
+ for r in range(0, 8):
+ l = cl + lC[s + r]
+ p = cp + pC[s + r]
+ if self.topoMap[l][p] == 1:
+ return [l, p, num]
+
+ # ("_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.OptimizeLinearPaths
+
+ 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"))
+
+ # Create first point
+ pnt.x = loop[0].x
+ pnt.y = loop[0].y
+ pnt.z = layDep
+
+ # Position cutter to begin loop
+ output.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid}))
+ output.append(Path.Command('G0', {'X': pnt.x, 'Y': pnt.y, 'F': self.horizRapid}))
+ output.append(Path.Command('G1', {'Z': pnt.z, 'F': self.vertFeed}))
+
+ lenCLP = len(loop)
+ lastIdx = lenCLP - 1
+ # Cycle through each point on loop
+ for i in range(0, lenCLP):
+ if i < lastIdx:
+ nxt.x = loop[i + 1].x
+ nxt.y = loop[i + 1].y
+ nxt.z = layDep
+ else:
+ optimize = 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, 'F': self.horizFeed}))
+
+ # Rotate point data
+ prev.x = pnt.x
+ prev.y = pnt.y
+ prev.z = pnt.z
+ pnt.x = nxt.x
+ pnt.y = nxt.y
+ pnt.z = nxt.z
+
+ # Save layer end point for use in transitioning to next layer
+ self.layerEndPnt.x = pnt.x
+ self.layerEndPnt.y = pnt.y
+ self.layerEndPnt.z = pnt.z
+
+ return output
+
+ # 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 = endPnt - strtPnt
+ vectorAC = pointP - strtPnt
+ crossproduct = vectorAB.cross(vectorAC)
+ dotproduct = vectorAB.dot(vectorAC)
+
+ if crossproduct.Length > tolerance:
+ return False
+
+ if dotproduct < 0:
+ return False
+
+ if dotproduct > vectorAB.Length * vectorAB.Length:
+ return False
+
+ 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
+ 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, 'F': self.vertFeed})) # drop cutter down to current Z depth, returning to normal cut path and speed
+ 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
+ cmds.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, '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
+ return cmds
+
+ def subsectionCLP(self, CLP, xmin, ymin, xmax, ymax):
+ '''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:
+ if CLP[i].y < ymax:
+ if CLP[i].x > xmin:
+ if CLP[i].y > ymin:
+ section.append(CLP[i])
+ return section
+
+ 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 # Need to employ a global tolerance here
+ m = (p2.y - p1.y) / dx
+ b = p1.y - (m * p1.x)
+
+ avoidTool = round(cutter * 0.75, 1) # 1/2 diam. of cutter is theoretically safe, but 3/4 diam is used for extra clearance
+ zMax = finalDepth
+ lenCLP = len(CLP)
+ for i in range(0, lenCLP):
+ mSqrd = m**2
+ 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 resetOpVariables(self, all=True):
+ '''resetOpVariables() ... Reset class variables used for instance of operation.'''
+ self.holdPoint = None
+ self.layerEndPnt = None
+ self.onHold = False
+ self.SafeHeightOffset = 2.0
+ self.ClearHeightOffset = 4.0
+ self.layerEndzMax = 0.0
+ self.resetTolerance = 0.0
+ self.holdPntCnt = 0
+ self.bbRadius = 0.0
+ self.axialFeed = 0.0
+ self.axialRapid = 0.0
+ self.FinalDepth = 0.0
+ self.clearHeight = 0.0
+ self.safeHeight = 0.0
+ self.faceZMax = -999999999999.0
+ if all is True:
+ self.cutter = None
+ self.stl = None
+ self.fullSTL = None
+ self.cutOut = 0.0
+ self.radius = 0.0
+ self.useTiltCutter = False
+ return True
+
+ def deleteOpVariables(self, all=True):
+ '''deleteOpVariables() ... Reset class variables used for instance of operation.'''
+ del self.holdPoint
+ del self.layerEndPnt
+ del self.onHold
+ del self.SafeHeightOffset
+ del self.ClearHeightOffset
+ del self.layerEndzMax
+ del self.resetTolerance
+ del self.holdPntCnt
+ del self.bbRadius
+ del self.axialFeed
+ del self.axialRapid
+ del self.FinalDepth
+ del self.clearHeight
+ del self.safeHeight
+ del self.faceZMax
+ if all is True:
+ del self.cutter
+ del self.stl
+ del self.fullSTL
+ del self.cutOut
+ del self.radius
+ del self.useTiltCutter
+ return True
+
+ def 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)
+ lenOfst = obj.ToolController.Tool.LengthOffset if hasattr(obj.ToolController.Tool, 'LengthOffset') else 0
+ FR = obj.ToolController.Tool.FlatRadius if hasattr(obj.ToolController.Tool, 'FlatRadius') else 0
+ 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
+ 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.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)
+ 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)
+ 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)
+ return ocl.ConeCutter(diam_1, (CEA / 2), lenOfst)
+ else:
+ # Default 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
+ '''
+ # 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
+ '''
+ # 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 determineVectDirect(self, pnt, nxt, travVect):
+ 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
+ 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('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('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
+
+
+def Create(name, obj=None):
+ '''Create(name) ... Creates and returns a Surface operation.'''
+ if obj is None:
+ obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name)
+ obj.Proxy = ObjectSurface(obj, name)
+ return obj
diff --git a/src/Mod/Path/PathScripts/PathWaterlineGui.py b/src/Mod/Path/PathScripts/PathWaterlineGui.py
new file mode 100644
index 0000000000..2c9e5d31d1
--- /dev/null
+++ b/src/Mod/Path/PathScripts/PathWaterlineGui.py
@@ -0,0 +1,154 @@
+# -*- coding: utf-8 -*-
+
+# ***************************************************************************
+# * *
+# * Copyright (c) 2017 sliptonic *
+# * *
+# * This program is free software; you can redistribute it and/or modify *
+# * it under the terms of the GNU Lesser General Public License (LGPL) *
+# * as published by the Free Software Foundation; either version 2 of *
+# * the License, or (at your option) any later version. *
+# * for detail see the LICENCE text file. *
+# * *
+# * This program is distributed in the hope that it will be useful, *
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+# * GNU Library General Public License for more details. *
+# * *
+# * You should have received a copy of the GNU Library General Public *
+# * License along with this program; if not, write to the Free Software *
+# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
+# * USA *
+# * *
+# ***************************************************************************
+
+import FreeCAD
+import FreeCADGui
+import PathScripts.PathWaterline as PathWaterline
+import PathScripts.PathGui as PathGui
+import PathScripts.PathOpGui as PathOpGui
+
+from PySide import QtCore
+
+__title__ = "Path Waterline Operation UI"
+__author__ = "sliptonic (Brad Collette)"
+__url__ = "http://www.freecadweb.org"
+__doc__ = "Waterline operation page controller and command implementation."
+__contributors__ = "russ4262 (Russell Johnson)"
+__created__ = "2019"
+__scriptVersion__ = "3t Usable"
+__lastModified__ = "2019-05-18 21:18 CST"
+
+
+class TaskPanelOpPage(PathOpGui.TaskPanelPage):
+ '''Page controller class for the Waterline operation.'''
+
+ def getForm(self):
+ '''getForm() ... returns UI'''
+ return FreeCADGui.PySideUic.loadUi(":/panels/PageOpWaterlineEdit.ui")
+
+ def getFields(self, obj):
+ '''getFields(obj) ... transfers values from UI to obj's proprties'''
+ # if obj.StartVertex != self.form.startVertex.value():
+ # obj.StartVertex = self.form.startVertex.value()
+ PathGui.updateInputField(obj, 'DepthOffset', self.form.depthOffset)
+ PathGui.updateInputField(obj, 'SampleInterval', self.form.sampleInterval)
+
+ if obj.StepOver != self.form.stepOver.value():
+ obj.StepOver = self.form.stepOver.value()
+
+ # if obj.Algorithm != str(self.form.algorithmSelect.currentText()):
+ # obj.Algorithm = str(self.form.algorithmSelect.currentText())
+
+ if obj.BoundBox != str(self.form.boundBoxSelect.currentText()):
+ obj.BoundBox = str(self.form.boundBoxSelect.currentText())
+
+ # if obj.DropCutterDir != str(self.form.dropCutterDirSelect.currentText()):
+ # obj.DropCutterDir = str(self.form.dropCutterDirSelect.currentText())
+
+ # obj.DropCutterExtraOffset.x = FreeCAD.Units.Quantity(self.form.boundBoxExtraOffsetX.text()).Value
+ # obj.DropCutterExtraOffset.y = FreeCAD.Units.Quantity(self.form.boundBoxExtraOffsetY.text()).Value
+
+ if obj.OptimizeLinearPaths != self.form.optimizeEnabled.isChecked():
+ obj.OptimizeLinearPaths = self.form.optimizeEnabled.isChecked()
+
+ self.updateToolController(obj, self.form.toolController)
+
+ def setFields(self, obj):
+ '''setFields(obj) ... transfers obj's property values to UI'''
+ # self.form.startVertex.setValue(obj.StartVertex)
+ # self.selectInComboBox(obj.Algorithm, self.form.algorithmSelect)
+ self.selectInComboBox(obj.BoundBox, self.form.boundBoxSelect)
+ # self.selectInComboBox(obj.DropCutterDir, self.form.dropCutterDirSelect)
+
+ # self.form.boundBoxExtraOffsetX.setText(str(obj.DropCutterExtraOffset.x))
+ # self.form.boundBoxExtraOffsetY.setText(str(obj.DropCutterExtraOffset.y))
+ # self.form.depthOffset.setText(FreeCAD.Units.Quantity(obj.DepthOffset.Value, FreeCAD.Units.Length).UserString)
+ self.form.sampleInterval.setText(str(obj.SampleInterval))
+ self.form.stepOver.setValue(obj.StepOver)
+
+ if obj.OptimizeLinearPaths:
+ self.form.optimizeEnabled.setCheckState(QtCore.Qt.Checked)
+ else:
+ self.form.optimizeEnabled.setCheckState(QtCore.Qt.Unchecked)
+
+ self.setupToolController(obj, self.form.toolController)
+
+ def getSignalsForUpdate(self, obj):
+ '''getSignalsForUpdate(obj) ... return list of signals for updating obj'''
+ signals = []
+ # signals.append(self.form.startVertex.editingFinished)
+ signals.append(self.form.toolController.currentIndexChanged)
+ # signals.append(self.form.algorithmSelect.currentIndexChanged)
+ signals.append(self.form.boundBoxSelect.currentIndexChanged)
+ # signals.append(self.form.dropCutterDirSelect.currentIndexChanged)
+ # signals.append(self.form.boundBoxExtraOffsetX.editingFinished)
+ # signals.append(self.form.boundBoxExtraOffsetY.editingFinished)
+ signals.append(self.form.sampleInterval.editingFinished)
+ signals.append(self.form.stepOver.editingFinished)
+ # signals.append(self.form.depthOffset.editingFinished)
+ signals.append(self.form.optimizeEnabled.stateChanged)
+
+ return signals
+
+ def updateVisibility(self):
+ # self.form.boundBoxExtraOffsetX.setEnabled(True)
+ # self.form.boundBoxExtraOffsetY.setEnabled(True)
+ self.form.boundBoxSelect.setEnabled(True)
+ self.form.sampleInterval.setEnabled(True)
+ self.form.stepOver.setEnabled(True)
+ '''
+ if self.form.algorithmSelect.currentText() == "OCL Dropcutter":
+ self.form.boundBoxExtraOffsetX.setEnabled(True)
+ self.form.boundBoxExtraOffsetY.setEnabled(True)
+ self.form.boundBoxSelect.setEnabled(True)
+ self.form.dropCutterDirSelect.setEnabled(True)
+ self.form.stepOver.setEnabled(True)
+ else:
+ self.form.boundBoxExtraOffsetX.setEnabled(False)
+ self.form.boundBoxExtraOffsetY.setEnabled(False)
+ self.form.boundBoxSelect.setEnabled(False)
+ self.form.dropCutterDirSelect.setEnabled(False)
+ self.form.stepOver.setEnabled(False)
+ '''
+ self.form.boundBoxExtraOffsetX.setEnabled(False)
+ self.form.boundBoxExtraOffsetY.setEnabled(False)
+ self.form.dropCutterDirSelect.setEnabled(False)
+ self.form.depthOffset.setEnabled(False)
+ # self.form.stepOver.setEnabled(False)
+ pass
+
+ def registerSignalHandlers(self, obj):
+ # self.form.algorithmSelect.currentIndexChanged.connect(self.updateVisibility)
+ pass
+
+
+Command = PathOpGui.SetupOperation('Waterline',
+ PathWaterline.Create,
+ TaskPanelOpPage,
+ 'Path-Waterline',
+ QtCore.QT_TRANSLATE_NOOP("Waterline", "Waterline"),
+ QtCore.QT_TRANSLATE_NOOP("Waterline", "Create a Waterline Operation from a model"),
+ PathWaterline.SetupProperties)
+
+FreeCAD.Console.PrintLog("Loading PathWaterlineGui... done\n")
From dacb6e62f291ab13f67a708d8fa3be124d4e3771 Mon Sep 17 00:00:00 2001
From: Russell Johnson <47639332+Russ4262@users.noreply.github.com>
Date: Sun, 22 Mar 2020 18:30:41 -0500
Subject: [PATCH 03/18] Path: Add `Waterline` selection gate
Using the same gate as PathSurface
---
src/Mod/Path/PathScripts/PathSelection.py | 19 +++++++++++++++++--
1 file changed, 17 insertions(+), 2 deletions(-)
diff --git a/src/Mod/Path/PathScripts/PathSelection.py b/src/Mod/Path/PathScripts/PathSelection.py
index 386ff1c29c..0cccde13b3 100644
--- a/src/Mod/Path/PathScripts/PathSelection.py
+++ b/src/Mod/Path/PathScripts/PathSelection.py
@@ -30,12 +30,14 @@ import PathScripts.PathUtils as PathUtils
import math
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
-#PathLog.trackModule(PathLog.thisModule())
+# PathLog.trackModule(PathLog.thisModule())
+
class PathBaseGate(object):
# pylint: disable=no-init
pass
+
class EGate(PathBaseGate):
def allow(self, doc, obj, sub): # pylint: disable=unused-argument
return sub and sub[0:4] == 'Edge'
@@ -66,6 +68,7 @@ class ENGRAVEGate(PathBaseGate):
return False
+
class CHAMFERGate(PathBaseGate):
def allow(self, doc, obj, sub): # pylint: disable=unused-argument
try:
@@ -94,7 +97,7 @@ class DRILLGate(PathBaseGate):
if hasattr(obj, "Shape") and sub:
shape = obj.Shape
subobj = shape.getElement(sub)
- return PathUtils.isDrillable(shape, subobj, includePartials = True)
+ return PathUtils.isDrillable(shape, subobj, includePartials=True)
else:
return False
@@ -159,6 +162,7 @@ class POCKETGate(PathBaseGate):
return pocketable
+
class ADAPTIVEGate(PathBaseGate):
def allow(self, doc, obj, sub): # pylint: disable=unused-argument
@@ -170,6 +174,7 @@ class ADAPTIVEGate(PathBaseGate):
return adaptive
+
class CONTOURGate(PathBaseGate):
def allow(self, doc, obj, sub): # pylint: disable=unused-argument
pass
@@ -182,34 +187,42 @@ def contourselect():
FreeCADGui.Selection.addSelectionGate(CONTOURGate())
FreeCAD.Console.PrintWarning("Contour Select Mode\n")
+
def eselect():
FreeCADGui.Selection.addSelectionGate(EGate())
FreeCAD.Console.PrintWarning("Edge Select Mode\n")
+
def drillselect():
FreeCADGui.Selection.addSelectionGate(DRILLGate())
FreeCAD.Console.PrintWarning("Drilling Select Mode\n")
+
def engraveselect():
FreeCADGui.Selection.addSelectionGate(ENGRAVEGate())
FreeCAD.Console.PrintWarning("Engraving Select Mode\n")
+
def chamferselect():
FreeCADGui.Selection.addSelectionGate(CHAMFERGate())
FreeCAD.Console.PrintWarning("Deburr Select Mode\n")
+
def profileselect():
FreeCADGui.Selection.addSelectionGate(PROFILEGate())
FreeCAD.Console.PrintWarning("Profiling Select Mode\n")
+
def pocketselect():
FreeCADGui.Selection.addSelectionGate(POCKETGate())
FreeCAD.Console.PrintWarning("Pocketing Select Mode\n")
+
def adaptiveselect():
FreeCADGui.Selection.addSelectionGate(ADAPTIVEGate())
FreeCAD.Console.PrintWarning("Adaptive Select Mode\n")
+
def surfaceselect():
if(MESHGate() is True or PROFILEGate() is True):
FreeCADGui.Selection.addSelectionGate(True)
@@ -237,10 +250,12 @@ def select(op):
opsel['Profile Edges'] = eselect
opsel['Profile Faces'] = profileselect
opsel['Surface'] = surfaceselect
+ opsel['Waterline'] = surfaceselect
opsel['Adaptive'] = adaptiveselect
opsel['Probe'] = probeselect
return opsel[op]
+
def clear():
FreeCADGui.Selection.removeSelectionGate()
FreeCAD.Console.PrintWarning("Free Select\n")
From c8fc8e61ae882184fa587a26cf6475c572002c48 Mon Sep 17 00:00:00 2001
From: Russell Johnson <47639332+Russ4262@users.noreply.github.com>
Date: Mon, 23 Mar 2020 16:28:25 -0500
Subject: [PATCH 04/18] Path: Alphabetize and add `Waterline` files to list
Icon file
Task panel file
Path: Remove references to nonexistent files
Path: Add missing ToolTable icon file reference
---
src/Mod/Path/Gui/Resources/Path.qrc | 62 +++++++++++++++--------------
1 file changed, 32 insertions(+), 30 deletions(-)
diff --git a/src/Mod/Path/Gui/Resources/Path.qrc b/src/Mod/Path/Gui/Resources/Path.qrc
index ca09e140b8..4471e2eb6e 100644
--- a/src/Mod/Path/Gui/Resources/Path.qrc
+++ b/src/Mod/Path/Gui/Resources/Path.qrc
@@ -1,9 +1,19 @@
+ icons/Path-Adaptive.svg
+ icons/Path-ToolDuplicate.svg
icons/Path-3DPocket.svg
icons/Path-3DSurface.svg
+ icons/Path-Area-View.svg
+ icons/Path-Area-Workplane.svg
+ icons/Path-Area.svg
icons/Path-Array.svg
icons/Path-Axis.svg
+ icons/Path-BFastForward.svg
+ icons/Path-BPause.svg
+ icons/Path-BPlay.svg
+ icons/Path-BStep.svg
+ icons/Path-BStop.svg
icons/Path-BaseGeometry.svg
icons/Path-Comment.svg
icons/Path-Compound.svg
@@ -17,9 +27,9 @@
icons/Path-Drilling.svg
icons/Path-Engrave.svg
icons/Path-ExportTemplate.svg
+ icons/Path-Face.svg
icons/Path-FacePocket.svg
icons/Path-FaceProfile.svg
- icons/Path-Face.svg
icons/Path-Heights.svg
icons/Path-Helix.svg
icons/Path-Hop.svg
@@ -27,13 +37,13 @@
icons/Path-Job.svg
icons/Path-Kurve.svg
icons/Path-LengthOffset.svg
+ icons/Path-Machine.svg
icons/Path-MachineLathe.svg
icons/Path-MachineMill.svg
- icons/Path-Machine.svg
icons/Path-OpActive.svg
+ icons/Path-OpCopy.svg
icons/Path-OperationA.svg
icons/Path-OperationB.svg
- icons/Path-OpCopy.svg
icons/Path-Plane.svg
icons/Path-Pocket.svg
icons/Path-Post.svg
@@ -46,49 +56,40 @@
icons/Path-SetupSheet.svg
icons/Path-Shape.svg
icons/Path-SimpleCopy.svg
+ icons/Path-Simulator.svg
icons/Path-Speed.svg
icons/Path-Stock.svg
icons/Path-Stop.svg
icons/Path-ToolBit.svg
icons/Path-ToolChange.svg
icons/Path-ToolController.svg
- icons/Path-ToolDuplicate.svg
icons/Path-Toolpath.svg
icons/Path-ToolTable.svg
- icons/Path-Area.svg
- icons/Path-Area-View.svg
- icons/Path-Area-Workplane.svg
- icons/Path-Simulator.svg
- icons/Path-BFastForward.svg
- icons/Path-BPause.svg
- icons/Path-BPlay.svg
- icons/Path-BStep.svg
- icons/Path-BStop.svg
+ icons/Path-Waterline.svg
icons/arrow-ccw.svg
icons/arrow-cw.svg
icons/arrow-down.svg
- icons/arrow-left.svg
icons/arrow-left-down.svg
icons/arrow-left-up.svg
- icons/arrow-right.svg
+ icons/arrow-left.svg
icons/arrow-right-down.svg
icons/arrow-right-up.svg
+ icons/arrow-right.svg
icons/arrow-up.svg
- icons/edge-join-miter.svg
icons/edge-join-miter-not.svg
- icons/edge-join-round.svg
+ icons/edge-join-miter.svg
icons/edge-join-round-not.svg
+ icons/edge-join-round.svg
icons/preferences-path.svg
- icons/Path-Adaptive.svg
panels/DlgJobChooser.ui
panels/DlgJobCreate.ui
panels/DlgJobModelSelect.ui
panels/DlgJobTemplateExport.ui
panels/DlgSelectPostProcessor.ui
+ panels/DlgTCChooser.ui
panels/DlgToolControllerEdit.ui
panels/DlgToolCopy.ui
panels/DlgToolEdit.ui
- panels/DlgTCChooser.ui
panels/DogboneEdit.ui
panels/DressupPathBoundary.ui
panels/HoldingTagsEdit.ui
@@ -106,6 +107,7 @@
panels/PageOpProbeEdit.ui
panels/PageOpProfileFullEdit.ui
panels/PageOpSurfaceEdit.ui
+ panels/PageOpWaterlineEdit.ui
panels/PathEdit.ui
panels/PointEdit.ui
panels/SetupGlobal.ui
@@ -120,16 +122,25 @@
preferences/PathDressupHoldingTags.ui
preferences/PathJob.ui
translations/Path_af.qm
+ translations/Path_ar.qm
+ translations/Path_ca.qm
translations/Path_cs.qm
translations/Path_de.qm
translations/Path_el.qm
translations/Path_es-ES.qm
+ translations/Path_eu.qm
translations/Path_fi.qm
+ translations/Path_fil.qm
translations/Path_fr.qm
+ translations/Path_gl.qm
translations/Path_hr.qm
translations/Path_hu.qm
+ translations/Path_id.qm
translations/Path_it.qm
translations/Path_ja.qm
+ translations/Path_kab.qm
+ translations/Path_ko.qm
+ translations/Path_lt.qm
translations/Path_nl.qm
translations/Path_no.qm
translations/Path_pl.qm
@@ -143,18 +154,9 @@
translations/Path_sv-SE.qm
translations/Path_tr.qm
translations/Path_uk.qm
+ translations/Path_val-ES.qm
+ translations/Path_vi.qm
translations/Path_zh-CN.qm
translations/Path_zh-TW.qm
- translations/Path_eu.qm
- translations/Path_ca.qm
- translations/Path_gl.qm
- translations/Path_kab.qm
- translations/Path_ko.qm
- translations/Path_fil.qm
- translations/Path_id.qm
- translations/Path_lt.qm
- translations/Path_val-ES.qm
- translations/Path_ar.qm
- translations/Path_vi.qm
From ba48b9e48f5b13db1a5668cb1f654585e7f60df3 Mon Sep 17 00:00:00 2001
From: Russell Johnson <47639332+Russ4262@users.noreply.github.com>
Date: Sun, 22 Mar 2020 18:23:04 -0500
Subject: [PATCH 05/18] Path: Alphabetize and add `Waterline` module files
---
src/Mod/Path/CMakeLists.txt | 12 +++++++-----
1 file changed, 7 insertions(+), 5 deletions(-)
diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt
index 1a51ebc872..ded7c91a93 100644
--- a/src/Mod/Path/CMakeLists.txt
+++ b/src/Mod/Path/CMakeLists.txt
@@ -25,6 +25,8 @@ INSTALL(
SET(PathScripts_SRCS
PathCommands.py
+ PathScripts/PathAdaptive.py
+ PathScripts/PathAdaptiveGui.py
PathScripts/PathAreaOp.py
PathScripts/PathArray.py
PathScripts/PathCircularHoleBase.py
@@ -100,6 +102,7 @@ SET(PathScripts_SRCS
PathScripts/PathSetupSheetOpPrototype.py
PathScripts/PathSetupSheetOpPrototypeGui.py
PathScripts/PathSimpleCopy.py
+ PathScripts/PathSimulatorGui.py
PathScripts/PathStock.py
PathScripts/PathStop.py
PathScripts/PathSurface.py
@@ -113,15 +116,14 @@ SET(PathScripts_SRCS
PathScripts/PathToolController.py
PathScripts/PathToolControllerGui.py
PathScripts/PathToolEdit.py
- PathScripts/PathToolLibraryManager.py
PathScripts/PathToolLibraryEditor.py
+ PathScripts/PathToolLibraryManager.py
PathScripts/PathUtil.py
PathScripts/PathUtils.py
PathScripts/PathUtilsGui.py
- PathScripts/PathSimulatorGui.py
+ PathScripts/PathWaterline.py
+ PathScripts/PathWaterlineGui.py
PathScripts/PostUtils.py
- PathScripts/PathAdaptiveGui.py
- PathScripts/PathAdaptive.py
PathScripts/__init__.py
)
@@ -192,8 +194,8 @@ SET(PathTests_SRCS
PathTests/boxtest.fcstd
PathTests/test_centroid_00.ngc
PathTests/test_geomop.fcstd
- PathTests/test_linuxcnc_00.ngc
PathTests/test_holes00.fcstd
+ PathTests/test_linuxcnc_00.ngc
)
SET(PathImages_Ops
From 04f8c474a425ac211617082b290ade7fcadee228 Mon Sep 17 00:00:00 2001
From: Russell Johnson <47639332+Russ4262@users.noreply.github.com>
Date: Sun, 22 Mar 2020 18:24:58 -0500
Subject: [PATCH 06/18] Path: Add `Waterline` command and PEP8 clean-up
---
src/Mod/Path/InitGui.py | 33 +++++++++++++++++++--------------
1 file changed, 19 insertions(+), 14 deletions(-)
diff --git a/src/Mod/Path/InitGui.py b/src/Mod/Path/InitGui.py
index dc638a9299..6fabab05f0 100644
--- a/src/Mod/Path/InitGui.py
+++ b/src/Mod/Path/InitGui.py
@@ -21,8 +21,9 @@
# * *
# ***************************************************************************/
+
class PathCommandGroup:
- def __init__(self, cmdlist, menu, tooltip = None):
+ def __init__(self, cmdlist, menu, tooltip=None):
self.cmdlist = cmdlist
self.menu = menu
if tooltip is None:
@@ -34,7 +35,7 @@ class PathCommandGroup:
return tuple(self.cmdlist)
def GetResources(self):
- return { 'MenuText': self.menu, 'ToolTip': self.tooltip }
+ return {'MenuText': self.menu, 'ToolTip': self.tooltip}
def IsActive(self):
if FreeCAD.ActiveDocument is not None:
@@ -43,6 +44,7 @@ class PathCommandGroup:
return True
return False
+
class PathWorkbench (Workbench):
"Path workbench"
@@ -88,14 +90,14 @@ class PathWorkbench (Workbench):
projcmdlist = ["Path_Job", "Path_Post"]
toolcmdlist = ["Path_Inspect", "Path_Simulator", "Path_ToolLibraryEdit", "Path_SelectLoop", "Path_OpActiveToggle"]
prepcmdlist = ["Path_Fixture", "Path_Comment", "Path_Stop", "Path_Custom", "Path_Probe"]
- twodopcmdlist = ["Path_Contour", "Path_Profile_Faces", "Path_Profile_Edges", "Path_Pocket_Shape", "Path_Drilling", "Path_MillFace", "Path_Helix", "Path_Adaptive" ]
+ twodopcmdlist = ["Path_Contour", "Path_Profile_Faces", "Path_Profile_Edges", "Path_Pocket_Shape", "Path_Drilling", "Path_MillFace", "Path_Helix", "Path_Adaptive"]
threedopcmdlist = ["Path_Pocket_3D"]
engravecmdlist = ["Path_Engrave", "Path_Deburr"]
- modcmdlist = ["Path_OperationCopy", "Path_Array", "Path_SimpleCopy" ]
+ modcmdlist = ["Path_OperationCopy", "Path_Array", "Path_SimpleCopy"]
dressupcmdlist = ["Path_DressupAxisMap", "Path_DressupPathBoundary", "Path_DressupDogbone", "Path_DressupDragKnife", "Path_DressupLeadInOut", "Path_DressupRampEntry", "Path_DressupTag", "Path_DressupZCorrect"]
extracmdlist = []
- #modcmdmore = ["Path_Hop",]
- #remotecmdlist = ["Path_Remote"]
+ # modcmdmore = ["Path_Hop",]
+ # remotecmdlist = ["Path_Remote"]
engravecmdgroup = ['Path_EngraveTools']
FreeCADGui.addCommand('Path_EngraveTools', PathCommandGroup(engravecmdlist, QtCore.QT_TRANSLATE_NOOP("Path", 'Engraving Operations')))
@@ -107,11 +109,12 @@ class PathWorkbench (Workbench):
extracmdlist.extend(["Path_Area", "Path_Area_Workplane"])
try:
- import ocl # pylint: disable=unused-variable
+ import ocl # pylint: disable=unused-variable
from PathScripts import PathSurfaceGui
- threedopcmdlist.append("Path_Surface")
+ from PathScripts import PathWaterlineGui
+ threedopcmdlist.extend(["Path_Surface", "Path_Waterline"])
threedcmdgroup = ['Path_3dTools']
- FreeCADGui.addCommand('Path_3dTools', PathCommandGroup(threedopcmdlist, QtCore.QT_TRANSLATE_NOOP("Path",'3D Operations')))
+ FreeCADGui.addCommand('Path_3dTools', PathCommandGroup(threedopcmdlist, QtCore.QT_TRANSLATE_NOOP("Path", '3D Operations')))
except ImportError:
FreeCAD.Console.PrintError("OpenCamLib is not working!\n")
@@ -122,7 +125,9 @@ class PathWorkbench (Workbench):
if extracmdlist:
self.appendToolbar(QtCore.QT_TRANSLATE_NOOP("Path", "Helpful Tools"), extracmdlist)
- self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path")], projcmdlist +["Path_ExportTemplate", "Separator"] + toolbitcmdlist + toolcmdlist +["Separator"] + twodopcmdlist + engravecmdlist +["Separator"] +threedopcmdlist +["Separator"])
+ self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path")], projcmdlist + ["Path_ExportTemplate", "Separator"] +
+ toolbitcmdlist + toolcmdlist + ["Separator"] + twodopcmdlist + engravecmdlist + ["Separator"] +
+ threedopcmdlist + ["Separator"])
self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path"), QtCore.QT_TRANSLATE_NOOP(
"Path", "Path Dressup")], dressupcmdlist)
self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path"), QtCore.QT_TRANSLATE_NOOP(
@@ -136,7 +141,7 @@ class PathWorkbench (Workbench):
curveAccuracy = PathPreferences.defaultLibAreaCurveAccuracy()
if curveAccuracy:
- Path.Area.setDefaultParams(Accuracy = curveAccuracy)
+ Path.Area.setDefaultParams(Accuracy=curveAccuracy)
Log('Loading Path workbench... done\n')
@@ -171,8 +176,8 @@ class PathWorkbench (Workbench):
if obj.isDerivedFrom("Path::Feature"):
if "Profile" in selectedName or "Contour" in selectedName or "Dressup" in selectedName:
self.appendContextMenu("", "Separator")
- #self.appendContextMenu("", ["Set_StartPoint"])
- #self.appendContextMenu("", ["Set_EndPoint"])
+ # self.appendContextMenu("", ["Set_StartPoint"])
+ # self.appendContextMenu("", ["Set_EndPoint"])
for cmd in self.dressupcmds:
self.appendContextMenu("", [cmd])
menuAppended = True
@@ -182,10 +187,10 @@ class PathWorkbench (Workbench):
if menuAppended:
self.appendContextMenu("", "Separator")
+
Gui.addWorkbench(PathWorkbench())
FreeCAD.addImportType(
"GCode (*.nc *.gc *.ncc *.ngc *.cnc *.tap *.gcode)", "PathGui")
# FreeCAD.addExportType(
# "GCode (*.nc *.gc *.ncc *.ngc *.cnc *.tap *.gcode)", "PathGui")
-
From a2c72feba86fb2d98a601a961a6feeb005e98716 Mon Sep 17 00:00:00 2001
From: Russell Johnson <47639332+Russ4262@users.noreply.github.com>
Date: Sun, 22 Mar 2020 18:37:27 -0500
Subject: [PATCH 07/18] Path: Add commented references for missing imported
modules
PathSurfaceGui and PathWaterlineGui are imported in the initGui.py module due to OCL dependency.
---
src/Mod/Path/PathScripts/PathGuiInit.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/Mod/Path/PathScripts/PathGuiInit.py b/src/Mod/Path/PathScripts/PathGuiInit.py
index 29272c0382..52116e608b 100644
--- a/src/Mod/Path/PathScripts/PathGuiInit.py
+++ b/src/Mod/Path/PathScripts/PathGuiInit.py
@@ -35,6 +35,7 @@ else:
Processed = False
+
def Startup():
global Processed # pylint: disable=global-statement
if not Processed:
@@ -71,12 +72,13 @@ def Startup():
from PathScripts import PathSimpleCopy
from PathScripts import PathSimulatorGui
from PathScripts import PathStop
+ # from PathScripts import PathSurfaceGui # Added in initGui.py due to OCL dependency
from PathScripts import PathToolController
from PathScripts import PathToolControllerGui
from PathScripts import PathToolLibraryManager
from PathScripts import PathToolLibraryEditor
from PathScripts import PathUtilsGui
+ # from PathScripts import PathWaterlineGui # Added in initGui.py due to OCL dependency
Processed = True
else:
PathLog.debug('Skipping PathGui initialisation')
-
From df253cdcdcd05ac023bc8cf0ba53f20205aa9257 Mon Sep 17 00:00:00 2001
From: Russell Johnson <47639332+Russ4262@users.noreply.github.com>
Date: Mon, 23 Mar 2020 11:31:49 -0500
Subject: [PATCH 08/18] Path: Add `Waterline` task panel
Path: Update `Waterline` task panel and GUI code
---
.../Resources/panels/PageOpWaterlineEdit.ui | 206 ++++++++++++++++++
src/Mod/Path/PathScripts/PathWaterlineGui.py | 72 ++----
2 files changed, 219 insertions(+), 59 deletions(-)
create mode 100644 src/Mod/Path/Gui/Resources/panels/PageOpWaterlineEdit.ui
diff --git a/src/Mod/Path/Gui/Resources/panels/PageOpWaterlineEdit.ui b/src/Mod/Path/Gui/Resources/panels/PageOpWaterlineEdit.ui
new file mode 100644
index 0000000000..e9c94ac535
--- /dev/null
+++ b/src/Mod/Path/Gui/Resources/panels/PageOpWaterlineEdit.ui
@@ -0,0 +1,206 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 400
+ 400
+
+
+
+ Form
+
+
+ -
+
+
+ QFrame::StyledPanel
+
+
+ QFrame::Raised
+
+
+
-
+
+
+ ToolController
+
+
+
+ -
+
+
+ <html><head/><body><p>The tool and its settings to be used for this operation.</p></body></html>
+
+
+
+
+
+
+ -
+
+
+
-
+
+
+ mm
+
+
+
+ -
+
+
+ Cut Pattern
+
+
+
+ -
+
+
+ Sample interval
+
+
+
+ -
+
+
+ -
+
+
+
+ 9
+
+
+
-
+
+ Line
+
+
+ -
+
+ ZigZag
+
+
+ -
+
+ Circular
+
+
+ -
+
+ CircularZigZag
+
+
+
+
+ -
+
+
+ Depth offset
+
+
+
+ -
+
+
+
+ 9
+
+
+
-
+
+ Stock
+
+
+ -
+
+ BaseBoundBox
+
+
+
+
+ -
+
+
+ mm
+
+
+
+ -
+
+
+ Optimize Linear Paths
+
+
+
+ -
+
+
+ BoundBox
+
+
+
+ -
+
+
+ Boundary Adjustment
+
+
+
+ -
+
+
+ <html><head/><body><p>The amount by which the tool is laterally displaced on each cycle of the pattern, specified in percent of the tool diameter.</p><p>A step over of 100% results in no overlap between two different cycles.</p></body></html>
+
+
+ 1
+
+
+ 100
+
+
+ 10
+
+
+ 100
+
+
+
+ -
+
+
+ Step over
+
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+
+
+ Gui::InputField
+ QWidget
+
+
+
+
+
+
diff --git a/src/Mod/Path/PathScripts/PathWaterlineGui.py b/src/Mod/Path/PathScripts/PathWaterlineGui.py
index 2c9e5d31d1..ece048ecee 100644
--- a/src/Mod/Path/PathScripts/PathWaterlineGui.py
+++ b/src/Mod/Path/PathScripts/PathWaterlineGui.py
@@ -2,7 +2,8 @@
# ***************************************************************************
# * *
-# * Copyright (c) 2017 sliptonic *
+# * Copyright (c) 2020 sliptonic *
+# * Copyright (c) 2020 russ4262 *
# * *
# * This program is free software; you can redistribute it and/or modify *
# * it under the terms of the GNU Lesser General Public License (LGPL) *
@@ -31,13 +32,9 @@ import PathScripts.PathOpGui as PathOpGui
from PySide import QtCore
__title__ = "Path Waterline Operation UI"
-__author__ = "sliptonic (Brad Collette)"
+__author__ = "sliptonic (Brad Collette), russ4262 (Russell Johnson)"
__url__ = "http://www.freecadweb.org"
__doc__ = "Waterline operation page controller and command implementation."
-__contributors__ = "russ4262 (Russell Johnson)"
-__created__ = "2019"
-__scriptVersion__ = "3t Usable"
-__lastModified__ = "2019-05-18 21:18 CST"
class TaskPanelOpPage(PathOpGui.TaskPanelPage):
@@ -49,26 +46,15 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
def getFields(self, obj):
'''getFields(obj) ... transfers values from UI to obj's proprties'''
- # if obj.StartVertex != self.form.startVertex.value():
- # obj.StartVertex = self.form.startVertex.value()
- PathGui.updateInputField(obj, 'DepthOffset', self.form.depthOffset)
+ PathGui.updateInputField(obj, 'BoundaryAdjustment', self.form.boundaryAdjustment)
PathGui.updateInputField(obj, 'SampleInterval', self.form.sampleInterval)
if obj.StepOver != self.form.stepOver.value():
obj.StepOver = self.form.stepOver.value()
- # if obj.Algorithm != str(self.form.algorithmSelect.currentText()):
- # obj.Algorithm = str(self.form.algorithmSelect.currentText())
-
if obj.BoundBox != str(self.form.boundBoxSelect.currentText()):
obj.BoundBox = str(self.form.boundBoxSelect.currentText())
- # if obj.DropCutterDir != str(self.form.dropCutterDirSelect.currentText()):
- # obj.DropCutterDir = str(self.form.dropCutterDirSelect.currentText())
-
- # obj.DropCutterExtraOffset.x = FreeCAD.Units.Quantity(self.form.boundBoxExtraOffsetX.text()).Value
- # obj.DropCutterExtraOffset.y = FreeCAD.Units.Quantity(self.form.boundBoxExtraOffsetY.text()).Value
-
if obj.OptimizeLinearPaths != self.form.optimizeEnabled.isChecked():
obj.OptimizeLinearPaths = self.form.optimizeEnabled.isChecked()
@@ -76,70 +62,38 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
def setFields(self, obj):
'''setFields(obj) ... transfers obj's property values to UI'''
- # self.form.startVertex.setValue(obj.StartVertex)
- # self.selectInComboBox(obj.Algorithm, self.form.algorithmSelect)
+ self.setupToolController(obj, self.form.toolController)
self.selectInComboBox(obj.BoundBox, self.form.boundBoxSelect)
- # self.selectInComboBox(obj.DropCutterDir, self.form.dropCutterDirSelect)
-
- # self.form.boundBoxExtraOffsetX.setText(str(obj.DropCutterExtraOffset.x))
- # self.form.boundBoxExtraOffsetY.setText(str(obj.DropCutterExtraOffset.y))
- # self.form.depthOffset.setText(FreeCAD.Units.Quantity(obj.DepthOffset.Value, FreeCAD.Units.Length).UserString)
- self.form.sampleInterval.setText(str(obj.SampleInterval))
+ self.form.boundaryAdjustment.setText(FreeCAD.Units.Quantity(obj.BoundaryAdjustment.Value, FreeCAD.Units.Length).UserString)
self.form.stepOver.setValue(obj.StepOver)
+ self.form.sampleInterval.setText(str(obj.SampleInterval))
if obj.OptimizeLinearPaths:
self.form.optimizeEnabled.setCheckState(QtCore.Qt.Checked)
else:
self.form.optimizeEnabled.setCheckState(QtCore.Qt.Unchecked)
- self.setupToolController(obj, self.form.toolController)
-
def getSignalsForUpdate(self, obj):
'''getSignalsForUpdate(obj) ... return list of signals for updating obj'''
signals = []
- # signals.append(self.form.startVertex.editingFinished)
signals.append(self.form.toolController.currentIndexChanged)
- # signals.append(self.form.algorithmSelect.currentIndexChanged)
signals.append(self.form.boundBoxSelect.currentIndexChanged)
- # signals.append(self.form.dropCutterDirSelect.currentIndexChanged)
- # signals.append(self.form.boundBoxExtraOffsetX.editingFinished)
- # signals.append(self.form.boundBoxExtraOffsetY.editingFinished)
- signals.append(self.form.sampleInterval.editingFinished)
+ signals.append(self.form.boundaryAdjustment.editingFinished)
signals.append(self.form.stepOver.editingFinished)
- # signals.append(self.form.depthOffset.editingFinished)
+ signals.append(self.form.sampleInterval.editingFinished)
signals.append(self.form.optimizeEnabled.stateChanged)
return signals
def updateVisibility(self):
- # self.form.boundBoxExtraOffsetX.setEnabled(True)
- # self.form.boundBoxExtraOffsetY.setEnabled(True)
self.form.boundBoxSelect.setEnabled(True)
- self.form.sampleInterval.setEnabled(True)
+ self.form.boundaryAdjustment.setEnabled(True)
self.form.stepOver.setEnabled(True)
- '''
- if self.form.algorithmSelect.currentText() == "OCL Dropcutter":
- self.form.boundBoxExtraOffsetX.setEnabled(True)
- self.form.boundBoxExtraOffsetY.setEnabled(True)
- self.form.boundBoxSelect.setEnabled(True)
- self.form.dropCutterDirSelect.setEnabled(True)
- self.form.stepOver.setEnabled(True)
- else:
- self.form.boundBoxExtraOffsetX.setEnabled(False)
- self.form.boundBoxExtraOffsetY.setEnabled(False)
- self.form.boundBoxSelect.setEnabled(False)
- self.form.dropCutterDirSelect.setEnabled(False)
- self.form.stepOver.setEnabled(False)
- '''
- self.form.boundBoxExtraOffsetX.setEnabled(False)
- self.form.boundBoxExtraOffsetY.setEnabled(False)
- self.form.dropCutterDirSelect.setEnabled(False)
- self.form.depthOffset.setEnabled(False)
- # self.form.stepOver.setEnabled(False)
- pass
+ self.form.sampleInterval.setEnabled(True)
+ self.form.optimizeEnabled.setEnabled(True)
def registerSignalHandlers(self, obj):
- # self.form.algorithmSelect.currentIndexChanged.connect(self.updateVisibility)
+ # self.form.clearLayers.currentIndexChanged.connect(self.updateVisibility)
pass
From 4109f0c7df90eb8e332cb3e713453d76f5e7b7a2 Mon Sep 17 00:00:00 2001
From: Russell Johnson <47639332+Russ4262@users.noreply.github.com>
Date: Thu, 26 Mar 2020 18:04:04 -0500
Subject: [PATCH 09/18] Path: Restructure initOperation() for backward
compatibility
Added initOpProperties() to handle independent creation of properties for backward compatibility and to allow for future storage of enumeration property lists within the operation.
Path: Change from PathSurface code to `Waterline`
Path: Updates and fixes
Path: Simplify code
Remove `Algorithm` and `AreaParams` property usage.
Remove other unused properties.
Simplify setupEditorProperties().
Path: Remove unused methods
Path: Update property initialization and handling
Make properties backward compatible with previous versions of the operation.
Remove references to unused properties due to 3D Surface and Waterline separation.
Path: Insert placeholder for `Experimental` algorithm
Path: Fix missing property and fix syntax
Missing `Algrorithm
---
src/Mod/Path/PathScripts/PathSurface.py | 769 +++++-----------------
src/Mod/Path/PathScripts/PathWaterline.py | 410 +++++-------
2 files changed, 326 insertions(+), 853 deletions(-)
diff --git a/src/Mod/Path/PathScripts/PathSurface.py b/src/Mod/Path/PathScripts/PathSurface.py
index 0c83884bc7..89932a104a 100644
--- a/src/Mod/Path/PathScripts/PathSurface.py
+++ b/src/Mod/Path/PathScripts/PathSurface.py
@@ -82,138 +82,171 @@ class ObjectSurface(PathOp.ObjectOp):
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", "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", "LayerMode", "Algorithm", QtCore.QT_TRANSLATE_NOOP("App::Property", "The completion mode for the operation: single or multi-pass"))
- 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::PropertyDistance", "AngularDeflection", "Mesh Conversion", QtCore.QT_TRANSLATE_NOOP("App::Property", "Smaller values yield a finer, more accurate the mesh. Smaller values increase processing time a lot."))
- obj.addProperty("App::PropertyDistance", "LinearDeflection", "Mesh Conversion", QtCore.QT_TRANSLATE_NOOP("App::Property", "Smaller values yield a finer, more accurate the mesh. Smaller values do not increase processing time much."))
-
- 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::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::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 circular 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.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"))
+ '''initPocketOp(obj) ... create operation specific properties'''
+ self.initOpProperties(obj)
# 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.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.ProfileEdges = ['None', 'Only', 'First', 'Last']
- obj.RotationAxis = ['X', 'Y']
- obj.ScanType = ['Planar', 'Rotational']
-
if not hasattr(obj, 'DoNotSetDefaultValues'):
self.setEditorProperties(obj)
+ def initOpProperties(self, obj):
+ '''initOpProperties(obj) ... create operation specific properties'''
+
+ PROPS = [
+ ("App::PropertyBool", "ShowTempObjects", "Debug",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "If true, the temporary path construction objects will be shown.")),
+
+ ("App::PropertyDistance", "AngularDeflection", "Mesh Conversion",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Smaller values yield a finer, more accurate the mesh. Smaller values increase processing time a lot.")),
+ ("App::PropertyDistance", "LinearDeflection", "Mesh Conversion",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Smaller values yield a finer, more accurate the mesh. Smaller values do not increase processing time much.")),
+
+ ("App::PropertyFloat", "CutterTilt", "Rotational",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Stop index(angle) for rotational scan")),
+ ("App::PropertyEnumeration", "DropCutterDir", "Rotational",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "The direction along which dropcutter lines are created")),
+ ("App::PropertyVectorDistance", "DropCutterExtraOffset", "Rotational",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Additional offset to the selected bounding box")),
+ ("App::PropertyEnumeration", "RotationAxis", "Rotational",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "The model will be rotated around this axis.")),
+ ("App::PropertyFloat", "StartIndex", "Rotational",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Start index(angle) for rotational scan")),
+ ("App::PropertyFloat", "StopIndex", "Rotational",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Stop index(angle) for rotational scan")),
+
+ ("App::PropertyEnumeration", "BoundBox", "Surface",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Should the operation be limited by the stock object or by the bounding box of the base object")),
+ ("App::PropertyEnumeration", "ScanType", "Surface",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Planar: Flat, 3D surface scan. Rotational: 4th-axis rotational scan.")),
+ ("App::PropertyDistance", "SampleInterval", "Surface",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "The Sample Interval. Small values cause long wait times")),
+
+ ("App::PropertyInteger", "AvoidLastX_Faces", "Selected Face(s) Settings",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Avoid cutting the last 'N' faces in the Base Geometry list of selected faces.")),
+ ("App::PropertyBool", "AvoidLastX_InternalFeatures", "Selected Face(s) Settings",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Do not cut internal features on avoided faces.")),
+ ("App::PropertyDistance", "BoundaryAdjustment", "Selected Face(s) Settings",
+ 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.")),
+ ("App::PropertyBool", "BoundaryEnforcement", "Selected Face(s) Settings",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "If true, the cutter will remain inside the boundaries of the model or selected face(s).")),
+ ("App::PropertyEnumeration", "HandleMultipleFeatures", "Selected Face(s) Settings",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose how to process multiple Base Geometry features.")),
+ ("App::PropertyDistance", "InternalFeaturesAdjustment", "Selected Face(s) Settings",
+ 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.")),
+ ("App::PropertyBool", "InternalFeaturesCut", "Selected Face(s) Settings",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Ignore internal feature areas within a larger selected face.")),
+
+ ("App::PropertyVectorDistance", "CircularCenterCustom", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "The start point of this path")),
+ ("App::PropertyEnumeration", "CircularCenterAt", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose what point to start the ciruclar pattern: Center Of Mass, Center Of Boundbox, Xmin Ymin of boundbox, Custom.")),
+ ("App::PropertyEnumeration", "CutMode", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "The direction that the toolpath should go around the part: Climb(ClockWise) or Conventional(CounterClockWise)")),
+ ("App::PropertyEnumeration", "CutPattern", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Clearing pattern to use")),
+ ("App::PropertyFloat", "CutPatternAngle", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Yaw angle for certain clearing patterns")),
+ ("App::PropertyBool", "CutPatternReversed", "Clearing 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.")),
+ ("App::PropertyDistance", "DepthOffset", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Z-axis offset from the surface of the object")),
+ ("App::PropertyEnumeration", "LayerMode", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "The completion mode for the operation: single or multi-pass")),
+ ("App::PropertyEnumeration", "ProfileEdges", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Profile the edges of the selection.")),
+ ("App::PropertyPercent", "StepOver", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Step over percentage of the drop cutter path")),
+
+ ("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.")),
+ ("App::PropertyBool", "OptimizeStepOverTransitions", "Surface Optimization",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable separate optimization of transitions between, and breaks within, each step over path.")),
+ ("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.")),
+ ("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.")),
+ ("App::PropertyString", "GapSizes", "Surface Optimization",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Feedback: three smallest gaps identified in the path geometry.")),
+
+ ("App::PropertyVectorDistance", "StartPoint", "Start Point",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "The start point of this path")),
+ ("App::PropertyBool", "UseStartPoint", "Start Point",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Make True, if specifying a Start Point"))
+ ]
+
+ missing = list()
+ for (prtyp, nm, grp, tt) in PROPS:
+ if not hasattr(obj, nm):
+ obj.addProperty(prtyp, nm, grp, tt)
+ missing.append(nm)
+
+ # Set enumeration lists for enumeration properties
+ if len(missing) > 0:
+ ENUMS = self._propertyEnumerations()
+ for n in ENUMS:
+ if n in missing:
+ cmdStr = 'obj.{}={}'.format(n, ENUMS[n])
+ exec(cmdStr)
+
self.addedAllProperties = True
+ def _propertyEnumerations(self):
+ # Enumeration lists for App::PropertyEnumeration properties
+ return {
+ 'BoundBox': ['BaseBoundBox', 'Stock'],
+ 'CircularCenterAt': ['CenterOfMass', 'CenterOfBoundBox', 'XminYmin', 'Custom'],
+ 'CutMode': ['Conventional', 'Climb'],
+ 'CutPattern': ['Line', 'ZigZag', 'Circular', 'CircularZigZag'], # Additional goals ['Offset', 'Spiral', 'ZigZagOffset', 'Grid', 'Triangle']
+ 'DropCutterDir': ['X', 'Y'],
+ 'HandleMultipleFeatures': ['Collectively', 'Individually'],
+ 'LayerMode': ['Single-pass', 'Multi-pass'],
+ 'ProfileEdges': ['None', 'Only', 'First', 'Last'],
+ 'RotationAxis': ['X', 'Y'],
+ 'ScanType': ['Planar', 'Rotational']
+ }
+
def setEditorProperties(self, obj):
# Used to hide inputs in properties list
- if obj.Algorithm == 'OCL Dropcutter':
- 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('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)
+ mode = 2 # 2=hidden
+ if obj.ScanType == 'Planar':
+ show = 0
+ hide = 2
+ # if obj.CutPattern in ['Line', 'ZigZag']:
+ if obj.CutPattern in ['Circular', 'CircularZigZag']:
+ show = 2 # hide
+ hide = 0 # show
+ obj.setEditorMode('CutPatternAngle', show)
+ obj.setEditorMode('CircularCenterAt', hide)
+ obj.setEditorMode('CircularCenterCustom', hide)
+ elif obj.ScanType == 'Rotational':
+ mode = 0 # show and editable
+ obj.setEditorMode('DropCutterDir', mode)
+ obj.setEditorMode('DropCutterExtraOffset', mode)
+ obj.setEditorMode('RotationAxis', mode)
+ obj.setEditorMode('StartIndex', mode)
+ obj.setEditorMode('StopIndex', mode)
+ obj.setEditorMode('CutterTilt', mode)
def onChanged(self, obj, prop):
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):
+ self.initOpProperties(obj)
+
if PathLog.getLevel(PathLog.thisModule()) != 4:
obj.setEditorMode('ShowTempObjects', 2) # hide
else:
obj.setEditorMode('ShowTempObjects', 0) # show
- self.addedAllProperties = True
+
self.setEditorProperties(obj)
def opSetDefaultValues(self, obj, job):
@@ -221,8 +254,6 @@ class ObjectSurface(PathOp.ObjectOp):
job = PathUtils.findParentJob(obj)
obj.OptimizeLinearPaths = True
- obj.IgnoreWaste = False
- obj.ReleaseFromWaste = False
obj.InternalFeaturesCut = True
obj.OptimizeStepOverTransitions = False
obj.CircularUseG2G3 = False
@@ -241,7 +272,6 @@ class ObjectSurface(PathOp.ObjectOp):
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
@@ -366,9 +396,6 @@ class ObjectSurface(PathOp.ObjectOp):
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:
@@ -480,16 +507,6 @@ class ObjectSurface(PathOp.ObjectOp):
# ###### MAIN COMMANDS FOR OPERATION ######
- # 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
-
# Begin processing obj.Base data and creating GCode
# Process selected faces, if available
pPM = self._preProcessModel(JOB, obj)
@@ -520,12 +537,6 @@ class ObjectSurface(PathOp.ObjectOp):
# 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
@@ -750,7 +761,7 @@ class ObjectSurface(PathOp.ObjectOp):
# Handle profile edges request
if cont is True and obj.ProfileEdges != 'None':
ofstVal = self._calculateOffsetValue(obj, isHole)
- psOfst = self._extractFaceOffset(obj, cfsL, ofstVal)
+ psOfst = self._extractFaceOffset(cfsL, ofstVal)
if psOfst is not False:
mPS = [psOfst]
if obj.ProfileEdges == 'Only':
@@ -760,7 +771,7 @@ class ObjectSurface(PathOp.ObjectOp):
PathLog.error(' -Failed to create profile geometry for selected faces.')
cont = False
- if cont is True:
+ if cont:
if self.showDebugObjects is True:
T = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCollectiveShape')
T.Shape = cfsL
@@ -768,12 +779,12 @@ class ObjectSurface(PathOp.ObjectOp):
self.tempGroup.addObject(T)
ofstVal = self._calculateOffsetValue(obj, isHole)
- faceOfstShp = self._extractFaceOffset(obj, cfsL, ofstVal)
+ faceOfstShp = self._extractFaceOffset(cfsL, ofstVal)
if faceOfstShp is False:
PathLog.error(' -Failed to create offset face.')
cont = False
- if cont is True:
+ if cont:
lenIfL = len(ifL)
if obj.InternalFeaturesCut is False:
if lenIfL == 0:
@@ -789,7 +800,7 @@ class ObjectSurface(PathOp.ObjectOp):
C.purgeTouched()
self.tempGroup.addObject(C)
ofstVal = self._calculateOffsetValue(obj, isHole=True)
- intOfstShp = self._extractFaceOffset(obj, casL, ofstVal)
+ intOfstShp = self._extractFaceOffset(casL, ofstVal)
mIFS.append(intOfstShp)
# faceOfstShp = faceOfstShp.cut(intOfstShp)
@@ -825,7 +836,7 @@ class ObjectSurface(PathOp.ObjectOp):
if obj.ProfileEdges != 'None':
ofstVal = self._calculateOffsetValue(obj, isHole)
- psOfst = self._extractFaceOffset(obj, outerFace, ofstVal)
+ psOfst = self._extractFaceOffset(outerFace, ofstVal)
if psOfst is not False:
if mPS is False:
mPS = list()
@@ -839,9 +850,9 @@ class ObjectSurface(PathOp.ObjectOp):
PathLog.error(' -Failed to create profile geometry for Face{}.'.format(fNum))
cont = False
- if cont is True:
+ if cont:
ofstVal = self._calculateOffsetValue(obj, isHole)
- faceOfstShp = self._extractFaceOffset(obj, outerFace, ofstVal)
+ faceOfstShp = self._extractFaceOffset(outerFace, ofstVal)
lenIfl = len(ifL)
if obj.InternalFeaturesCut is False and lenIfl > 0:
@@ -851,7 +862,7 @@ class ObjectSurface(PathOp.ObjectOp):
casL = Part.makeCompound(ifL)
ofstVal = self._calculateOffsetValue(obj, isHole=True)
- intOfstShp = self._extractFaceOffset(obj, casL, ofstVal)
+ intOfstShp = self._extractFaceOffset(casL, ofstVal)
mIFS.append(intOfstShp)
# faceOfstShp = faceOfstShp.cut(intOfstShp)
@@ -907,7 +918,7 @@ class ObjectSurface(PathOp.ObjectOp):
P.purgeTouched()
self.tempGroup.addObject(P)
- if cont is True:
+ if cont:
if self.showDebugObjects is True:
PathLog.debug('*** tmpVoidCompound')
P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpVoidCompound')
@@ -916,12 +927,12 @@ class ObjectSurface(PathOp.ObjectOp):
P.purgeTouched()
self.tempGroup.addObject(P)
ofstVal = self._calculateOffsetValue(obj, isHole, isVoid=True)
- avdOfstShp = self._extractFaceOffset(obj, avoid, ofstVal)
+ avdOfstShp = self._extractFaceOffset(avoid, ofstVal)
if avdOfstShp is False:
PathLog.error('Failed to create collective offset avoid face.')
cont = False
- if cont is True:
+ if cont:
avdShp = avdOfstShp
if obj.AvoidLastX_InternalFeatures is False and len(intFEAT) > 0:
@@ -930,7 +941,7 @@ class ObjectSurface(PathOp.ObjectOp):
else:
ifc = intFEAT[0]
ofstVal = self._calculateOffsetValue(obj, isHole=True)
- ifOfstShp = self._extractFaceOffset(obj, ifc, ofstVal)
+ ifOfstShp = self._extractFaceOffset(ifc, ofstVal)
if ifOfstShp is False:
PathLog.error('Failed to create collective offset avoid internal features.')
else:
@@ -999,7 +1010,7 @@ class ObjectSurface(PathOp.ObjectOp):
cont = False
time.sleep(0.2)
- if cont is True:
+ if cont:
csFaceShape = self._getShapeSlice(baseEnv)
if csFaceShape is False:
PathLog.debug('_getShapeSlice(baseEnv) failed')
@@ -1014,7 +1025,7 @@ class ObjectSurface(PathOp.ObjectOp):
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)
+ psOfst = self._extractFaceOffset(csFaceShape, ofstVal)
if psOfst is not False:
if obj.ProfileEdges == 'Only':
return (True, psOfst)
@@ -1023,9 +1034,9 @@ class ObjectSurface(PathOp.ObjectOp):
PathLog.error(' -Failed to create profile geometry.')
cont = False
- if cont is True:
+ if cont:
ofstVal = self._calculateOffsetValue(obj, isHole)
- faceOffsetShape = self._extractFaceOffset(obj, csFaceShape, ofstVal)
+ faceOffsetShape = self._extractFaceOffset(csFaceShape, ofstVal)
if faceOffsetShape is False:
PathLog.error('_extractFaceOffset() failed.')
else:
@@ -1076,7 +1087,7 @@ class ObjectSurface(PathOp.ObjectOp):
WIRES.append((eArea, F.Wires[0], raised))
cont = False
- if cont is True:
+ if cont:
PathLog.debug(' -cont is True')
# If only one wire and not checkEdges, return first wire
if lenWrs == 1:
@@ -1126,7 +1137,7 @@ class ObjectSurface(PathOp.ObjectOp):
return offset
- def _extractFaceOffset(self, obj, fcShape, offset):
+ def _extractFaceOffset(self, 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.'''
@@ -1151,10 +1162,6 @@ class ObjectSurface(PathOp.ObjectOp):
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:
@@ -1420,26 +1427,15 @@ class ObjectSurface(PathOp.ObjectOp):
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
+ 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
return
def _makeSafeSTL(self, JOB, obj, mdlIdx, faceShapes, voidShapes):
@@ -1479,7 +1475,7 @@ class ObjectSurface(PathOp.ObjectOp):
except Exception as eee:
PathLog.error(str(eee))
- if cont is True:
+ if cont:
stckWst = JOB.Stock.Shape.cut(envBB)
if obj.BoundaryAdjustment > 0.0:
cmpndFS = Part.makeCompound(faceShapes)
@@ -1543,7 +1539,7 @@ class ObjectSurface(PathOp.ObjectOp):
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.'''
+ It then calls the correct scan method depending on the ScanType property.'''
PathLog.debug('_processCutAreas()')
final = list()
@@ -1561,10 +1557,7 @@ class ObjectSurface(PathOp.ObjectOp):
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':
+ if obj.ScanType == 'Planar':
final.extend(self._processPlanarOp(JOB, obj, mdlIdx, COMP, 0))
elif obj.ScanType == 'Rotational':
final.extend(self._processRotationalOp(obj, base, COMP))
@@ -1585,10 +1578,7 @@ class ObjectSurface(PathOp.ObjectOp):
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':
+ if obj.ScanType == 'Planar':
final.extend(self._processPlanarOp(JOB, obj, mdlIdx, COMP, fsi))
elif obj.ScanType == 'Rotational':
final.extend(self._processRotationalOp(JOB, obj, mdlIdx, COMP))
@@ -3550,347 +3540,7 @@ class ObjectSurface(PathOp.ObjectOp):
return output
- # 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"))
- if self.layerEndPnt is None:
- self.layerEndPnt = ocl.Point(float("inf"), float("inf"), float("inf"))
-
- # Set extra offset to diameter of cutter to allow cutter to move around perimeter of model
- # Need to make DropCutterExtraOffset available for waterline algorithm
- # cdeoX = obj.DropCutterExtraOffset.x
- # cdeoY = obj.DropCutterExtraOffset.y
- toolDiam = self.cutter.getDiameter()
- cdeoX = 0.6 * toolDiam
- cdeoY = 0.6 * toolDiam
-
- 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
-
- 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
-
- # Determine bounding box length for the OCL scan
- bbLength = math.fabs(ymax - ymin)
- numScanLines = int(math.ceil(bbLength / smplInt) + 1) # Number of lines
-
- # Compute number and size of stepdowns, and final depth
- if obj.LayerMode == 'Single-pass':
- depthparams = [obj.FinalDepth.Value]
- else:
- 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)
-
- # Convert oclScan list of points to multi-dimensional list
- scanLines = []
- for L in range(0, numScanLines):
- scanLines.append([])
- for P in range(0, ptPrLn):
- pi = L * ptPrLn + P
- scanLines[L].append(oclScan[pi])
- lenSL = len(scanLines)
- pntsPerLine = len(scanLines[0])
- PathLog.debug("--OCL scan: " + str(lenSL * pntsPerLine) + " points, with " + str(numScanLines) + " lines and " + str(pntsPerLine) + " pts/line")
-
- # Extract Wl layers per depthparams
- lyr = 0
- cmds = []
- layTime = time.time()
- self.topoMap = []
- for layDep in depthparams:
- cmds = self._getWaterline(obj, scanLines, layDep, lyr, lenSL, pntsPerLine)
- commands.extend(cmds)
- lyr += 1
- 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)
- pdc.setZ(fd) # set minimumZ (final / target depth value)
- pdc.setSampling(smplInt)
-
- # Create line object as path
- path = ocl.Path() # create an empty path object
- for nSL in range(0, numScanLines):
- yVal = ymin + (nSL * smplInt)
- p1 = ocl.Point(xmin, yVal, fd) # start-point of line
- p2 = ocl.Point(xmax, yVal, fd) # end-point of line
- path.append(ocl.Line(p1, p2))
- # path.append(l) # add the line to the path
- pdc.setPath(path)
- pdc.run() # run drop-cutter on the path
-
- # return the list the points
- return pdc.getCLPoints()
-
- def _getWaterline(self, obj, scanLines, layDep, lyr, lenSL, pntsPerLine):
- '''_getWaterline(obj, scanLines, layDep, lyr, lenSL, pntsPerLine) ... Get waterline.'''
- commands = []
- cmds = []
- loopList = []
- self.topoMap = []
- # Create topo map from scanLines (highs and lows)
- self.topoMap = self._createTopoMap(scanLines, layDep, lenSL, pntsPerLine)
- # Add buffer lines and columns to topo map
- self._bufferTopoMap(lenSL, pntsPerLine)
- # Identify layer waterline from OCL scan
- self._highlightWaterline(4, 9)
- # Extract waterline and convert to gcode
- loopList = self._extractWaterlines(obj, scanLines, lyr, layDep)
- # save commands
- for loop in loopList:
- cmds = self._loopToGcode(obj, layDep, loop)
- commands.extend(cmds)
- return commands
-
- def _createTopoMap(self, scanLines, layDep, lenSL, pntsPerLine):
- '''_createTopoMap(scanLines, layDep, lenSL, pntsPerLine) ... Create topo map version of OCL scan data.'''
- topoMap = []
- for L in range(0, lenSL):
- topoMap.append([])
- for P in range(0, pntsPerLine):
- if scanLines[L][P].z > layDep:
- topoMap[L].append(2)
- else:
- topoMap[L].append(0)
- return topoMap
-
- def _bufferTopoMap(self, lenSL, pntsPerLine):
- '''_bufferTopoMap(lenSL, pntsPerLine) ... Add buffer boarder of zeros to all sides to topoMap data.'''
- pre = [0, 0]
- post = [0, 0]
- for p in range(0, pntsPerLine):
- pre.append(0)
- post.append(0)
- for l in range(0, lenSL):
- self.topoMap[l].insert(0, 0)
- self.topoMap[l].append(0)
- self.topoMap.insert(0, pre)
- self.topoMap.append(post)
- return True
-
- def _highlightWaterline(self, extraMaterial, insCorn):
- '''_highlightWaterline(extraMaterial, insCorn) ... Highlight the waterline data, separating from extra material.'''
- TM = self.topoMap
- lastPnt = len(TM[1]) - 1
- lastLn = len(TM) - 1
- highFlag = 0
-
- # ("--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:
- if TM[lin][pt + 1] == 2: # step up
- TM[lin][pt] = 1
- if TM[lin][pt - 1] == 2: # step down
- TM[lin][pt] = 1
-
- # ("--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:
- highFlag = 0
- if TM[lin + 1][pt] == 2: # step up
- TM[lin][pt] = 1
- if TM[lin - 1][pt] == 2: # step down
- TM[lin][pt] = 1
- elif TM[lin][pt] == 2:
- highFlag += 1
- if highFlag == 3:
- if TM[lin - 1][pt - 1] < 2 or TM[lin - 1][pt + 1] < 2:
- highFlag = 2
- else:
- TM[lin - 1][pt] = extraMaterial
- highFlag = 2
-
- # ("--Square corners")
- for pt in range(1, lastPnt):
- for lin in range(1, lastLn):
- if TM[lin][pt] == 1: # point == 1
- cont = True
- if TM[lin + 1][pt] == 0: # forward == 0
- if TM[lin + 1][pt - 1] == 1: # forward left == 1
- if TM[lin][pt - 1] == 2: # left == 2
- TM[lin + 1][pt] = 1 # square the corner
- cont = False
-
- if cont is True and TM[lin + 1][pt + 1] == 1: # forward right == 1
- if TM[lin][pt + 1] == 2: # right == 2
- TM[lin + 1][pt] = 1 # square the corner
- cont = True
-
- if TM[lin - 1][pt] == 0: # back == 0
- if TM[lin - 1][pt - 1] == 1: # back left == 1
- if TM[lin][pt - 1] == 2: # left == 2
- TM[lin - 1][pt] = 1 # square the corner
- cont = False
-
- if cont is True and TM[lin - 1][pt + 1] == 1: # back right == 1
- if TM[lin][pt + 1] == 2: # right == 2
- TM[lin - 1][pt] = 1 # square the corner
-
- # remove inside corners
- for pt in range(1, lastPnt):
- for lin in range(1, lastLn):
- if TM[lin][pt] == 1: # point == 1
- if TM[lin][pt + 1] == 1:
- if TM[lin - 1][pt + 1] == 1 or TM[lin + 1][pt + 1] == 1:
- TM[lin][pt + 1] = insCorn
- elif TM[lin][pt - 1] == 1:
- if TM[lin - 1][pt - 1] == 1 or TM[lin + 1][pt - 1] == 1:
- TM[lin][pt - 1] = insCorn
-
- return True
-
- def _extractWaterlines(self, obj, oclScan, lyr, layDep):
- '''_extractWaterlines(obj, oclScan, lyr, layDep) ... Extract water lines from OCL scan data.'''
- srch = True
- lastPnt = len(self.topoMap[0]) - 1
- lastLn = len(self.topoMap) - 1
- maxSrchs = 5
- srchCnt = 1
- loopList = []
- loop = []
- loopNum = 0
-
- if self.CutClimb is True:
- lC = [-1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0]
- pC = [-1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1]
- else:
- lC = [1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0]
- pC = [-1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1]
-
- while srch is True:
- srch = False
- if srchCnt > maxSrchs:
- 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):
- if self.topoMap[L][P] == 1:
- # start loop follow
- srch = True
- loopNum += 1
- loop = self._trackLoop(oclScan, lC, pC, L, P, loopNum)
- self.topoMap[L][P] = 0 # Mute the starting point
- loopList.append(loop)
- srchCnt += 1
- 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]
- nxt = [L, P + 1, 1]
- follow = True
- ptc = 0
- ptLmt = 200000
- while follow is True:
- ptc += 1
- if ptc > ptLmt:
- 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
- self.topoMap[nxt[0]][nxt[1]] = nxt[2] # Mute the point, if not Y stem
- if nxt[0] == L and nxt[1] == P: # check if loop complete
- follow = False
- elif nxt[0] == cur[0] and nxt[1] == cur[1]: # check if line cannot be detected
- follow = False
- prv = cur
- cur = nxt
- return loop
-
- def _findNextWlPoint(self, lC, pC, cl, cp, pl, pp):
- '''_findNextWlPoint(lC, pC, cl, cp, pl, pp) ...
- Find the next waterline point in the point cloud layer provided.'''
- dl = cl - pl
- dp = cp - pp
- num = 0
- i = 3
- s = 0
- mtch = 0
- found = False
- while mtch < 8: # check all 8 points around current point
- if lC[i] == dl:
- if pC[i] == dp:
- s = i - 3
- found = True
- # Check for y branch where current point is connection between branches
- for y in range(1, mtch):
- if lC[i + y] == dl:
- if pC[i + y] == dp:
- num = 1
- break
- break
- i += 1
- mtch += 1
- if found is False:
- # ("_findNext: No start point found.")
- return [cl, cp, num]
-
- for r in range(0, 8):
- l = cl + lC[s + r]
- p = cp + pC[s + r]
- if self.topoMap[l][p] == 1:
- return [l, p, num]
-
- # ("_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 = []
@@ -3971,54 +3621,6 @@ class ObjectSurface(PathOp.ObjectOp):
cmds.append(Path.Command('G0', {'Z': p2.z, 'F': self.vertFeed})) # drop cutter down to current Z depth, returning to normal cut path and speed
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
- cmds.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, '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
- return cmds
-
- def subsectionCLP(self, CLP, xmin, ymin, xmax, ymax):
- '''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:
- if CLP[i].y < ymax:
- if CLP[i].x > xmin:
- if CLP[i].y > ymin:
- section.append(CLP[i])
- return section
-
- 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 # Need to employ a global tolerance here
- m = (p2.y - p1.y) / dx
- b = p1.y - (m * p1.x)
-
- avoidTool = round(cutter * 0.75, 1) # 1/2 diam. of cutter is theoretically safe, but 3/4 diam is used for extra clearance
- zMax = finalDepth
- lenCLP = len(CLP)
- for i in range(0, lenCLP):
- mSqrd = m**2
- 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 resetOpVariables(self, all=True):
'''resetOpVariables() ... Reset class variables used for instance of operation.'''
self.holdPoint = None
@@ -4131,31 +3733,6 @@ class ObjectSurface(PathOp.ObjectOp):
PathLog.error('Unable to set OCL cutter.')
return False
- def determineVectDirect(self, pnt, nxt, travVect):
- 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
- 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)
@@ -4173,7 +3750,6 @@ class ObjectSurface(PathOp.ObjectOp):
def SetupProperties():
''' SetupProperties() ... Return list of properties required for operation.'''
setup = []
- setup.append('Algorithm')
setup.append('AvoidLastX_Faces')
setup.append('AvoidLastX_InternalFeatures')
setup.append('BoundBox')
@@ -4208,12 +3784,7 @@ def SetupProperties():
setup.append('AngularDeflection')
setup.append('LinearDeflection')
# 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/PathWaterline.py b/src/Mod/Path/PathScripts/PathWaterline.py
index db2c2229bd..06c960c8c3 100644
--- a/src/Mod/Path/PathScripts/PathWaterline.py
+++ b/src/Mod/Path/PathScripts/PathWaterline.py
@@ -23,7 +23,7 @@
# ***************************************************************************
# * *
# * Additional modifications and contributions beginning 2019 *
-# * by Russell Johnson 2020-03-15 10:55 CST *
+# * by Russell Johnson 2020-03-23 16:15 CST *
# * *
# ***************************************************************************
@@ -45,7 +45,7 @@ import Draft
if FreeCAD.GuiUp:
import FreeCADGui
-__title__ = "Path Surface Operation"
+__title__ = "Path Waterline Operation"
__author__ = "sliptonic (Brad Collette)"
__url__ = "http://www.freecadweb.org"
__doc__ = "Class and implementation of Mill Facing operation."
@@ -64,12 +64,12 @@ try:
import ocl
except ImportError:
FreeCAD.Console.PrintError(
- translate("Path_Surface", "This operation requires OpenCamLib to be installed.") + "\n")
+ translate("Path_Waterline", "This operation requires OpenCamLib to be installed.") + "\n")
import sys
- sys.exit(translate("Path_Surface", "This operation requires OpenCamLib to be installed."))
+ sys.exit(translate("Path_Waterline", "This operation requires OpenCamLib to be installed."))
-class ObjectSurface(PathOp.ObjectOp):
+class ObjectWaterline(PathOp.ObjectOp):
'''Proxy object for Surfacing operation.'''
def baseObject(self):
@@ -82,122 +82,148 @@ class ObjectSurface(PathOp.ObjectOp):
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", "BoundBox", "Waterline", 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", "LayerMode", "Waterline", QtCore.QT_TRANSLATE_NOOP("App::Property", "The completion mode for the operation: single or multi-pass"))
- obj.addProperty("App::PropertyEnumeration", "ScanType", "Waterline", 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::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::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.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"))
+ '''initPocketOp(obj) ...
+ Initialize the operation - property creation and property editor status.'''
+ self.initOpProperties(obj)
# 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.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.HandleMultipleFeatures = ['Collectively', 'Individually']
- obj.LayerMode = ['Single-pass', 'Multi-pass']
- obj.ProfileEdges = ['None', 'Only', 'First', 'Last']
- obj.RotationAxis = ['X', 'Y']
- obj.ScanType = ['Planar', 'Rotational']
-
if not hasattr(obj, 'DoNotSetDefaultValues'):
self.setEditorProperties(obj)
+ def initOpProperties(self, obj):
+ '''initOpProperties(obj) ... create operation specific properties'''
+
+ PROPS = [
+ ("App::PropertyBool", "ShowTempObjects", "Debug",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "If true, the temporary path construction objects will be shown.")),
+
+ ("App::PropertyDistance", "AngularDeflection", "Mesh Conversion",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Smaller values yield a finer, more accurate the mesh. Smaller values increase processing time a lot.")),
+ ("App::PropertyDistance", "LinearDeflection", "Mesh Conversion",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Smaller values yield a finer, more accurate the mesh. Smaller values do not increase processing time much.")),
+
+ ("App::PropertyInteger", "AvoidLastX_Faces", "Selected Face(s) Settings",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Avoid cutting the last 'N' faces in the Base Geometry list of selected faces.")),
+ ("App::PropertyBool", "AvoidLastX_InternalFeatures", "Selected Face(s) Settings",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Do not cut internal features on avoided faces.")),
+ ("App::PropertyDistance", "BoundaryAdjustment", "Selected Face(s) Settings",
+ 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.")),
+ ("App::PropertyBool", "BoundaryEnforcement", "Selected Face(s) Settings",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "If true, the cutter will remain inside the boundaries of the model or selected face(s).")),
+ ("App::PropertyEnumeration", "HandleMultipleFeatures", "Selected Face(s) Settings",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose how to process multiple Base Geometry features.")),
+ ("App::PropertyDistance", "InternalFeaturesAdjustment", "Selected Face(s) Settings",
+ 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.")),
+ ("App::PropertyBool", "InternalFeaturesCut", "Selected Face(s) Settings",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Ignore internal feature areas within a larger selected face.")),
+
+ ("App::PropertyEnumeration", "Algorithm", "Waterline",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Select the algorithm to use: OCL Dropcutter*, or Experimental.")),
+ ("App::PropertyEnumeration", "BoundBox", "Waterline",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Should the operation be limited by the stock object or by the bounding box of the base object")),
+ ("App::PropertyDistance", "SampleInterval", "Waterline",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "The Sample Interval. Small values cause long wait times")),
+
+ ("App::PropertyVectorDistance", "CircularCenterCustom", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "The start point of this path")),
+ ("App::PropertyEnumeration", "CircularCenterAt", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose what point to start the ciruclar pattern: Center Of Mass, Center Of Boundbox, Xmin Ymin of boundbox, Custom.")),
+ ("App::PropertyEnumeration", "CutMode", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "The direction that the toolpath should go around the part: Climb(ClockWise) or Conventional(CounterClockWise)")),
+ ("App::PropertyEnumeration", "CutPattern", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Clearing pattern to use")),
+ ("App::PropertyFloat", "CutPatternAngle", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Yaw angle for certain clearing patterns")),
+ ("App::PropertyBool", "CutPatternReversed", "Clearing 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.")),
+ ("App::PropertyDistance", "DepthOffset", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Z-axis offset from the surface of the object")),
+ ("App::PropertyEnumeration", "LayerMode", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "The completion mode for the operation: single or multi-pass")),
+ ("App::PropertyEnumeration", "ProfileEdges", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Profile the edges of the selection.")),
+ ("App::PropertyPercent", "StepOver", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Step over percentage of the drop cutter path")),
+
+ ("App::PropertyBool", "OptimizeLinearPaths", "Waterline Optimization",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable optimization of linear paths (co-linear points). Removes unnecessary co-linear points from G-Code output.")),
+ ("App::PropertyBool", "OptimizeStepOverTransitions", "Waterline Optimization",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable separate optimization of transitions between, and breaks within, each step over path.")),
+ ("App::PropertyBool", "CircularUseG2G3", "Waterline Optimization",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Convert co-planar arcs to G2/G3 gcode commands for `Circular` and `CircularZigZag` cut patterns.")),
+ ("App::PropertyDistance", "GapThreshold", "Waterline Optimization",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Collinear and co-radial artifact gaps that are smaller than this threshold are closed in the path.")),
+ ("App::PropertyString", "GapSizes", "Waterline Optimization",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Feedback: three smallest gaps identified in the path geometry.")),
+
+ ("App::PropertyVectorDistance", "StartPoint", "Start Point",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "The start point of this path")),
+ ("App::PropertyBool", "UseStartPoint", "Start Point",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Make True, if specifying a Start Point"))
+ ]
+
+ missing = list()
+ for (prtyp, nm, grp, tt) in PROPS:
+ if not hasattr(obj, nm):
+ obj.addProperty(prtyp, nm, grp, tt)
+ missing.append(nm)
+
+ # Set enumeration lists for enumeration properties
+ if len(missing) > 0:
+ ENUMS = self._propertyEnumerations()
+ for n in ENUMS:
+ if n in missing:
+ cmdStr = 'obj.{}={}'.format(n, ENUMS[n])
+ exec(cmdStr)
+
self.addedAllProperties = True
+ def _propertyEnumerations(self):
+ # Enumeration lists for App::PropertyEnumeration properties
+ return {
+ 'Algorithm': ['OCL Dropcutter', 'Experimental'],
+ 'BoundBox': ['BaseBoundBox', 'Stock'],
+ 'CircularCenterAt': ['CenterOfMass', 'CenterOfBoundBox', 'XminYmin', 'Custom'],
+ 'CutMode': ['Conventional', 'Climb'],
+ 'CutPattern': ['Line', 'ZigZag', 'Circular', 'CircularZigZag'], # Additional goals ['Offset', 'Spiral', 'ZigZagOffset', 'Grid', 'Triangle']
+ 'HandleMultipleFeatures': ['Collectively', 'Individually'],
+ 'LayerMode': ['Single-pass', 'Multi-pass'],
+ 'ProfileEdges': ['None', 'Only', 'First', 'Last'],
+ }
+
def setEditorProperties(self, obj):
# Used to hide inputs in properties list
-
- '''
- 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('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('RotationAxis', 0) # 0=show & editable
- obj.setEditorMode('StartIndex', 0)
- obj.setEditorMode('StopIndex', 0)
- obj.setEditorMode('CutterTilt', 0)
- '''
-
- 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)
+ show = 0
+ hide = 2
+ obj.setEditorMode('BoundaryEnforcement', hide)
+ obj.setEditorMode('ProfileEdges', hide)
+ obj.setEditorMode('InternalFeaturesAdjustment', hide)
+ obj.setEditorMode('InternalFeaturesCut', hide)
+ # if obj.CutPattern in ['Line', 'ZigZag']:
+ if obj.CutPattern in ['Circular', 'CircularZigZag']:
+ show = 2 # hide
+ hide = 0 # show
+ obj.setEditorMode('CutPatternAngle', show)
+ obj.setEditorMode('CircularCenterAt', hide)
+ obj.setEditorMode('CircularCenterCustom', hide)
def onChanged(self, obj, prop):
if hasattr(self, 'addedAllProperties'):
if self.addedAllProperties is True:
- if prop == 'ScanType':
- self.setEditorProperties(obj)
if prop == 'CutPattern':
self.setEditorProperties(obj)
def opOnDocumentRestored(self, obj):
+ self.initOpProperties(obj)
+
if PathLog.getLevel(PathLog.thisModule()) != 4:
obj.setEditorMode('ShowTempObjects', 2) # hide
else:
obj.setEditorMode('ShowTempObjects', 0) # show
- self.addedAllProperties = True
+
self.setEditorProperties(obj)
def opSetDefaultValues(self, obj, job):
@@ -205,8 +231,6 @@ class ObjectSurface(PathOp.ObjectOp):
job = PathUtils.findParentJob(obj)
obj.OptimizeLinearPaths = True
- obj.IgnoreWaste = False
- obj.ReleaseFromWaste = False
obj.InternalFeaturesCut = True
obj.OptimizeStepOverTransitions = False
obj.CircularUseG2G3 = False
@@ -217,21 +241,16 @@ class ObjectSurface(PathOp.ObjectOp):
obj.StartPoint.x = 0.0
obj.StartPoint.y = 0.0
obj.StartPoint.z = obj.ClearanceHeight.Value
+ obj.Algorithm = 'OCL Dropcutter'
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
@@ -266,39 +285,21 @@ class ObjectSurface(PathOp.ObjectOp):
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
- if obj.StartIndex > 360.0:
- obj.StartIndex = 360.0
-
- # Limit stop index
- if obj.StopIndex > 360.0:
- obj.StopIndex = 360.0
- if obj.StopIndex < 0.0:
- obj.StopIndex = 0.0
-
- # Limit cutter tilt
- if obj.CutterTilt < -90.0:
- obj.CutterTilt = -90.0
- if obj.CutterTilt > 90.0:
- obj.CutterTilt = 90.0
-
# Limit sample interval
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.'))
+ PathLog.error(translate('PathWaterline', '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.'))
+ PathLog.error(translate('PathWaterline', '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.'))
+ PathLog.error(translate('PathWaterline', '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.'))
+ PathLog.error(translate('PathWaterline', 'Cut pattern angle limits are +- 360 degrees.'))
# Limit StepOver to natural number percentage
if obj.StepOver > 100:
@@ -309,10 +310,10 @@ class ObjectSurface(PathOp.ObjectOp):
# 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.'))
+ PathLog.error(translate('PathWaterline', '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.'))
+ PathLog.error(translate('PathWaterline', 'AvoidLastX_Faces: Avoid last X faces count limited to 100.'))
def opExecute(self, obj):
'''opExecute(obj) ... process surface operation'''
@@ -345,16 +346,13 @@ class ObjectSurface(PathOp.ObjectOp):
self.showDebugObjects = False
# mark beginning of operation and identify parent Job
- PathLog.info('\nBegin 3D Surface operation...')
+ PathLog.info('\nBegin Waterline 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"))
+ PathLog.error(translate('PathWaterline', "No JOB"))
return
self.stockZMin = JOB.Stock.Shape.BoundBox.ZMin
@@ -390,7 +388,7 @@ class ObjectSurface(PathOp.ObjectOp):
# Create temporary group for temporary objects, removing existing
# if self.showDebugObjects is True:
- tempGroupName = 'tempPathSurfaceGroup'
+ tempGroupName = 'tempPathWaterlineGroup'
if FCAD.getObject(tempGroupName):
for to in FCAD.getObject(tempGroupName).Group:
FCAD.removeObject(to.Name)
@@ -410,7 +408,7 @@ class ObjectSurface(PathOp.ObjectOp):
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."))
+ PathLog.error(translate('PathWaterline', "Canceling Waterline operation. Error creating OCL cutter."))
return
toolDiam = self.cutter.getDiameter()
self.cutOut = (toolDiam * (float(obj.StepOver) / 100.0))
@@ -463,18 +461,6 @@ class ObjectSurface(PathOp.ObjectOp):
# ###### MAIN COMMANDS FOR OPERATION ######
- # 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
- '''
-
# Begin processing obj.Base data and creating GCode
# Process selected faces, if available
pPM = self._preProcessModel(JOB, obj)
@@ -505,14 +491,6 @@ class ObjectSurface(PathOp.ObjectOp):
# 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
@@ -593,8 +571,8 @@ class ObjectSurface(PathOp.ObjectOp):
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')
+ preProcEr = translate('PathWaterline', 'Error pre-processing Face')
+ warnFinDep = translate('PathWaterline', 'Final Depth might need to be lower. Internal features detected in Face')
GRP = JOB.Model.Group
lenGRP = len(GRP)
@@ -936,8 +914,8 @@ class ObjectSurface(PathOp.ObjectOp):
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')
+ # preProcEr = translate('PathWaterline', 'Error pre-processing Face')
+ warnFinDep = translate('PathWaterline', 'Final Depth might need to be lower. Internal features detected in Face')
PathLog.debug('_getFaceWires() from Face{}'.format(fNum))
WIRES = self._extractWiresFromFace(base, fcshp)
@@ -1140,10 +1118,6 @@ class ObjectSurface(PathOp.ObjectOp):
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:
@@ -1514,7 +1488,7 @@ class ObjectSurface(PathOp.ObjectOp):
def _processCutAreas(self, JOB, obj, mdlIdx, FCS, VDS):
'''_processCutAreas(JOB, obj, mdlIdx, FCS, VDS)...
This method applies any avoided faces or regions to the selected faces.
- It then calls the correct scan method depending on the ScanType property.'''
+ It then calls the correct method.'''
PathLog.debug('_processCutAreas()')
final = list()
@@ -1533,7 +1507,10 @@ class ObjectSurface(PathOp.ObjectOp):
COMP = ADD
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
+ if obj.Algorithm == 'OCL Dropcutter':
+ final.extend(self._oclWaterlineOp(JOB, obj, mdlIdx, COMP)) # independent method set for Waterline
+ else:
+ final.extend(self._experimentalWaterlineOp(JOB, obj, mdlIdx, COMP)) # independent method set for Waterline
elif obj.HandleMultipleFeatures == 'Individually':
for fsi in range(0, len(FCS)):
@@ -1552,7 +1529,10 @@ class ObjectSurface(PathOp.ObjectOp):
COMP = ADD
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
+ if obj.Algorithm == 'OCL Dropcutter':
+ final.extend(self._oclWaterlineOp(JOB, obj, mdlIdx, COMP)) # independent method set for Waterline
+ else:
+ final.extend(self._experimentalWaterlineOp(JOB, obj, mdlIdx, COMP)) # independent method set for Waterline
COMP = None
# Eif
@@ -1570,8 +1550,8 @@ class ObjectSurface(PathOp.ObjectOp):
return pdc
# Main waterline functions
- def _waterlineOp(self, JOB, obj, mdlIdx, subShp=None):
- '''_waterlineOp(obj, base) ... Main waterline function to perform waterline extraction from model.'''
+ def _oclWaterlineOp(self, JOB, obj, mdlIdx, subShp=None):
+ '''_oclWaterlineOp(obj, base) ... Main waterline function to perform waterline extraction from model.'''
commands = []
t_begin = time.time()
@@ -1955,6 +1935,10 @@ class ObjectSurface(PathOp.ObjectOp):
return output
+ def _experimentalWaterlineOp(self, JOB, obj, mdlIdx, subShp=None):
+ PathLog.error('The `Experimental` algorithm is not available at this time.')
+ return []
+
# 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.'''
@@ -1987,54 +1971,6 @@ class ObjectSurface(PathOp.ObjectOp):
cmds.append(Path.Command('G0', {'Z': p2.z, 'F': self.vertFeed})) # drop cutter down to current Z depth, returning to normal cut path and speed
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
- cmds.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, '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
- return cmds
-
- def subsectionCLP(self, CLP, xmin, ymin, xmax, ymax):
- '''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:
- if CLP[i].y < ymax:
- if CLP[i].x > xmin:
- if CLP[i].y > ymin:
- section.append(CLP[i])
- return section
-
- 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 # Need to employ a global tolerance here
- m = (p2.y - p1.y) / dx
- b = p1.y - (m * p1.x)
-
- avoidTool = round(cutter * 0.75, 1) # 1/2 diam. of cutter is theoretically safe, but 3/4 diam is used for extra clearance
- zMax = finalDepth
- lenCLP = len(CLP)
- for i in range(0, lenCLP):
- mSqrd = m**2
- 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 resetOpVariables(self, all=True):
'''resetOpVariables() ... Reset class variables used for instance of operation.'''
self.holdPoint = None
@@ -2147,31 +2083,6 @@ class ObjectSurface(PathOp.ObjectOp):
PathLog.error('Unable to set OCL cutter.')
return False
- def determineVectDirect(self, pnt, nxt, travVect):
- 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
- 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)
@@ -2189,6 +2100,7 @@ class ObjectSurface(PathOp.ObjectOp):
def SetupProperties():
''' SetupProperties() ... Return list of properties required for operation.'''
setup = []
+ setup.append('Algorithm')
setup.append('AvoidLastX_Faces')
setup.append('AvoidLastX_InternalFeatures')
setup.append('BoundBox')
@@ -2202,7 +2114,6 @@ def SetupProperties():
setup.append('CutPattern')
setup.append('CutPatternAngle')
setup.append('CutPatternReversed')
- setup.append('CutterTilt')
setup.append('DepthOffset')
setup.append('GapSizes')
setup.append('GapThreshold')
@@ -2211,27 +2122,18 @@ def SetupProperties():
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
def Create(name, obj=None):
- '''Create(name) ... Creates and returns a Surface operation.'''
+ '''Create(name) ... Creates and returns a Waterline operation.'''
if obj is None:
obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name)
- obj.Proxy = ObjectSurface(obj, name)
+ obj.Proxy = ObjectWaterline(obj, name)
return obj
From d495ee202dbf1744fa538eb02e09989f92d257d3 Mon Sep 17 00:00:00 2001
From: Russell Johnson <47639332+Russ4262@users.noreply.github.com>
Date: Thu, 26 Mar 2020 16:05:13 -0500
Subject: [PATCH 10/18] Path: Add `ScanType` and `LayerMode` inputs
Added combo boxes with labels
Path: Remove 'Algorithm' property usage.
Path: Connect `ScanType` and `LayerMode' selections from task panel
Path: Update GUI inputs for independent 3D Surface operation
---
.../Gui/Resources/panels/PageOpSurfaceEdit.ui | 292 ++++++++++--------
src/Mod/Path/PathScripts/PathSurfaceGui.py | 83 +++--
2 files changed, 217 insertions(+), 158 deletions(-)
diff --git a/src/Mod/Path/Gui/Resources/panels/PageOpSurfaceEdit.ui b/src/Mod/Path/Gui/Resources/panels/PageOpSurfaceEdit.ui
index d019e3fe67..7013dd6e28 100644
--- a/src/Mod/Path/Gui/Resources/panels/PageOpSurfaceEdit.ui
+++ b/src/Mod/Path/Gui/Resources/panels/PageOpSurfaceEdit.ui
@@ -6,8 +6,8 @@
0
0
- 357
- 427
+ 350
+ 400
@@ -57,126 +57,7 @@
-
-
-
-
-
- Algorithm
-
-
-
- -
-
-
-
-
- OCL Dropcutter
-
-
- -
-
- OCL Waterline
-
-
-
-
- -
-
-
- BoundBox
-
-
-
- -
-
-
-
-
- Stock
-
-
- -
-
- BaseBoundBox
-
-
-
-
- -
-
-
- BoundBox extra offset X, Y
-
-
-
- -
-
-
- mm
-
-
-
- -
-
-
- mm
-
-
-
- -
-
-
- Drop Cutter Direction
-
-
-
- -
-
-
-
-
- X
-
-
- -
-
- Y
-
-
-
-
- -
-
-
- Depth offset
-
-
-
- -
-
-
- mm
-
-
-
- -
-
-
- Sample interval
-
-
-
- -
-
-
- mm
-
-
-
- -
-
-
- Step over
-
-
-
- -
+
-
<html><head/><body><p>The amount by which the tool is laterally displaced on each cycle of the pattern, specified in percent of the tool diameter.</p><p>A step over of 100% results in no overlap between two different cycles.</p></body></html>
@@ -195,17 +76,174 @@
- -
-
+
-
+
- Optimize output
+ BoundBox
- -
+
-
+
+
+ Layer Mode
+
+
+
+ -
+
+
+ Depth offset
+
+
+
+ -
+
+
+ mm
+
+
+
+ -
+
+
+ Sample interval
+
+
+
+ -
+
+
-
+
+ X
+
+
+ -
+
+ Y
+
+
+
+
+ -
+
+
+ Step over
+
+
+
+ -
+
+
-
+
+ Planar
+
+
+ -
+
+ Rotational
+
+
+
+
+ -
+
+
-
+
+ Single-pass
+
+
+ -
+
+ Multi-pass
+
+
+
+
+ -
- Enabled
+ Optimize Linear Paths
+
+
+
+ -
+
+
-
+
+ Stock
+
+
+ -
+
+ BaseBoundBox
+
+
+
+
+ -
+
+
+ Optimize StepOver Transitions
+
+
+
+ -
+
+
+ mm
+
+
+
+ -
+
+
+ Scan Type
+
+
+
+ -
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ mm
+
+
+
+ -
+
+
+ mm
+
+
+
+
+
+ -
+
+
+ Drop Cutter Direction
+
+
+
+ -
+
+
+ BoundBox extra offset X, Y
+
+
+
+ -
+
+
+ Use Start Point
diff --git a/src/Mod/Path/PathScripts/PathSurfaceGui.py b/src/Mod/Path/PathScripts/PathSurfaceGui.py
index 40feff2c70..f11bdd4864 100644
--- a/src/Mod/Path/PathScripts/PathSurfaceGui.py
+++ b/src/Mod/Path/PathScripts/PathSurfaceGui.py
@@ -45,83 +45,104 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
def getFields(self, obj):
'''getFields(obj) ... transfers values from UI to obj's proprties'''
+ self.updateToolController(obj, self.form.toolController)
+ self.updateCoolant(obj, self.form.coolantController)
+
PathGui.updateInputField(obj, 'DepthOffset', self.form.depthOffset)
PathGui.updateInputField(obj, 'SampleInterval', self.form.sampleInterval)
- if obj.StepOver != self.form.stepOver.value():
- obj.StepOver = self.form.stepOver.value()
-
- if obj.Algorithm != str(self.form.algorithmSelect.currentText()):
- obj.Algorithm = str(self.form.algorithmSelect.currentText())
-
if obj.BoundBox != str(self.form.boundBoxSelect.currentText()):
obj.BoundBox = str(self.form.boundBoxSelect.currentText())
- if obj.DropCutterDir != str(self.form.dropCutterDirSelect.currentText()):
- obj.DropCutterDir = str(self.form.dropCutterDirSelect.currentText())
+ if obj.ScanType != str(self.form.scanType.currentText()):
+ obj.ScanType = str(self.form.scanType.currentText())
+
+ if obj.StepOver != self.form.stepOver.value():
+ obj.StepOver = self.form.stepOver.value()
+
+ if obj.LayerMode != str(self.form.layerMode.currentText()):
+ obj.LayerMode = str(self.form.layerMode.currentText())
obj.DropCutterExtraOffset.x = FreeCAD.Units.Quantity(self.form.boundBoxExtraOffsetX.text()).Value
obj.DropCutterExtraOffset.y = FreeCAD.Units.Quantity(self.form.boundBoxExtraOffsetY.text()).Value
+ if obj.DropCutterDir != str(self.form.dropCutterDirSelect.currentText()):
+ obj.DropCutterDir = str(self.form.dropCutterDirSelect.currentText())
+
+ PathGui.updateInputField(obj, 'DepthOffset', self.form.depthOffset)
+ PathGui.updateInputField(obj, 'SampleInterval', self.form.sampleInterval)
+
+ if obj.UseStartPoint != self.form.useStartPoint.isChecked():
+ obj.UseStartPoint = self.form.useStartPoint.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)
+ if obj.OptimizeStepOverTransitions != self.form.optimizeStepOverTransitions.isChecked():
+ obj.OptimizeStepOverTransitions = self.form.optimizeStepOverTransitions.isChecked()
def setFields(self, obj):
'''setFields(obj) ... transfers obj's property values to UI'''
- self.selectInComboBox(obj.Algorithm, self.form.algorithmSelect)
+ self.setupToolController(obj, self.form.toolController)
+ self.setupCoolant(obj, self.form.coolantController)
self.selectInComboBox(obj.BoundBox, self.form.boundBoxSelect)
- self.selectInComboBox(obj.DropCutterDir, self.form.dropCutterDirSelect)
-
+ self.selectInComboBox(obj.ScanType, self.form.scanType)
+ self.selectInComboBox(obj.LayerMode, self.form.layerMode)
self.form.boundBoxExtraOffsetX.setText(str(obj.DropCutterExtraOffset.x))
self.form.boundBoxExtraOffsetY.setText(str(obj.DropCutterExtraOffset.y))
+ self.selectInComboBox(obj.DropCutterDir, self.form.dropCutterDirSelect)
self.form.depthOffset.setText(FreeCAD.Units.Quantity(obj.DepthOffset.Value, FreeCAD.Units.Length).UserString)
- self.form.sampleInterval.setText(str(obj.SampleInterval))
self.form.stepOver.setValue(obj.StepOver)
+ self.form.sampleInterval.setText(str(obj.SampleInterval))
+
+ if obj.UseStartPoint:
+ self.form.useStartPoint.setCheckState(QtCore.Qt.Checked)
+ else:
+ self.form.useStartPoint.setCheckState(QtCore.Qt.Unchecked)
if obj.OptimizeLinearPaths:
self.form.optimizeEnabled.setCheckState(QtCore.Qt.Checked)
else:
self.form.optimizeEnabled.setCheckState(QtCore.Qt.Unchecked)
- self.setupToolController(obj, self.form.toolController)
- self.setupCoolant(obj, self.form.coolantController)
+ if obj.OptimizeStepOverTransitions:
+ self.form.optimizeStepOverTransitions.setCheckState(QtCore.Qt.Checked)
+ else:
+ self.form.optimizeStepOverTransitions.setCheckState(QtCore.Qt.Unchecked)
+
def getSignalsForUpdate(self, obj):
'''getSignalsForUpdate(obj) ... return list of signals for updating obj'''
signals = []
signals.append(self.form.toolController.currentIndexChanged)
- signals.append(self.form.algorithmSelect.currentIndexChanged)
+ signals.append(self.form.coolantController.currentIndexChanged)
signals.append(self.form.boundBoxSelect.currentIndexChanged)
- signals.append(self.form.dropCutterDirSelect.currentIndexChanged)
+ signals.append(self.form.scanType.currentIndexChanged)
+ signals.append(self.form.layerMode.currentIndexChanged)
signals.append(self.form.boundBoxExtraOffsetX.editingFinished)
signals.append(self.form.boundBoxExtraOffsetY.editingFinished)
- signals.append(self.form.sampleInterval.editingFinished)
- signals.append(self.form.stepOver.editingFinished)
+ signals.append(self.form.dropCutterDirSelect.currentIndexChanged)
signals.append(self.form.depthOffset.editingFinished)
+ signals.append(self.form.stepOver.editingFinished)
+ signals.append(self.form.sampleInterval.editingFinished)
+ signals.append(self.form.useStartPoint.stateChanged)
signals.append(self.form.optimizeEnabled.stateChanged)
- signals.append(self.form.coolantController.currentIndexChanged)
+ signals.append(self.form.optimizeStepOverTransitions.stateChanged)
return signals
def updateVisibility(self):
- if self.form.algorithmSelect.currentText() == "OCL Dropcutter":
- self.form.boundBoxExtraOffsetX.setEnabled(True)
- self.form.boundBoxExtraOffsetY.setEnabled(True)
- self.form.boundBoxSelect.setEnabled(True)
- self.form.dropCutterDirSelect.setEnabled(True)
- self.form.stepOver.setEnabled(True)
- else:
+ if self.form.scanType.currentText() == "Planar":
self.form.boundBoxExtraOffsetX.setEnabled(False)
self.form.boundBoxExtraOffsetY.setEnabled(False)
- self.form.boundBoxSelect.setEnabled(False)
self.form.dropCutterDirSelect.setEnabled(False)
- self.form.stepOver.setEnabled(False)
+ else:
+ self.form.boundBoxExtraOffsetX.setEnabled(True)
+ self.form.boundBoxExtraOffsetY.setEnabled(True)
+ self.form.dropCutterDirSelect.setEnabled(True)
def registerSignalHandlers(self, obj):
- self.form.algorithmSelect.currentIndexChanged.connect(self.updateVisibility)
+ self.form.scanType.currentIndexChanged.connect(self.updateVisibility)
Command = PathOpGui.SetupOperation('Surface',
From 33636396819bc6023524cfcee45bf7073873e398 Mon Sep 17 00:00:00 2001
From: Russell Johnson <47639332+Russ4262@users.noreply.github.com>
Date: Thu, 26 Mar 2020 16:05:36 -0500
Subject: [PATCH 11/18] Path: Update GUI inputs for independent Waterline
operation
---
.../Resources/panels/PageOpWaterlineEdit.ui | 229 +++++++++++-------
src/Mod/Path/PathScripts/PathWaterlineGui.py | 52 +++-
2 files changed, 185 insertions(+), 96 deletions(-)
diff --git a/src/Mod/Path/Gui/Resources/panels/PageOpWaterlineEdit.ui b/src/Mod/Path/Gui/Resources/panels/PageOpWaterlineEdit.ui
index e9c94ac535..5e0edef1c9 100644
--- a/src/Mod/Path/Gui/Resources/panels/PageOpWaterlineEdit.ui
+++ b/src/Mod/Path/Gui/Resources/panels/PageOpWaterlineEdit.ui
@@ -6,7 +6,7 @@
0
0
- 400
+ 350
400
@@ -43,37 +43,100 @@
-
-
-
-
-
- mm
-
-
-
- -
-
-
- Cut Pattern
-
-
-
- -
-
-
- Sample interval
-
-
-
-
-
-
- -
-
+
- 9
+ 8
+
-
+
+ Stock
+
+
+ -
+
+ BaseBoundBox
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ -
+
+
+
+ 8
+
+
+
-
+
+ Single-pass
+
+
+ -
+
+ Multi-pass
+
+
+
+
+ -
+
+
-
+
+ OCL Dropcutter
+
+
+ -
+
+ Experimental
+
+
+
+
+ -
+
+
+ Optimize Linear Paths
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Boundary Adjustment
+
+
+
+ -
+
+
+
+ 8
+
+
+
-
+
+ None
+
+
-
Line
@@ -96,62 +159,14 @@
- -
-
-
- Depth offset
-
-
-
- -
-
-
-
- 9
-
-
-
-
-
- Stock
-
-
- -
-
- BaseBoundBox
-
-
-
-
- -
-
-
- mm
-
-
-
-
-
-
- Optimize Linear Paths
-
-
-
- -
-
-
- BoundBox
-
-
-
- -
-
-
- Boundary Adjustment
-
-
-
- -
+
+
+ 0
+ 0
+
+
<html><head/><body><p>The amount by which the tool is laterally displaced on each cycle of the pattern, specified in percent of the tool diameter.</p><p>A step over of 100% results in no overlap between two different cycles.</p></body></html>
@@ -169,13 +184,61 @@
- -
-
+
-
+
+
+ Layer Mode
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ BoundBox
+
+
+
+ -
+
Step over
+ -
+
+
+ mm
+
+
+
+ -
+
+
+ Cut Pattern
+
+
+
+ -
+
+
+ Sample interval
+
+
+
+ -
+
+
+ Algorithm
+
+
+
diff --git a/src/Mod/Path/PathScripts/PathWaterlineGui.py b/src/Mod/Path/PathScripts/PathWaterlineGui.py
index ece048ecee..7a631efd11 100644
--- a/src/Mod/Path/PathScripts/PathWaterlineGui.py
+++ b/src/Mod/Path/PathScripts/PathWaterlineGui.py
@@ -46,24 +46,37 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
def getFields(self, obj):
'''getFields(obj) ... transfers values from UI to obj's proprties'''
- PathGui.updateInputField(obj, 'BoundaryAdjustment', self.form.boundaryAdjustment)
- PathGui.updateInputField(obj, 'SampleInterval', self.form.sampleInterval)
+ self.updateToolController(obj, self.form.toolController)
- if obj.StepOver != self.form.stepOver.value():
- obj.StepOver = self.form.stepOver.value()
+ if obj.Algorithm != str(self.form.algorithmSelect.currentText()):
+ obj.Algorithm = str(self.form.algorithmSelect.currentText())
if obj.BoundBox != str(self.form.boundBoxSelect.currentText()):
obj.BoundBox = str(self.form.boundBoxSelect.currentText())
+ if obj.LayerMode != str(self.form.layerMode.currentText()):
+ obj.LayerMode = str(self.form.layerMode.currentText())
+
+ if obj.CutPattern != str(self.form.cutPattern.currentText()):
+ obj.CutPattern = str(self.form.cutPattern.currentText())
+
+ PathGui.updateInputField(obj, 'BoundaryAdjustment', self.form.boundaryAdjustment)
+
+ if obj.StepOver != self.form.stepOver.value():
+ obj.StepOver = self.form.stepOver.value()
+
+ PathGui.updateInputField(obj, 'SampleInterval', self.form.sampleInterval)
+
if obj.OptimizeLinearPaths != self.form.optimizeEnabled.isChecked():
obj.OptimizeLinearPaths = self.form.optimizeEnabled.isChecked()
- self.updateToolController(obj, self.form.toolController)
-
def setFields(self, obj):
'''setFields(obj) ... transfers obj's property values to UI'''
self.setupToolController(obj, self.form.toolController)
+ self.selectInComboBox(obj.Algorithm, self.form.algorithmSelect)
self.selectInComboBox(obj.BoundBox, self.form.boundBoxSelect)
+ self.selectInComboBox(obj.LayerMode, self.form.layerMode)
+ self.selectInComboBox(obj.CutPattern, self.form.cutPattern)
self.form.boundaryAdjustment.setText(FreeCAD.Units.Quantity(obj.BoundaryAdjustment.Value, FreeCAD.Units.Length).UserString)
self.form.stepOver.setValue(obj.StepOver)
self.form.sampleInterval.setText(str(obj.SampleInterval))
@@ -77,7 +90,10 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
'''getSignalsForUpdate(obj) ... return list of signals for updating obj'''
signals = []
signals.append(self.form.toolController.currentIndexChanged)
+ signals.append(self.form.algorithmSelect.currentIndexChanged)
signals.append(self.form.boundBoxSelect.currentIndexChanged)
+ signals.append(self.form.layerMode.currentIndexChanged)
+ signals.append(self.form.cutPattern.currentIndexChanged)
signals.append(self.form.boundaryAdjustment.editingFinished)
signals.append(self.form.stepOver.editingFinished)
signals.append(self.form.sampleInterval.editingFinished)
@@ -86,15 +102,25 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
return signals
def updateVisibility(self):
- self.form.boundBoxSelect.setEnabled(True)
- self.form.boundaryAdjustment.setEnabled(True)
- self.form.stepOver.setEnabled(True)
- self.form.sampleInterval.setEnabled(True)
- self.form.optimizeEnabled.setEnabled(True)
+ if self.form.algorithmSelect.currentText() == 'OCL Dropcutter':
+ self.form.cutPattern.setEnabled(False)
+ self.form.boundaryAdjustment.setEnabled(False)
+ self.form.stepOver.setEnabled(False)
+ self.form.sampleInterval.setEnabled(True)
+ self.form.optimizeEnabled.setEnabled(True)
+ else:
+ self.form.cutPattern.setEnabled(True)
+ self.form.boundaryAdjustment.setEnabled(True)
+ if self.form.cutPattern.currentText() == 'None':
+ self.form.stepOver.setEnabled(False)
+ else:
+ self.form.stepOver.setEnabled(True)
+ self.form.sampleInterval.setEnabled(False)
+ self.form.optimizeEnabled.setEnabled(False)
def registerSignalHandlers(self, obj):
- # self.form.clearLayers.currentIndexChanged.connect(self.updateVisibility)
- pass
+ self.form.algorithmSelect.currentIndexChanged.connect(self.updateVisibility)
+ self.form.cutPattern.currentIndexChanged.connect(self.updateVisibility)
Command = PathOpGui.SetupOperation('Waterline',
From 3720b57d63586eb2a9be9f816962298a08de760f Mon Sep 17 00:00:00 2001
From: Russell Johnson <47639332+Russ4262@users.noreply.github.com>
Date: Thu, 26 Mar 2020 18:07:02 -0500
Subject: [PATCH 12/18] Path: Add `initPage()` call to update GUI upon loading
Added initPage() function call updates task panel values and visibilities when user edits an existing operation.
---
src/Mod/Path/PathScripts/PathSurfaceGui.py | 5 ++++-
src/Mod/Path/PathScripts/PathWaterlineGui.py | 4 ++++
2 files changed, 8 insertions(+), 1 deletion(-)
diff --git a/src/Mod/Path/PathScripts/PathSurfaceGui.py b/src/Mod/Path/PathScripts/PathSurfaceGui.py
index f11bdd4864..5709c0341e 100644
--- a/src/Mod/Path/PathScripts/PathSurfaceGui.py
+++ b/src/Mod/Path/PathScripts/PathSurfaceGui.py
@@ -39,6 +39,10 @@ __doc__ = "Surface operation page controller and command implementation."
class TaskPanelOpPage(PathOpGui.TaskPanelPage):
'''Page controller class for the Surface operation.'''
+ def initPage(self, obj):
+ self.setTitle("3D Surface")
+ self.updateVisibility()
+
def getForm(self):
'''getForm() ... returns UI'''
return FreeCADGui.PySideUic.loadUi(":/panels/PageOpSurfaceEdit.ui")
@@ -110,7 +114,6 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
else:
self.form.optimizeStepOverTransitions.setCheckState(QtCore.Qt.Unchecked)
-
def getSignalsForUpdate(self, obj):
'''getSignalsForUpdate(obj) ... return list of signals for updating obj'''
signals = []
diff --git a/src/Mod/Path/PathScripts/PathWaterlineGui.py b/src/Mod/Path/PathScripts/PathWaterlineGui.py
index 7a631efd11..cf72300d46 100644
--- a/src/Mod/Path/PathScripts/PathWaterlineGui.py
+++ b/src/Mod/Path/PathScripts/PathWaterlineGui.py
@@ -40,6 +40,10 @@ __doc__ = "Waterline operation page controller and command implementation."
class TaskPanelOpPage(PathOpGui.TaskPanelPage):
'''Page controller class for the Waterline operation.'''
+ def initPage(self, obj):
+ # self.setTitle("Waterline")
+ self.updateVisibility()
+
def getForm(self):
'''getForm() ... returns UI'''
return FreeCADGui.PySideUic.loadUi(":/panels/PageOpWaterlineEdit.ui")
From 11db9553d4ce5c805e59998557604a6a4c73507b Mon Sep 17 00:00:00 2001
From: Russell Johnson <47639332+Russ4262@users.noreply.github.com>
Date: Fri, 27 Mar 2020 14:14:36 -0500
Subject: [PATCH 13/18] Path: Add `Cut Pattern` selection to task panel
---
.../Gui/Resources/panels/PageOpSurfaceEdit.ui | 251 ++++++++++--------
src/Mod/Path/PathScripts/PathSurfaceGui.py | 7 +
2 files changed, 148 insertions(+), 110 deletions(-)
diff --git a/src/Mod/Path/Gui/Resources/panels/PageOpSurfaceEdit.ui b/src/Mod/Path/Gui/Resources/panels/PageOpSurfaceEdit.ui
index 7013dd6e28..e4aaf19e5e 100644
--- a/src/Mod/Path/Gui/Resources/panels/PageOpSurfaceEdit.ui
+++ b/src/Mod/Path/Gui/Resources/panels/PageOpSurfaceEdit.ui
@@ -24,7 +24,7 @@
-
-
+
ToolController
@@ -38,7 +38,7 @@
-
-
+
Coolant Mode
@@ -57,81 +57,6 @@
-
-
-
-
-
- <html><head/><body><p>The amount by which the tool is laterally displaced on each cycle of the pattern, specified in percent of the tool diameter.</p><p>A step over of 100% results in no overlap between two different cycles.</p></body></html>
-
-
- 1
-
-
- 100
-
-
- 10
-
-
- 100
-
-
-
- -
-
-
- BoundBox
-
-
-
- -
-
-
- Layer Mode
-
-
-
- -
-
-
- Depth offset
-
-
-
- -
-
-
- mm
-
-
-
- -
-
-
- Sample interval
-
-
-
- -
-
-
-
-
- X
-
-
- -
-
- Y
-
-
-
-
- -
-
-
- Step over
-
-
-
-
-
@@ -160,38 +85,71 @@
- -
+
-
+
+
+ <html><head/><body><p>The amount by which the tool is laterally displaced on each cycle of the pattern, specified in percent of the tool diameter.</p><p>A step over of 100% results in no overlap between two different cycles.</p></body></html>
+
+
+ 1
+
+
+ 100
+
+
+ 10
+
+
+ 100
+
+
+
+ -
+
+
+ Step over
+
+
+
+ -
+
+
+ Sample interval
+
+
+
+ -
+
+
+ Layer Mode
+
+
+
+ -
Optimize Linear Paths
- -
-
-
-
-
- Stock
-
-
- -
-
- BaseBoundBox
-
-
-
-
- -
-
+
-
+
- Optimize StepOver Transitions
+ Drop Cutter Direction
- -
-
-
- mm
+
-
+
+
+ BoundBox extra offset X, Y
+
+
+
+ -
+
+
+ Use Start Point
@@ -202,7 +160,21 @@
- -
+
-
+
+
+ BoundBox
+
+
+
+ -
+
+
+ mm
+
+
+
+ -
-
@@ -226,25 +198,84 @@
- -
-
+
-
+
+
+ mm
+
+
+
+ -
+
- Drop Cutter Direction
+ Depth offset
+
+
+
+ -
+
+
-
+
+ X
+
+
+ -
+
+ Y
+
+
+
+
+ -
+
+
-
+
+ Stock
+
+
+ -
+
+ BaseBoundBox
+
+
+
+
+ -
+
+
+ Optimize StepOver Transitions
-
-
+
- BoundBox extra offset X, Y
+ Cut Pattern
- -
-
-
- Use Start Point
-
+
-
+
+
-
+
+ Line
+
+
+ -
+
+ ZigZag
+
+
+ -
+
+ Circular
+
+
+ -
+
+ CircularZigZag
+
+
diff --git a/src/Mod/Path/PathScripts/PathSurfaceGui.py b/src/Mod/Path/PathScripts/PathSurfaceGui.py
index 5709c0341e..1eaca56e4f 100644
--- a/src/Mod/Path/PathScripts/PathSurfaceGui.py
+++ b/src/Mod/Path/PathScripts/PathSurfaceGui.py
@@ -67,6 +67,9 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
if obj.LayerMode != str(self.form.layerMode.currentText()):
obj.LayerMode = str(self.form.layerMode.currentText())
+ if obj.CutPattern != str(self.form.cutPattern.currentText()):
+ obj.CutPattern = str(self.form.cutPattern.currentText())
+
obj.DropCutterExtraOffset.x = FreeCAD.Units.Quantity(self.form.boundBoxExtraOffsetX.text()).Value
obj.DropCutterExtraOffset.y = FreeCAD.Units.Quantity(self.form.boundBoxExtraOffsetY.text()).Value
@@ -92,6 +95,7 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
self.selectInComboBox(obj.BoundBox, self.form.boundBoxSelect)
self.selectInComboBox(obj.ScanType, self.form.scanType)
self.selectInComboBox(obj.LayerMode, self.form.layerMode)
+ self.selectInComboBox(obj.CutPattern, self.form.cutPattern)
self.form.boundBoxExtraOffsetX.setText(str(obj.DropCutterExtraOffset.x))
self.form.boundBoxExtraOffsetY.setText(str(obj.DropCutterExtraOffset.y))
self.selectInComboBox(obj.DropCutterDir, self.form.dropCutterDirSelect)
@@ -122,6 +126,7 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
signals.append(self.form.boundBoxSelect.currentIndexChanged)
signals.append(self.form.scanType.currentIndexChanged)
signals.append(self.form.layerMode.currentIndexChanged)
+ signals.append(self.form.cutPattern.currentIndexChanged)
signals.append(self.form.boundBoxExtraOffsetX.editingFinished)
signals.append(self.form.boundBoxExtraOffsetY.editingFinished)
signals.append(self.form.dropCutterDirSelect.currentIndexChanged)
@@ -136,10 +141,12 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
def updateVisibility(self):
if self.form.scanType.currentText() == "Planar":
+ self.form.cutPattern.setEnabled(True)
self.form.boundBoxExtraOffsetX.setEnabled(False)
self.form.boundBoxExtraOffsetY.setEnabled(False)
self.form.dropCutterDirSelect.setEnabled(False)
else:
+ self.form.cutPattern.setEnabled(False)
self.form.boundBoxExtraOffsetX.setEnabled(True)
self.form.boundBoxExtraOffsetY.setEnabled(True)
self.form.dropCutterDirSelect.setEnabled(True)
From a907585f0d9f621ee0509cc45012dfe158116bd0 Mon Sep 17 00:00:00 2001
From: Russell Johnson <47639332+Russ4262@users.noreply.github.com>
Date: Sun, 29 Mar 2020 23:32:22 -0500
Subject: [PATCH 14/18] Path: Four bug fixes per forum discussion
Fix grammar mistake.
Fix Gcode comment formatting.
Fix application of `Optimize Step Over Transitions` by adjusting the offset tolerance for creating the offset cut area for the operation.
Fix math error in circular based cut patterns for small diameter cutters.
Identification of bugs begins in the forum at https://forum.freecadweb.org/viewtopic.php?style=3&f=15&t=41997&start=210.
Path: Lower SampleInterval minimum to 0.0001mm
---
src/Mod/Path/PathScripts/PathSurface.py | 42 +++++++++++++++----------
1 file changed, 25 insertions(+), 17 deletions(-)
diff --git a/src/Mod/Path/PathScripts/PathSurface.py b/src/Mod/Path/PathScripts/PathSurface.py
index 89932a104a..1d8c695925 100644
--- a/src/Mod/Path/PathScripts/PathSurface.py
+++ b/src/Mod/Path/PathScripts/PathSurface.py
@@ -100,9 +100,9 @@ class ObjectSurface(PathOp.ObjectOp):
QtCore.QT_TRANSLATE_NOOP("App::Property", "If true, the temporary path construction objects will be shown.")),
("App::PropertyDistance", "AngularDeflection", "Mesh Conversion",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Smaller values yield a finer, more accurate the mesh. Smaller values increase processing time a lot.")),
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Smaller values yield a finer, more accurate mesh. Smaller values increase processing time a lot.")),
("App::PropertyDistance", "LinearDeflection", "Mesh Conversion",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Smaller values yield a finer, more accurate the mesh. Smaller values do not increase processing time much.")),
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Smaller values yield a finer, more accurate mesh. Smaller values do not increase processing time much.")),
("App::PropertyFloat", "CutterTilt", "Rotational",
QtCore.QT_TRANSLATE_NOOP("App::Property", "Stop index(angle) for rotational scan")),
@@ -333,8 +333,8 @@ class ObjectSurface(PathOp.ObjectOp):
obj.CutterTilt = 90.0
# Limit sample interval
- if obj.SampleInterval.Value < 0.001:
- obj.SampleInterval.Value = 0.001
+ if obj.SampleInterval.Value < 0.0001:
+ obj.SampleInterval.Value = 0.0001
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
@@ -416,12 +416,12 @@ class ObjectSurface(PathOp.ObjectOp):
# ... 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(str(obj.Comment)), {}))
+ self.commandlist.append(Path.Command('N ({})'.format(obj.Label), {}))
+ self.commandlist.append(Path.Command('N (Tool type: {})'.format(str(obj.ToolController.Tool.ToolType)), {}))
+ self.commandlist.append(Path.Command('N (Compensated Tool Path. Diameter: {})'.format(str(obj.ToolController.Tool.Diameter)), {}))
+ self.commandlist.append(Path.Command('N (Sample interval: {})'.format(str(obj.SampleInterval.Value)), {}))
+ self.commandlist.append(Path.Command('N (Step over %: {})'.format(str(obj.StepOver)), {}))
self.commandlist.append(Path.Command('N ({})'.format(output), {}))
self.commandlist.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid}))
if obj.UseStartPoint is True:
@@ -1123,17 +1123,17 @@ class ObjectSurface(PathOp.ObjectOp):
if isVoid is False:
if isHole is True:
offset = -1 * obj.InternalFeaturesAdjustment.Value
- offset += self.radius # (self.radius + (tolrnc / 10.0))
+ offset += self.radius + (tolrnc / 10.0)
else:
offset = -1 * obj.BoundaryAdjustment.Value
if obj.BoundaryEnforcement is True:
- offset += self.radius # (self.radius + (tolrnc / 10.0))
+ offset += self.radius + (tolrnc / 10.0)
else:
- offset -= self.radius # (self.radius + (tolrnc / 10.0))
+ offset -= self.radius + (tolrnc / 10.0)
offset = 0.0 - offset
else:
offset = -1 * obj.BoundaryAdjustment.Value
- offset += self.radius # (self.radius + (tolrnc / 10.0))
+ offset += self.radius + (tolrnc / 10.0)
return offset
@@ -2356,7 +2356,11 @@ class ObjectSurface(PathOp.ObjectOp):
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)
+ spcRadRatio = space/rad
+ if spcRadRatio < 1.0:
+ tolrncAng = math.asin(spcRadRatio)
+ else:
+ tolrncAng = 0.999998 * math.pi
X = COM.x + (rad * math.cos(tolrncAng))
Y = v1.Y - space # rad * math.sin(tolrncAng)
@@ -2392,8 +2396,12 @@ class ObjectSurface(PathOp.ObjectOp):
# 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 iEi > iSi:
+ EI.pop(iEi)
+ EI.pop(iSi)
+ else:
+ EI.pop(iSi)
+ EI.pop(iEi)
if len(EI) > 0:
PRTS.append('BRK')
chkGap = True
From 5d0329498099d15775e526cbe3c0ae1f6d0fef1e Mon Sep 17 00:00:00 2001
From: Russell Johnson <47639332+Russ4262@users.noreply.github.com>
Date: Fri, 27 Mar 2020 18:49:34 -0500
Subject: [PATCH 15/18] Path: Fix "increase by factor of 10" issue
Forum discussion at https://forum.freecadweb.org/viewtopic.php?style=3&f=15&t=44486.
---
src/Mod/Path/PathScripts/PathSurfaceGui.py | 6 +++---
src/Mod/Path/PathScripts/PathWaterlineGui.py | 2 +-
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/Mod/Path/PathScripts/PathSurfaceGui.py b/src/Mod/Path/PathScripts/PathSurfaceGui.py
index 1eaca56e4f..41f11f6007 100644
--- a/src/Mod/Path/PathScripts/PathSurfaceGui.py
+++ b/src/Mod/Path/PathScripts/PathSurfaceGui.py
@@ -96,12 +96,12 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
self.selectInComboBox(obj.ScanType, self.form.scanType)
self.selectInComboBox(obj.LayerMode, self.form.layerMode)
self.selectInComboBox(obj.CutPattern, self.form.cutPattern)
- self.form.boundBoxExtraOffsetX.setText(str(obj.DropCutterExtraOffset.x))
- self.form.boundBoxExtraOffsetY.setText(str(obj.DropCutterExtraOffset.y))
+ self.form.boundBoxExtraOffsetX.setText(FreeCAD.Units.Quantity(obj.DropCutterExtraOffset.x, FreeCAD.Units.Length).UserString)
+ self.form.boundBoxExtraOffsetY.setText(FreeCAD.Units.Quantity(obj.DropCutterExtraOffset.y, FreeCAD.Units.Length).UserString)
self.selectInComboBox(obj.DropCutterDir, self.form.dropCutterDirSelect)
self.form.depthOffset.setText(FreeCAD.Units.Quantity(obj.DepthOffset.Value, FreeCAD.Units.Length).UserString)
self.form.stepOver.setValue(obj.StepOver)
- self.form.sampleInterval.setText(str(obj.SampleInterval))
+ self.form.sampleInterval.setText(FreeCAD.Units.Quantity(obj.SampleInterval.Value, FreeCAD.Units.Length).UserString)
if obj.UseStartPoint:
self.form.useStartPoint.setCheckState(QtCore.Qt.Checked)
diff --git a/src/Mod/Path/PathScripts/PathWaterlineGui.py b/src/Mod/Path/PathScripts/PathWaterlineGui.py
index cf72300d46..eed15fc3d3 100644
--- a/src/Mod/Path/PathScripts/PathWaterlineGui.py
+++ b/src/Mod/Path/PathScripts/PathWaterlineGui.py
@@ -83,7 +83,7 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
self.selectInComboBox(obj.CutPattern, self.form.cutPattern)
self.form.boundaryAdjustment.setText(FreeCAD.Units.Quantity(obj.BoundaryAdjustment.Value, FreeCAD.Units.Length).UserString)
self.form.stepOver.setValue(obj.StepOver)
- self.form.sampleInterval.setText(str(obj.SampleInterval))
+ self.form.sampleInterval.setText(FreeCAD.Units.Quantity(obj.SampleInterval.Value, FreeCAD.Units.Length).UserString)
if obj.OptimizeLinearPaths:
self.form.optimizeEnabled.setCheckState(QtCore.Qt.Checked)
From 7d4b2f2a501e8c6ff223835274a3c0e1dadcee92 Mon Sep 17 00:00:00 2001
From: Russell Johnson <47639332+Russ4262@users.noreply.github.com>
Date: Sat, 28 Mar 2020 21:51:19 -0500
Subject: [PATCH 16/18] Path: Fix bug that fails `Multi-pass` usage
---
src/Mod/Path/PathScripts/PathSurface.py | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/src/Mod/Path/PathScripts/PathSurface.py b/src/Mod/Path/PathScripts/PathSurface.py
index 1d8c695925..a485225402 100644
--- a/src/Mod/Path/PathScripts/PathSurface.py
+++ b/src/Mod/Path/PathScripts/PathSurface.py
@@ -2643,6 +2643,7 @@ class ObjectSurface(PathOp.ObjectOp):
# Process each layer in depthparams
prvLyrFirst = None
prvLyrLast = None
+ lastPrvStpLast = None
actvLyrs = 0
for lyr in range(0, lenDP):
odd = True # ZigZag directional switch
@@ -2651,8 +2652,12 @@ class ObjectSurface(PathOp.ObjectOp):
actvSteps = 0
LYR = list()
prvStpFirst = None
+ if lyr > 0:
+ if prvStpLast is not None:
+ lastPrvStpLast = prvStpLast
prvStpLast = None
lyrDep = depthparams[lyr]
+ PathLog.debug('Multi-pass lyrDep: {}'.format(round(lyrDep, 4)))
# Cycle through step-over sections (line segments or arcs)
for so in range(0, len(SCANDATA)):
@@ -2693,6 +2698,7 @@ class ObjectSurface(PathOp.ObjectOp):
# Manage step over transition and CircularZigZag direction
if so > 0:
+ # PathLog.debug(' stepover index: {}'.format(so))
# Control ZigZag direction
if obj.CutPattern == 'CircularZigZag':
if odd is True:
@@ -2700,6 +2706,8 @@ class ObjectSurface(PathOp.ObjectOp):
else:
odd = True
# Control step over transition
+ if prvStpLast is None:
+ prvStpLast = lastPrvStpLast
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))
@@ -2712,6 +2720,7 @@ class ObjectSurface(PathOp.ObjectOp):
for i in range(0, lenAdjPrts):
prt = ADJPRTS[i]
lenPrt = len(prt)
+ # PathLog.debug(' adj parts index - lenPrt: {} - {}'.format(i, lenPrt))
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
From 6644d08191c7a5be205bb945319d09dc649baea3 Mon Sep 17 00:00:00 2001
From: Russell Johnson <47639332+Russ4262@users.noreply.github.com>
Date: Mon, 30 Mar 2020 22:34:49 -0500
Subject: [PATCH 17/18] Path: Implement experimental Waterline algorithm
Insert experimental Waterline algorithm that is non-OCL based. It slices by step-down value, not stepover.
---
src/Mod/Path/PathScripts/PathWaterline.py | 1572 +++++++++++++++++++--
1 file changed, 1473 insertions(+), 99 deletions(-)
diff --git a/src/Mod/Path/PathScripts/PathWaterline.py b/src/Mod/Path/PathScripts/PathWaterline.py
index 06c960c8c3..b2d3fdc197 100644
--- a/src/Mod/Path/PathScripts/PathWaterline.py
+++ b/src/Mod/Path/PathScripts/PathWaterline.py
@@ -2,7 +2,8 @@
# ***************************************************************************
# * *
-# * Copyright (c) 2016 sliptonic *
+# * Copyright (c) 2019 Russell Johnson (russ4262) *
+# * Copyright (c) 2019 sliptonic *
# * *
# * This program is free software; you can redistribute it and/or modify *
# * it under the terms of the GNU Lesser General Public License (LGPL) *
@@ -23,7 +24,7 @@
# ***************************************************************************
# * *
# * Additional modifications and contributions beginning 2019 *
-# * by Russell Johnson 2020-03-23 16:15 CST *
+# * by Russell Johnson 2020-03-30 22:27 CST *
# * *
# ***************************************************************************
@@ -46,7 +47,7 @@ if FreeCAD.GuiUp:
import FreeCADGui
__title__ = "Path Waterline Operation"
-__author__ = "sliptonic (Brad Collette)"
+__author__ = "russ4262 (Russell Johnson), sliptonic (Brad Collette)"
__url__ = "http://www.freecadweb.org"
__doc__ = "Class and implementation of Mill Facing operation."
@@ -95,7 +96,6 @@ class ObjectWaterline(PathOp.ObjectOp):
def initOpProperties(self, obj):
'''initOpProperties(obj) ... create operation specific properties'''
-
PROPS = [
("App::PropertyBool", "ShowTempObjects", "Debug",
QtCore.QT_TRANSLATE_NOOP("App::Property", "If true, the temporary path construction objects will be shown.")),
@@ -105,58 +105,57 @@ class ObjectWaterline(PathOp.ObjectOp):
("App::PropertyDistance", "LinearDeflection", "Mesh Conversion",
QtCore.QT_TRANSLATE_NOOP("App::Property", "Smaller values yield a finer, more accurate the mesh. Smaller values do not increase processing time much.")),
- ("App::PropertyInteger", "AvoidLastX_Faces", "Selected Face(s) Settings",
+ ("App::PropertyInteger", "AvoidLastX_Faces", "Selected Geometry Settings",
QtCore.QT_TRANSLATE_NOOP("App::Property", "Avoid cutting the last 'N' faces in the Base Geometry list of selected faces.")),
- ("App::PropertyBool", "AvoidLastX_InternalFeatures", "Selected Face(s) Settings",
+ ("App::PropertyBool", "AvoidLastX_InternalFeatures", "Selected Geometry Settings",
QtCore.QT_TRANSLATE_NOOP("App::Property", "Do not cut internal features on avoided faces.")),
- ("App::PropertyDistance", "BoundaryAdjustment", "Selected Face(s) Settings",
+ ("App::PropertyDistance", "BoundaryAdjustment", "Selected Geometry Settings",
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.")),
- ("App::PropertyBool", "BoundaryEnforcement", "Selected Face(s) Settings",
+ ("App::PropertyBool", "BoundaryEnforcement", "Selected Geometry Settings",
QtCore.QT_TRANSLATE_NOOP("App::Property", "If true, the cutter will remain inside the boundaries of the model or selected face(s).")),
- ("App::PropertyEnumeration", "HandleMultipleFeatures", "Selected Face(s) Settings",
+ ("App::PropertyEnumeration", "HandleMultipleFeatures", "Selected Geometry Settings",
QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose how to process multiple Base Geometry features.")),
- ("App::PropertyDistance", "InternalFeaturesAdjustment", "Selected Face(s) Settings",
+ ("App::PropertyDistance", "InternalFeaturesAdjustment", "Selected Geometry Settings",
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.")),
- ("App::PropertyBool", "InternalFeaturesCut", "Selected Face(s) Settings",
+ ("App::PropertyBool", "InternalFeaturesCut", "Selected Geometry Settings",
QtCore.QT_TRANSLATE_NOOP("App::Property", "Ignore internal feature areas within a larger selected face.")),
- ("App::PropertyEnumeration", "Algorithm", "Waterline",
+ ("App::PropertyEnumeration", "Algorithm", "Clearing Options",
QtCore.QT_TRANSLATE_NOOP("App::Property", "Select the algorithm to use: OCL Dropcutter*, or Experimental.")),
- ("App::PropertyEnumeration", "BoundBox", "Waterline",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Should the operation be limited by the stock object or by the bounding box of the base object")),
- ("App::PropertyDistance", "SampleInterval", "Waterline",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "The Sample Interval. Small values cause long wait times")),
-
+ ("App::PropertyEnumeration", "BoundBox", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Select the overall boundary for the operation. ")),
("App::PropertyVectorDistance", "CircularCenterCustom", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "The start point of this path")),
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "The start point of this path.")),
("App::PropertyEnumeration", "CircularCenterAt", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose what point to start the ciruclar pattern: Center Of Mass, Center Of Boundbox, Xmin Ymin of boundbox, Custom.")),
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose center point to start the ciruclar pattern.")),
+ ("App::PropertyEnumeration", "ClearLastLayer", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Set to clear last layer in a `Multi-pass` operation.")),
("App::PropertyEnumeration", "CutMode", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "The direction that the toolpath should go around the part: Climb(ClockWise) or Conventional(CounterClockWise)")),
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the direction for the cutting tool to engage the material: Climb (ClockWise) or Conventional (CounterClockWise)")),
("App::PropertyEnumeration", "CutPattern", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Clearing pattern to use")),
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the geometric clearing pattern to use.")),
("App::PropertyFloat", "CutPatternAngle", "Clearing Options",
QtCore.QT_TRANSLATE_NOOP("App::Property", "Yaw angle for certain clearing patterns")),
("App::PropertyBool", "CutPatternReversed", "Clearing 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.")),
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Reverse the cut order of the stepover paths. For circular cut patterns, begin at the outside and work toward the center.")),
("App::PropertyDistance", "DepthOffset", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Z-axis offset from the surface of the object")),
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the Z-axis depth offset from the target surface.")),
("App::PropertyEnumeration", "LayerMode", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "The completion mode for the operation: single or multi-pass")),
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "The step down mode for the operation.")),
("App::PropertyEnumeration", "ProfileEdges", "Clearing Options",
QtCore.QT_TRANSLATE_NOOP("App::Property", "Profile the edges of the selection.")),
+ ("App::PropertyDistance", "SampleInterval", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the sampling resolution. Smaller values increase processing time.")),
("App::PropertyPercent", "StepOver", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Step over percentage of the drop cutter path")),
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the stepover percentage, based on the tool's diameter.")),
- ("App::PropertyBool", "OptimizeLinearPaths", "Waterline Optimization",
+ ("App::PropertyBool", "OptimizeLinearPaths", "Optimization",
QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable optimization of linear paths (co-linear points). Removes unnecessary co-linear points from G-Code output.")),
- ("App::PropertyBool", "OptimizeStepOverTransitions", "Waterline Optimization",
+ ("App::PropertyBool", "OptimizeStepOverTransitions", "Optimization",
QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable separate optimization of transitions between, and breaks within, each step over path.")),
- ("App::PropertyBool", "CircularUseG2G3", "Waterline Optimization",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Convert co-planar arcs to G2/G3 gcode commands for `Circular` and `CircularZigZag` cut patterns.")),
- ("App::PropertyDistance", "GapThreshold", "Waterline Optimization",
+ ("App::PropertyDistance", "GapThreshold", "Optimization",
QtCore.QT_TRANSLATE_NOOP("App::Property", "Collinear and co-radial artifact gaps that are smaller than this threshold are closed in the path.")),
- ("App::PropertyString", "GapSizes", "Waterline Optimization",
+ ("App::PropertyString", "GapSizes", "Optimization",
QtCore.QT_TRANSLATE_NOOP("App::Property", "Feedback: three smallest gaps identified in the path geometry.")),
("App::PropertyVectorDistance", "StartPoint", "Start Point",
@@ -187,8 +186,9 @@ class ObjectWaterline(PathOp.ObjectOp):
'Algorithm': ['OCL Dropcutter', 'Experimental'],
'BoundBox': ['BaseBoundBox', 'Stock'],
'CircularCenterAt': ['CenterOfMass', 'CenterOfBoundBox', 'XminYmin', 'Custom'],
+ 'ClearLastLayer': ['Off', 'Line', 'Circular', 'CircularZigZag', 'Offset', 'ZigZag'],
'CutMode': ['Conventional', 'Climb'],
- 'CutPattern': ['Line', 'ZigZag', 'Circular', 'CircularZigZag'], # Additional goals ['Offset', 'Spiral', 'ZigZagOffset', 'Grid', 'Triangle']
+ 'CutPattern': ['None', 'Line', 'Circular', 'CircularZigZag', 'Offset', 'ZigZag'], # Additional goals ['Offset', 'Spiral', 'ZigZagOffset', 'Grid', 'Triangle']
'HandleMultipleFeatures': ['Collectively', 'Individually'],
'LayerMode': ['Single-pass', 'Multi-pass'],
'ProfileEdges': ['None', 'Only', 'First', 'Last'],
@@ -198,22 +198,36 @@ class ObjectWaterline(PathOp.ObjectOp):
# Used to hide inputs in properties list
show = 0
hide = 2
+ cpShow = 0
+ expMode = 0
obj.setEditorMode('BoundaryEnforcement', hide)
obj.setEditorMode('ProfileEdges', hide)
obj.setEditorMode('InternalFeaturesAdjustment', hide)
obj.setEditorMode('InternalFeaturesCut', hide)
- # if obj.CutPattern in ['Line', 'ZigZag']:
- if obj.CutPattern in ['Circular', 'CircularZigZag']:
+ if obj.CutPattern == 'None':
+ show = 2
+ hide = 2
+ cpShow = 2
+ # elif obj.CutPattern in ['Line', 'ZigZag']:
+ # show = 0
+ # hide = 2
+ elif obj.CutPattern in ['Circular', 'CircularZigZag']:
show = 2 # hide
hide = 0 # show
+ # obj.setEditorMode('StepOver', cpShow)
obj.setEditorMode('CutPatternAngle', show)
obj.setEditorMode('CircularCenterAt', hide)
obj.setEditorMode('CircularCenterCustom', hide)
+ if obj.Algorithm == 'Experimental':
+ expMode = 2
+ obj.setEditorMode('SampleInterval', expMode)
+ obj.setEditorMode('LinearDeflection', expMode)
+ obj.setEditorMode('AngularDeflection', expMode)
def onChanged(self, obj, prop):
if hasattr(self, 'addedAllProperties'):
if self.addedAllProperties is True:
- if prop == 'CutPattern':
+ if prop in ['Algorithm', 'CutPattern']:
self.setEditorProperties(obj)
def opOnDocumentRestored(self, obj):
@@ -233,7 +247,6 @@ class ObjectWaterline(PathOp.ObjectOp):
obj.OptimizeLinearPaths = True
obj.InternalFeaturesCut = True
obj.OptimizeStepOverTransitions = False
- obj.CircularUseG2G3 = False
obj.BoundaryEnforcement = True
obj.UseStartPoint = False
obj.AvoidLastX_InternalFeatures = True
@@ -245,10 +258,11 @@ class ObjectWaterline(PathOp.ObjectOp):
obj.ProfileEdges = 'None'
obj.LayerMode = 'Single-pass'
obj.CutMode = 'Conventional'
- obj.CutPattern = 'Line'
+ obj.CutPattern = 'None'
obj.HandleMultipleFeatures = 'Collectively' # 'Individually'
obj.CircularCenterAt = 'CenterOfMass' # 'CenterOfBoundBox', 'XminYmin', 'Custom'
obj.GapSizes = 'No gaps identified.'
+ obj.ClearLastLayer = 'Off'
obj.StepOver = 100
obj.CutPatternAngle = 0.0
obj.SampleInterval.Value = 1.0
@@ -259,6 +273,8 @@ class ObjectWaterline(PathOp.ObjectOp):
obj.CircularCenterCustom.y = 0.0
obj.CircularCenterCustom.z = 0.0
obj.GapThreshold.Value = 0.005
+ obj.LinearDeflection.Value = 0.0001
+ obj.AngularDeflection.Value = 0.25
# For debugging
obj.ShowTempObjects = False
@@ -327,7 +343,7 @@ class ObjectWaterline(PathOp.ObjectOp):
self.collectiveShapes = list()
self.individualShapes = list()
self.avoidShapes = list()
- self.deflection = None
+ self.geoTlrnc = None
self.tempGroup = None
self.CutClimb = False
self.closedGap = False
@@ -369,12 +385,12 @@ class ObjectWaterline(PathOp.ObjectOp):
# ... 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(str(obj.Comment)), {}))
+ self.commandlist.append(Path.Command('N ({})'.format(obj.Label), {}))
+ self.commandlist.append(Path.Command('N (Tool type: {})'.format(str(obj.ToolController.Tool.ToolType)), {}))
+ self.commandlist.append(Path.Command('N (Compensated Tool Path. Diameter: {})'.format(str(obj.ToolController.Tool.Diameter)), {}))
+ self.commandlist.append(Path.Command('N (Sample interval: {})'.format(str(obj.SampleInterval.Value)), {}))
+ self.commandlist.append(Path.Command('N (Step over %: {})'.format(str(obj.StepOver)), {}))
self.commandlist.append(Path.Command('N ({})'.format(output), {}))
self.commandlist.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid}))
if obj.UseStartPoint is True:
@@ -419,6 +435,19 @@ class ObjectWaterline(PathOp.ObjectOp):
self.SafeHeightOffset = JOB.SetupSheet.SafeHeightOffset.Value
self.ClearHeightOffset = JOB.SetupSheet.ClearanceHeightOffset.Value
+ # Set deflection values for mesh generation
+ useDGT = False
+ try: # try/except is for Path Jobs created before GeometryTolerance
+ self.geoTlrnc = JOB.GeometryTolerance.Value
+ if self.geoTlrnc == 0.0:
+ useDGT = True
+ except AttributeError as ee:
+ PathLog.warning('{}\nPlease set Job.GeometryTolerance to an acceptable value. Using PathPreferences.defaultGeometryTolerance().'.format(ee))
+ useDGT = True
+ if useDGT:
+ import PathScripts.PathPreferences as PathPreferences
+ self.geoTlrnc = PathPreferences.defaultGeometryTolerance()
+
# 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
@@ -426,15 +455,6 @@ class ObjectWaterline(PathOp.ObjectOp):
# 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)):
@@ -470,7 +490,10 @@ class ObjectWaterline(PathOp.ObjectOp):
(FACES, VOIDS) = pPM
# Create OCL.stl model objects
- self._prepareModelSTLs(JOB, obj)
+ if obj.Algorithm == 'OCL Dropcutter':
+ self._prepareModelSTLs(JOB, obj)
+ PathLog.debug('obj.LinearDeflection.Value: {}'.format(obj.LinearDeflection.Value))
+ PathLog.debug('obj.AngularDeflection.Value: {}'.format(obj.AngularDeflection.Value))
for m in range(0, len(JOB.Model.Group)):
Mdl = JOB.Model.Group[m]
@@ -483,8 +506,9 @@ class ObjectWaterline(PathOp.ObjectOp):
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)
+ if obj.Algorithm == 'OCL Dropcutter':
+ 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]))
@@ -542,8 +566,6 @@ class ObjectWaterline(PathOp.ObjectOp):
self.depthParams = None
self.midDep = None
self.wpc = None
- self.angularDeflection = None
- self.deflection = None
del self.modelSTLs
del self.safeSTLs
del self.modelTypes
@@ -555,8 +577,6 @@ class ObjectWaterline(PathOp.ObjectOp):
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))
@@ -964,7 +984,7 @@ class ObjectWaterline(PathOp.ObjectOp):
except Exception as eee:
PathLog.error(str(eee))
cont = False
- time.sleep(0.2)
+ # time.sleep(0.2)
if cont is True:
csFaceShape = self._getShapeSlice(baseEnv)
@@ -1093,7 +1113,7 @@ class ObjectWaterline(PathOp.ObjectOp):
return offset
- def _extractFaceOffset(self, obj, fcShape, offset):
+ def _extractFaceOffset(self, obj, fcShape, offset, makeComp=True):
'''_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.'''
@@ -1104,13 +1124,14 @@ class ObjectWaterline(PathOp.ObjectOp):
areaParams = {}
areaParams['Offset'] = offset
- areaParams['Fill'] = 1
+ areaParams['Fill'] = 1 # 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
+ # areaParams['Tolerance'] = 0.001
area = Path.Area() # Create instance of Area() class object
# area.setPlane(PathUtils.makeWorkplane(fcShape)) # Set working plane
@@ -1118,17 +1139,26 @@ class ObjectWaterline(PathOp.ObjectOp):
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])
+ if not makeComp:
+ ofstFace = [ofstFace]
else:
W = list()
for wr in offsetShape.Wires:
W.append(Part.Face(wr))
- ofstFace = Part.makeCompound(W)
+ if makeComp:
+ ofstFace = Part.makeCompound(W)
+ else:
+ ofstFace = W
return ofstFace # offsetShape
@@ -1373,8 +1403,10 @@ class ObjectWaterline(PathOp.ObjectOp):
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)
+ mesh = MeshPart.meshFromShape(Shape=M.Shape,
+ LinearDeflection=obj.LinearDeflection.Value,
+ AngularDeflection=obj.AngularDeflection.Value,
+ Relative=False)
if self.modelSTLs[m] is True:
stl = ocl.STLSurf()
@@ -1438,7 +1470,7 @@ class ObjectWaterline(PathOp.ObjectOp):
fuseShapes.append(adjStckWst)
else:
PathLog.warning('Path transitions might not avoid the model. Verify paths.')
- time.sleep(0.3)
+ # time.sleep(0.3)
else:
# If boundbox is Job.Stock, add hidden pad under stock as base plate
@@ -1471,8 +1503,11 @@ class ObjectWaterline(PathOp.ObjectOp):
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)
+ meshFuse = MeshPart.meshFromShape(Shape=fused,
+ LinearDeflection=obj.LinearDeflection.Value,
+ AngularDeflection=obj.AngularDeflection.Value,
+ Relative=False)
+ # time.sleep(0.2)
stl = ocl.STLSurf()
for f in meshFuse.Facets:
p = f.Points[0]
@@ -1538,6 +1573,763 @@ class ObjectWaterline(PathOp.ObjectOp):
return final
+ # Methods for creating path geometry
+ 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:
+ avgArea = totArea / fCnt
+ zeroCOM.multiply(1 / fCnt)
+ zeroCOM.multiply(1 / avgArea)
+ COM = FreeCAD.Vector(zeroCOM.x, zeroCOM.y, 0.0)
+
+ # 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
+
+ # 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
+
+ # 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
+
+ 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)
+
+ # 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) )
+
+ # 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 _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
+ # PathLog.debug("SO[0] == 'Loop'")
+ lei = SO[1] # loop Edges index
+ v1 = compGeoShp.Edges[lei].Vertexes[0]
+
+ # space = obj.SampleInterval.Value / 2.0
+ space = 0.0000001
+
+ # p1 = FreeCAD.Vector(v1.X, v1.Y, v1.Z)
+ p1 = FreeCAD.Vector(v1.X, v1.Y, 0.0)
+ rad = p1.sub(COM).Length
+ spcRadRatio = space/rad
+ if spcRadRatio < 1.0:
+ tolrncAng = math.asin(spcRadRatio)
+ else:
+ tolrncAng = 0.9999998 * math.pi
+ EX = COM.x + (rad * math.cos(tolrncAng))
+ EY = v1.Y - space # rad * math.sin(tolrncAng)
+
+ sp = (v1.X, v1.Y, 0.0)
+ ep = (EX, EY, 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
+ # PathLog.debug("SO[0] == '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)
+ if iEi > iSi:
+ EI.pop(iEi)
+ EI.pop(iSi)
+ else:
+ EI.pop(iSi)
+ EI.pop(iEi)
+ 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 _getExperimentalWaterlinePaths(self, obj, PNTSET, csHght):
+ '''_getExperimentalWaterlinePaths(obj, PNTSET, csHght)...
+ Switching fuction for calling the appropriate path-geometry to OCL points conversion fucntion
+ for the various cut patterns.'''
+ PathLog.debug('_getExperimentalWaterlinePaths()')
+ SCANS = list()
+
+ if obj.CutPattern == 'Line':
+ stpOvr = list()
+ for D in PNTSET:
+ for SEG in D:
+ if SEG == 'BRK':
+ stpOvr.append(SEG)
+ else:
+ # D format is ((p1, p2), (p3, p4))
+ (A, B) = SEG
+ P1 = FreeCAD.Vector(A[0], A[1], csHght)
+ P2 = FreeCAD.Vector(B[0], B[1], csHght)
+ stpOvr.append((P1, P2))
+ SCANS.append(stpOvr)
+ stpOvr = list()
+ elif obj.CutPattern == 'ZigZag':
+ stpOvr = list()
+ 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
+ P1 = FreeCAD.Vector(A[0], A[1], csHght)
+ P2 = FreeCAD.Vector(B[0], B[1], csHght)
+ stpOvr.append((P1, P2))
+ SCANS.append(stpOvr)
+ stpOvr = list()
+ elif obj.CutPattern in ['Circular', 'CircularZigZag']:
+ # PNTSET is list, by stepover.
+ # Each stepover is a list containing arc/loop descriptions, (sp, ep, cp)
+ for so in range(0, len(PNTSET)):
+ stpOvr = list()
+ erFlg = False
+ (aTyp, dirFlg, ARCS) = PNTSET[so]
+
+ if dirFlg == 1: # 1
+ cMode = True # Climb mode
+ else:
+ cMode = False
+
+ for a in range(0, len(ARCS)):
+ Arc = ARCS[a]
+ if Arc == 'BRK':
+ stpOvr.append('BRK')
+ else:
+ (sp, ep, cp) = Arc
+ S = FreeCAD.Vector(sp[0], sp[1], csHght)
+ E = FreeCAD.Vector(ep[0], ep[1], csHght)
+ C = FreeCAD.Vector(cp[0], cp[1], csHght)
+ scan = (S, E, C, cMode)
+ if scan is False:
+ erFlg = True
+ else:
+ ##if aTyp == 'L':
+ ## stpOvr.append(FreeCAD.Vector(scan[0][0].x, scan[0][0].y, scan[0][0].z))
+ stpOvr.append(scan)
+ if erFlg is False:
+ SCANS.append(stpOvr)
+
+ return SCANS
+
+ # Main planar scan functions
+ 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 _planarGetPDC(self, stl, finalDep, SampleInterval, useSafeCutter=False):
pdc = ocl.PathDropCutter() # create a pdc [PathDropCutter] object
pdc.setSTL(stl) # add stl model
@@ -1935,9 +2727,614 @@ class ObjectWaterline(PathOp.ObjectOp):
return output
+ # Main waterline functions
def _experimentalWaterlineOp(self, JOB, obj, mdlIdx, subShp=None):
- PathLog.error('The `Experimental` algorithm is not available at this time.')
- return []
+ '''_waterlineOp(JOB, obj, mdlIdx, subShp=None) ...
+ Main waterline function to perform waterline extraction from model.'''
+ PathLog.debug('_experimentalWaterlineOp()')
+
+ msg = translate('PathWaterline', 'Experimental Waterline does not currently support selected faces.')
+ PathLog.info('\n..... ' + msg)
+
+ commands = []
+ t_begin = time.time()
+ base = JOB.Model.Group[mdlIdx]
+ bb = self.boundBoxes[mdlIdx]
+ stl = self.modelSTLs[mdlIdx]
+ safeSTL = self.safeSTLs[mdlIdx]
+ self.endVector = None
+
+ finDep = obj.FinalDepth.Value + (self.geoTlrnc / 10.0)
+ depthParams = PathUtils.depth_params(obj.ClearanceHeight.Value, obj.SafeHeight.Value, obj.StartDepth.Value, obj.StepDown.Value, 0.0, finDep)
+
+ # Compute number and size of stepdowns, and final depth
+ if obj.LayerMode == 'Single-pass':
+ depthparams = [finDep]
+ else:
+ depthparams = [dp for dp in depthParams]
+ lenDP = len(depthparams)
+ PathLog.debug('Experimental Waterline depthparams:\n{}'.format(depthparams))
+
+ # Prepare PathDropCutter objects with STL data
+ # safePDC = self._planarGetPDC(safeSTL, depthparams[lenDP - 1], obj.SampleInterval.Value, useSafeCutter=False)
+
+ buffer = self.cutter.getDiameter() * 2.0
+ borderFace = Part.Face(self._makeExtendedBoundBox(JOB.Stock.Shape.BoundBox, buffer, 0.0))
+
+ # Get correct boundbox
+ if obj.BoundBox == 'Stock':
+ stockEnv = self._getShapeEnvelope(JOB.Stock.Shape)
+ bbFace = self._getCrossSection(stockEnv) # returned at Z=0.0
+ elif obj.BoundBox == 'BaseBoundBox':
+ baseEnv = self._getShapeEnvelope(base.Shape)
+ bbFace = self._getCrossSection(baseEnv) # returned at Z=0.0
+
+ trimFace = borderFace.cut(bbFace)
+ if self.showDebugObjects is True:
+ TF = FreeCAD.ActiveDocument.addObject('Part::Feature', 'trimFace')
+ TF.Shape = trimFace
+ TF.purgeTouched()
+ self.tempGroup.addObject(TF)
+
+ # Cycle through layer depths
+ CUTAREAS = self._getCutAreas(base.Shape, depthparams, bbFace, trimFace, borderFace)
+ if not CUTAREAS:
+ PathLog.error('No cross-section cut areas identified.')
+ return commands
+
+ caCnt = 0
+ ofst = obj.BoundaryAdjustment.Value
+ ofst -= self.radius # (self.radius + (tolrnc / 10.0))
+ caLen = len(CUTAREAS)
+ lastCA = caLen - 1
+ lastClearArea = None
+ lastCsHght = None
+ clearLastLayer = True
+ for ca in range(0, caLen):
+ area = CUTAREAS[ca]
+ csHght = area.BoundBox.ZMin
+ cont = False
+ caCnt += 1
+ if area.Area > 0.0:
+ cont = True
+ caWireCnt = len(area.Wires) - 1 # first wire is boundFace wire
+ PathLog.debug('cutAreaWireCnt: {}'.format(caWireCnt))
+ if self.showDebugObjects is True:
+ CA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'cutArea_{}'.format(caCnt))
+ CA.Shape = area
+ CA.purgeTouched()
+ self.tempGroup.addObject(CA)
+ else:
+ PathLog.error('Cut area at {} is zero.'.format(round(csHght, 4)))
+
+ # get offset wire(s) based upon cross-section cut area
+ if cont:
+ area.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - area.BoundBox.ZMin))
+ activeArea = area.cut(trimFace)
+ activeAreaWireCnt = len(activeArea.Wires) # first wire is boundFace wire
+ PathLog.debug('activeAreaWireCnt: {}'.format(activeAreaWireCnt))
+ if self.showDebugObjects is True:
+ CA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'activeArea_{}'.format(caCnt))
+ CA.Shape = activeArea
+ CA.purgeTouched()
+ self.tempGroup.addObject(CA)
+ ofstArea = self._extractFaceOffset(obj, activeArea, ofst, makeComp=False)
+ if not ofstArea:
+ PathLog.error('No offset area returned for cut area depth: {}'.format(csHght))
+ cont = False
+
+ if cont:
+ # Identify solid areas in the offset data
+ ofstSolidFacesList = self._getSolidAreasFromPlanarFaces(ofstArea)
+ if ofstSolidFacesList:
+ clearArea = Part.makeCompound(ofstSolidFacesList)
+ if self.showDebugObjects is True:
+ CA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'clearArea_{}'.format(caCnt))
+ CA.Shape = clearArea
+ CA.purgeTouched()
+ self.tempGroup.addObject(CA)
+ else:
+ cont = False
+ PathLog.error('ofstSolids is False.')
+
+ if cont:
+ # Make waterline path for current CUTAREA depth (csHght)
+ commands.extend(self._wiresToWaterlinePath(obj, clearArea, csHght))
+ clearArea.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - clearArea.BoundBox.ZMin))
+ lastClearArea = clearArea
+ lastCsHght = csHght
+
+ # Clear layer as needed
+ (useOfst, usePat, clearLastLayer) = self._clearLayer(obj, ca, lastCA, clearLastLayer)
+ ##if self.showDebugObjects is True and (usePat or useOfst):
+ ## OA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'clearPatternArea_{}'.format(round(csHght, 2)))
+ ## OA.Shape = clearArea
+ ## OA.purgeTouched()
+ ## self.tempGroup.addObject(OA)
+ if usePat:
+ commands.extend(self._makeCutPatternLayerPaths(JOB, obj, clearArea, csHght))
+ if useOfst:
+ commands.extend(self._makeOffsetLayerPaths(JOB, obj, clearArea, csHght))
+ # Efor
+
+ if clearLastLayer:
+ (useOfst, usePat, cLL) = self._clearLayer(obj, 1, 1, False)
+ clearArea.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - lastClearArea.BoundBox.ZMin))
+ if usePat:
+ commands.extend(self._makeCutPatternLayerPaths(JOB, obj, lastClearArea, lastCsHght))
+
+ if useOfst:
+ commands.extend(self._makeOffsetLayerPaths(JOB, obj, lastClearArea, lastCsHght))
+
+ PathLog.info("Waterline: All layer scans combined took " + str(time.time() - t_begin) + " s")
+ return commands
+
+ def _getCutAreas(self, shape, depthparams, bbFace, trimFace, borderFace):
+ '''_getCutAreas(JOB, shape, depthparams, bbFace, borderFace) ...
+ Takes shape, depthparams and base-envelope-cross-section, and
+ returns a list of cut areas - one for each depth.'''
+ PathLog.debug('_getCutAreas()')
+
+ CUTAREAS = list()
+ lastLayComp = None
+ isFirst = True
+ lenDP = len(depthparams)
+
+ # Cycle through layer depths
+ for dp in range(0, lenDP):
+ csHght = depthparams[dp]
+ PathLog.debug('Depth {} is {}'.format(dp + 1, csHght))
+
+ # Get slice at depth of shape
+ csFaces = self._getModelCrossSection(shape, csHght) # returned at Z=0.0
+ if not csFaces:
+ PathLog.error('No cross-section wires at {}'.format(csHght))
+ else:
+ PathLog.debug('cross-section face count {}'.format(len(csFaces)))
+ if len(csFaces) > 0:
+ useFaces = self._getSolidAreasFromPlanarFaces(csFaces)
+ else:
+ useFaces = False
+
+ if useFaces:
+ PathLog.debug('useFacesCnt: {}'.format(len(useFaces)))
+ compAdjFaces = Part.makeCompound(useFaces)
+
+ if self.showDebugObjects is True:
+ CA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpSolids_{}'.format(dp + 1))
+ CA.Shape = compAdjFaces
+ CA.purgeTouched()
+ self.tempGroup.addObject(CA)
+
+ if isFirst:
+ allPrevComp = compAdjFaces
+ cutArea = borderFace.cut(compAdjFaces)
+ else:
+ preCutArea = borderFace.cut(compAdjFaces)
+ cutArea = preCutArea.cut(allPrevComp) # cut out higher layers to avoid cutting recessed areas
+ allPrevComp = allPrevComp.fuse(compAdjFaces)
+ cutArea.translate(FreeCAD.Vector(0.0, 0.0, csHght - cutArea.BoundBox.ZMin))
+ CUTAREAS.append(cutArea)
+ isFirst = False
+ else:
+ PathLog.error('No waterline at depth: {} mm.'.format(csHght))
+ # Efor
+
+ if len(CUTAREAS) > 0:
+ return CUTAREAS
+
+ return False
+
+ def _wiresToWaterlinePath(self, obj, ofstPlnrShp, csHght):
+ PathLog.debug('_wiresToWaterlinePath()')
+ commands = list()
+
+ # Translate path geometry to layer height
+ ofstPlnrShp.translate(FreeCAD.Vector(0.0, 0.0, csHght - ofstPlnrShp.BoundBox.ZMin))
+ if self.showDebugObjects is True:
+ OA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'waterlinePathArea_{}'.format(round(csHght, 2)))
+ OA.Shape = ofstPlnrShp
+ OA.purgeTouched()
+ self.tempGroup.addObject(OA)
+
+ commands.append(Path.Command('N (Cut Area {}.)'.format(round(csHght, 2))))
+ for w in range(0, len(ofstPlnrShp.Wires)):
+ wire = ofstPlnrShp.Wires[w]
+ V = wire.Vertexes
+ if obj.CutMode == 'Climb':
+ lv = len(V) - 1
+ startVect = FreeCAD.Vector(V[lv].X, V[lv].Y, V[lv].Z)
+ else:
+ startVect = FreeCAD.Vector(V[0].X, V[0].Y, V[0].Z)
+
+ commands.append(Path.Command('N (Wire {}.)'.format(w)))
+ (cmds, endVect) = self._wireToPath(obj, wire, startVect)
+ commands.extend(cmds)
+ commands.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid}))
+
+ return commands
+
+ def _makeCutPatternLayerPaths(self, JOB, obj, clrAreaShp, csHght):
+ PathLog.debug('_makeCutPatternLayerPaths()')
+ commands = []
+
+ clrAreaShp.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - clrAreaShp.BoundBox.ZMin))
+ pathGeom = self._planarMakePathGeom(obj, clrAreaShp)
+ pathGeom.translate(FreeCAD.Vector(0.0, 0.0, csHght - pathGeom.BoundBox.ZMin))
+ # clrAreaShp.translate(FreeCAD.Vector(0.0, 0.0, csHght - clrAreaShp.BoundBox.ZMin))
+
+ if self.showDebugObjects is True:
+ OA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'pathGeom_{}'.format(round(csHght, 2)))
+ OA.Shape = pathGeom
+ OA.purgeTouched()
+ self.tempGroup.addObject(OA)
+
+ # Convert pathGeom to gcode more efficiently
+ if True:
+ if obj.CutPattern == 'Offset':
+ commands.extend(self._makeOffsetLayerPaths(JOB, obj, clrAreaShp, csHght))
+ else:
+ clrAreaShp.translate(FreeCAD.Vector(0.0, 0.0, csHght - clrAreaShp.BoundBox.ZMin))
+ if obj.CutPattern == 'Line':
+ pntSet = self._pathGeomToLinesPointSet(obj, pathGeom)
+ elif obj.CutPattern == 'ZigZag':
+ pntSet = self._pathGeomToZigzagPointSet(obj, pathGeom)
+ elif obj.CutPattern in ['Circular', 'CircularZigZag']:
+ pntSet = self._pathGeomToArcPointSet(obj, pathGeom)
+ stpOVRS = self._getExperimentalWaterlinePaths(obj, pntSet, csHght)
+ # PathLog.debug('stpOVRS:\n{}'.format(stpOVRS))
+ safePDC = False
+ cmds = self._clearGeomToPaths(JOB, obj, safePDC, stpOVRS, csHght)
+ commands.extend(cmds)
+ else:
+ # Use Path.fromShape() to convert edges to paths
+ for w in range(0, len(pathGeom.Edges)):
+ wire = pathGeom.Edges[w]
+ V = wire.Vertexes
+ if obj.CutMode == 'Climb':
+ lv = len(V) - 1
+ startVect = FreeCAD.Vector(V[lv].X, V[lv].Y, V[lv].Z)
+ else:
+ startVect = FreeCAD.Vector(V[0].X, V[0].Y, V[0].Z)
+
+ commands.append(Path.Command('N (Wire {}.)'.format(w)))
+ (cmds, endVect) = self._wireToPath(obj, wire, startVect)
+ commands.extend(cmds)
+ commands.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid}))
+
+ return commands
+
+ def _makeOffsetLayerPaths(self, JOB, obj, clrAreaShp, csHght):
+ PathLog.debug('_makeOffsetLayerPaths()')
+ PathLog.warning('Using `Offset` for clearing bottom layer.')
+ cmds = list()
+ # ofst = obj.BoundaryAdjustment.Value
+ ofst = 0.0 - self.cutOut # - self.cutter.getDiameter() # (self.radius + (tolrnc / 10.0))
+ shape = clrAreaShp
+ cont = True
+ cnt = 0
+ while cont:
+ ofstArea = self._extractFaceOffset(obj, shape, ofst, makeComp=True)
+ if not ofstArea:
+ PathLog.warning('No offset clearing area returned.')
+ break
+ for F in ofstArea.Faces:
+ cmds.extend(self._wiresToWaterlinePath(obj, F, csHght))
+ shape = ofstArea
+ if cnt == 0:
+ ofst = 0.0 - self.cutOut # self.cutter.Diameter()
+ cnt += 1
+ return cmds
+
+ def _clearGeomToPaths(self, JOB, obj, safePDC, SCANDATA, csHght):
+ PathLog.debug('_clearGeomToPaths()')
+
+ GCODE = [Path.Command('N (Beginning of Single-pass layer.)', {})]
+ tolrnc = JOB.GeometryTolerance.Value
+ prevDepth = obj.SafeHeight.Value
+ lenSCANDATA = len(SCANDATA)
+ gDIR = ['G3', 'G2']
+
+ if self.CutClimb is True:
+ gDIR = ['G2', 'G3']
+
+ # 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
+ minTrnsHght = obj.SafeHeight.Value
+ # cmds.append(Path.Command('N (Transition: last, first: {}, {}: minSTH: {})'.format(lstStpEnd, first, minTrnsHght), {}))
+ cmds.extend(self._stepTransitionCmds(obj, lstStpEnd, first, minTrnsHght, tolrnc))
+
+ # Cycle through current step-over parts
+ for i in range(0, lenPRTS):
+ prt = PRTS[i]
+ lenPrt = len(prt)
+ # PathLog.debug('prt: {}'.format(prt))
+ if prt == 'BRK':
+ nxtStart = PRTS[i + 1][0]
+ # minSTH = self._getMinSafeTravelHeight(safePDC, last, nxtStart) # Check safe travel height against fullSTL
+ minSTH = obj.SafeHeight.Value
+ 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), {}))
+ if obj.CutPattern in ['Line', 'ZigZag']:
+ start, last = prt
+ cmds.append(Path.Command('G1', {'X': start.x, 'Y': start.y, 'Z': start.z, 'F': self.horizFeed}))
+ cmds.append(Path.Command('G1', {'X': last.x, 'Y': last.y, 'F': self.horizFeed}))
+ elif obj.CutPattern in ['Circular', 'CircularZigZag']:
+ start, last, centPnt, cMode = prt
+ gcode = self._makeGcodeArc(start, last, odd, gDIR, tolrnc)
+ cmds.extend(gcode)
+ cmds.append(Path.Command('N (End of step {}.)'.format(so), {}))
+ GCODE.extend(cmds) # save line commands
+ lstStpEnd = last
+ # Efor
+
+ return GCODE
+
+ def _getSolidAreasFromPlanarFaces(self, csFaces):
+ PathLog.debug('_getSolidAreasFromPlanarFaces()')
+ holds = list()
+ cutFaces = list()
+ useFaces = list()
+ lenCsF = len(csFaces)
+ PathLog.debug('lenCsF: {}'.format(lenCsF))
+
+ if lenCsF == 1:
+ useFaces = csFaces
+ else:
+ fIds = list()
+ aIds = list()
+ pIds = list()
+ cIds = list()
+
+ for af in range(0, lenCsF):
+ fIds.append(af) # face ids
+ aIds.append(af) # face ids
+ pIds.append(-1) # parent ids
+ cIds.append(False) # cut ids
+ holds.append(False)
+
+ while len(fIds) > 0:
+ li = fIds.pop()
+ low = csFaces[li] # senior face
+ pIds = self._idInternalFeature(csFaces, fIds, pIds, li, low)
+ # Ewhile
+ ##PathLog.info('fIds: {}'.format(fIds))
+ ##PathLog.info('pIds: {}'.format(pIds))
+
+ for af in range(lenCsF - 1, -1, -1): # cycle from last item toward first
+ ##PathLog.info('af: {}'.format(af))
+ prnt = pIds[af]
+ ##PathLog.info('prnt: {}'.format(prnt))
+ if prnt == -1:
+ stack = -1
+ else:
+ stack = [af]
+ # get_face_ids_to_parent
+ stack.insert(0, prnt)
+ nxtPrnt = pIds[prnt]
+ # find af value for nxtPrnt
+ while nxtPrnt != -1:
+ stack.insert(0, nxtPrnt)
+ nxtPrnt = pIds[nxtPrnt]
+ cIds[af] = stack
+ # PathLog.debug('cIds: {}\n'.format(cIds))
+
+ for af in range(0, lenCsF):
+ # PathLog.debug('af is {}'.format(af))
+ pFc = cIds[af]
+ if pFc == -1:
+ # Simple, independent region
+ holds[af] = csFaces[af] # place face in hold
+ # PathLog.debug('pFc == -1')
+ else:
+ # Compound region
+ # PathLog.debug('pFc is not -1')
+ cnt = len(pFc)
+ if cnt % 2.0 == 0.0:
+ # even is donut cut
+ # PathLog.debug('cnt is even')
+ inr = pFc[cnt - 1]
+ otr = pFc[cnt - 2]
+ # PathLog.debug('inr / otr: {} / {}'.format(inr, otr))
+ holds[otr] = holds[otr].cut(csFaces[inr])
+ else:
+ # odd is floating solid
+ # PathLog.debug('cnt is ODD')
+ holds[af] = csFaces[af]
+ # Efor
+
+ for af in range(0, lenCsF):
+ if holds[af]:
+ useFaces.append(holds[af]) # save independent solid
+
+ # Eif
+
+ if len(useFaces) > 0:
+ return useFaces
+
+ return False
+
+ def _getModelCrossSection(self, shape, csHght):
+ PathLog.debug('_getCrossSection()')
+ wires = list()
+
+ def byArea(fc):
+ return fc.Area
+
+ for i in shape.slice(FreeCAD.Vector(0, 0, 1), csHght):
+ wires.append(i)
+
+ if len(wires) > 0:
+ for w in wires:
+ if w.isClosed() is False:
+ return False
+ FCS = list()
+ for w in wires:
+ w.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - w.BoundBox.ZMin))
+ FCS.append(Part.Face(w))
+ FCS.sort(key=byArea, reverse=True)
+ return FCS
+ else:
+ PathLog.debug(' -No wires from .slice() method')
+
+ return False
+
+ def _isInBoundBox(self, outShp, inShp):
+ obb = outShp.BoundBox
+ ibb = inShp.BoundBox
+
+ if obb.XMin < ibb.XMin:
+ if obb.XMax > ibb.XMax:
+ if obb.YMin < ibb.YMin:
+ if obb.YMax > ibb.YMax:
+ return True
+ return False
+
+ def _idInternalFeature(self, csFaces, fIds, pIds, li, low):
+ Ids = list()
+ for i in fIds:
+ Ids.append(i)
+ while len(Ids) > 0:
+ hi = Ids.pop()
+ high = csFaces[hi]
+ if self._isInBoundBox(high, low):
+ cmn = high.common(low)
+ if cmn.Area > 0.0:
+ pIds[li] = hi
+ break
+ # Ewhile
+ return pIds
+
+ def _wireToPath(self, obj, wire, startVect):
+ '''_wireToPath(obj, wire, startVect) ... wire to path.'''
+ PathLog.track()
+
+ paths = []
+ pathParams = {} # pylint: disable=assignment-from-no-return
+ V = wire.Vertexes
+
+ pathParams['shapes'] = [wire]
+ pathParams['feedrate'] = self.horizFeed
+ pathParams['feedrate_v'] = self.vertFeed
+ pathParams['verbose'] = True
+ pathParams['resume_height'] = obj.SafeHeight.Value
+ pathParams['retraction'] = obj.ClearanceHeight.Value
+ pathParams['return_end'] = True
+ # Note that emitting preambles between moves breaks some dressups and prevents path optimization on some controllers
+ pathParams['preamble'] = False
+ pathParams['start'] = startVect
+
+ (pp, end_vector) = Path.fromShapes(**pathParams)
+ paths.extend(pp.Commands)
+ # PathLog.debug('pp: {}, end vector: {}'.format(pp, end_vector))
+
+ self.endVector = end_vector # pylint: disable=attribute-defined-outside-init
+
+ return (paths, end_vector)
+
+ def _makeExtendedBoundBox(self, wBB, bbBfr, zDep):
+ pl = FreeCAD.Placement()
+ pl.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), 0)
+ pl.Base = FreeCAD.Vector(0, 0, 0)
+
+ p1 = FreeCAD.Vector(wBB.XMin - bbBfr, wBB.YMin - bbBfr, zDep)
+ p2 = FreeCAD.Vector(wBB.XMax + bbBfr, wBB.YMin - bbBfr, zDep)
+ p3 = FreeCAD.Vector(wBB.XMax + bbBfr, wBB.YMax + bbBfr, zDep)
+ p4 = FreeCAD.Vector(wBB.XMin - bbBfr, wBB.YMax + bbBfr, zDep)
+ bb = Part.makePolygon([p1, p2, p3, p4, p1])
+
+ return bb
+
+ def _makeGcodeArc(self, strtPnt, endPnt, odd, gDIR, tolrnc):
+ cmds = list()
+ 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:
+ 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:
+ # 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 cmds
+
+ def _clearLayer(self, obj, ca, lastCA, clearLastLayer):
+ PathLog.debug('_clearLayer()')
+ usePat = False
+ useOfst = False
+
+ if obj.ClearLastLayer == 'Off':
+ if obj.CutPattern != 'None':
+ usePat = True
+ else:
+ if ca == lastCA:
+ PathLog.debug('... Clearing bottom layer.')
+ if obj.ClearLastLayer == 'Offset':
+ obj.CutPattern = 'None'
+ useOfst = True
+ else:
+ obj.CutPattern = obj.ClearLastLayer
+ usePat = True
+ clearLastLayer = False
+
+ return (useOfst, usePat, clearLastLayer)
# Support functions for both dropcutter and waterline operations
def isPointOnLine(self, strtPnt, endPnt, pointP):
@@ -1959,18 +3356,6 @@ class ObjectWaterline(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
- 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, 'F': self.vertFeed})) # drop cutter down to current Z depth, returning to normal cut path and speed
- return cmds
-
def resetOpVariables(self, all=True):
'''resetOpVariables() ... Reset class variables used for instance of operation.'''
self.holdPoint = None
@@ -2083,33 +3468,19 @@ class ObjectWaterline(PathOp.ObjectOp):
PathLog.error('Unable to set OCL cutter.')
return False
- 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('AngularDeflection')
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('ClearLastLayer')
setup.append('CutMode')
setup.append('CutPattern')
setup.append('CutPatternAngle')
@@ -2118,7 +3489,10 @@ def SetupProperties():
setup.append('GapSizes')
setup.append('GapThreshold')
setup.append('HandleMultipleFeatures')
+ setup.append('InternalFeaturesCut')
+ setup.append('InternalFeaturesAdjustment')
setup.append('LayerMode')
+ setup.append('LinearDeflection')
setup.append('OptimizeStepOverTransitions')
setup.append('ProfileEdges')
setup.append('BoundaryEnforcement')
From ebb383cc6c54fcb94c923e52dc7d127d96ea12d7 Mon Sep 17 00:00:00 2001
From: Russell Johnson <47639332+Russ4262@users.noreply.github.com>
Date: Mon, 30 Mar 2020 23:22:17 -0500
Subject: [PATCH 18/18] Path: Synchronize tooltips. Apply `DepthOffset`. Hide
properties.
Sync tooltip text between 3D Surface and Waterline.
Also, apply `DepthOffset` to Experimental Waterline algorithm.
Hide some properties in Data tab.
---
src/Mod/Path/PathScripts/PathSurface.py | 56 +++++++++++------------
src/Mod/Path/PathScripts/PathWaterline.py | 33 +++++++------
2 files changed, 47 insertions(+), 42 deletions(-)
diff --git a/src/Mod/Path/PathScripts/PathSurface.py b/src/Mod/Path/PathScripts/PathSurface.py
index a485225402..19af63586d 100644
--- a/src/Mod/Path/PathScripts/PathSurface.py
+++ b/src/Mod/Path/PathScripts/PathSurface.py
@@ -97,7 +97,7 @@ class ObjectSurface(PathOp.ObjectOp):
PROPS = [
("App::PropertyBool", "ShowTempObjects", "Debug",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "If true, the temporary path construction objects will be shown.")),
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Show the temporary path construction objects when module is in DEBUG mode.")),
("App::PropertyDistance", "AngularDeflection", "Mesh Conversion",
QtCore.QT_TRANSLATE_NOOP("App::Property", "Smaller values yield a finer, more accurate mesh. Smaller values increase processing time a lot.")),
@@ -117,62 +117,62 @@ class ObjectSurface(PathOp.ObjectOp):
("App::PropertyFloat", "StopIndex", "Rotational",
QtCore.QT_TRANSLATE_NOOP("App::Property", "Stop index(angle) for rotational scan")),
- ("App::PropertyEnumeration", "BoundBox", "Surface",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Should the operation be limited by the stock object or by the bounding box of the base object")),
("App::PropertyEnumeration", "ScanType", "Surface",
QtCore.QT_TRANSLATE_NOOP("App::Property", "Planar: Flat, 3D surface scan. Rotational: 4th-axis rotational scan.")),
- ("App::PropertyDistance", "SampleInterval", "Surface",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "The Sample Interval. Small values cause long wait times")),
- ("App::PropertyInteger", "AvoidLastX_Faces", "Selected Face(s) Settings",
+ ("App::PropertyInteger", "AvoidLastX_Faces", "Selected Geometry Settings",
QtCore.QT_TRANSLATE_NOOP("App::Property", "Avoid cutting the last 'N' faces in the Base Geometry list of selected faces.")),
- ("App::PropertyBool", "AvoidLastX_InternalFeatures", "Selected Face(s) Settings",
+ ("App::PropertyBool", "AvoidLastX_InternalFeatures", "Selected Geometry Settings",
QtCore.QT_TRANSLATE_NOOP("App::Property", "Do not cut internal features on avoided faces.")),
- ("App::PropertyDistance", "BoundaryAdjustment", "Selected Face(s) Settings",
+ ("App::PropertyDistance", "BoundaryAdjustment", "Selected Geometry Settings",
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.")),
- ("App::PropertyBool", "BoundaryEnforcement", "Selected Face(s) Settings",
+ ("App::PropertyBool", "BoundaryEnforcement", "Selected Geometry Settings",
QtCore.QT_TRANSLATE_NOOP("App::Property", "If true, the cutter will remain inside the boundaries of the model or selected face(s).")),
- ("App::PropertyEnumeration", "HandleMultipleFeatures", "Selected Face(s) Settings",
+ ("App::PropertyEnumeration", "HandleMultipleFeatures", "Selected Geometry Settings",
QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose how to process multiple Base Geometry features.")),
- ("App::PropertyDistance", "InternalFeaturesAdjustment", "Selected Face(s) Settings",
+ ("App::PropertyDistance", "InternalFeaturesAdjustment", "Selected Geometry Settings",
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.")),
- ("App::PropertyBool", "InternalFeaturesCut", "Selected Face(s) Settings",
+ ("App::PropertyBool", "InternalFeaturesCut", "Selected Geometry Settings",
QtCore.QT_TRANSLATE_NOOP("App::Property", "Ignore internal feature areas within a larger selected face.")),
+ ("App::PropertyEnumeration", "BoundBox", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Select the overall boundary for the operation. ")),
("App::PropertyVectorDistance", "CircularCenterCustom", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "The start point of this path")),
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the start point for circular cut patterns.")),
("App::PropertyEnumeration", "CircularCenterAt", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose what point to start the ciruclar pattern: Center Of Mass, Center Of Boundbox, Xmin Ymin of boundbox, Custom.")),
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose location of the center point for starting the ciruclar pattern.")),
("App::PropertyEnumeration", "CutMode", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "The direction that the toolpath should go around the part: Climb(ClockWise) or Conventional(CounterClockWise)")),
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the direction for the cutting tool to engage the material: Climb (ClockWise) or Conventional (CounterClockWise)")),
("App::PropertyEnumeration", "CutPattern", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Clearing pattern to use")),
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the geometric clearing pattern to use for the operation.")),
("App::PropertyFloat", "CutPatternAngle", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Yaw angle for certain clearing patterns")),
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "The yaw angle used for certain clearing patterns")),
("App::PropertyBool", "CutPatternReversed", "Clearing 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.")),
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Reverse the cut order of the stepover paths. For circular cut patterns, begin at the outside and work toward the center.")),
("App::PropertyDistance", "DepthOffset", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Z-axis offset from the surface of the object")),
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the Z-axis depth offset from the target surface.")),
("App::PropertyEnumeration", "LayerMode", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "The completion mode for the operation: single or multi-pass")),
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Complete the operation in a single pass at depth, or mulitiple passes to final depth.")),
("App::PropertyEnumeration", "ProfileEdges", "Clearing Options",
QtCore.QT_TRANSLATE_NOOP("App::Property", "Profile the edges of the selection.")),
+ ("App::PropertyDistance", "SampleInterval", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the sampling resolution. Smaller values quickly increase processing time.")),
("App::PropertyPercent", "StepOver", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Step over percentage of the drop cutter path")),
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the stepover percentage, based on the tool's diameter.")),
- ("App::PropertyBool", "OptimizeLinearPaths", "Surface Optimization",
+ ("App::PropertyBool", "OptimizeLinearPaths", "Optimization",
QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable optimization of linear paths (co-linear points). Removes unnecessary co-linear points from G-Code output.")),
- ("App::PropertyBool", "OptimizeStepOverTransitions", "Surface Optimization",
+ ("App::PropertyBool", "OptimizeStepOverTransitions", "Optimization",
QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable separate optimization of transitions between, and breaks within, each step over path.")),
- ("App::PropertyBool", "CircularUseG2G3", "Surface Optimization",
+ ("App::PropertyBool", "CircularUseG2G3", "Optimization",
QtCore.QT_TRANSLATE_NOOP("App::Property", "Convert co-planar arcs to G2/G3 gcode commands for `Circular` and `CircularZigZag` cut patterns.")),
- ("App::PropertyDistance", "GapThreshold", "Surface Optimization",
+ ("App::PropertyDistance", "GapThreshold", "Optimization",
QtCore.QT_TRANSLATE_NOOP("App::Property", "Collinear and co-radial artifact gaps that are smaller than this threshold are closed in the path.")),
- ("App::PropertyString", "GapSizes", "Surface Optimization",
+ ("App::PropertyString", "GapSizes", "Optimization",
QtCore.QT_TRANSLATE_NOOP("App::Property", "Feedback: three smallest gaps identified in the path geometry.")),
("App::PropertyVectorDistance", "StartPoint", "Start Point",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "The start point of this path")),
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "The custom start point for the path of this operation")),
("App::PropertyBool", "UseStartPoint", "Start Point",
QtCore.QT_TRANSLATE_NOOP("App::Property", "Make True, if specifying a Start Point"))
]
@@ -199,7 +199,7 @@ class ObjectSurface(PathOp.ObjectOp):
'BoundBox': ['BaseBoundBox', 'Stock'],
'CircularCenterAt': ['CenterOfMass', 'CenterOfBoundBox', 'XminYmin', 'Custom'],
'CutMode': ['Conventional', 'Climb'],
- 'CutPattern': ['Line', 'ZigZag', 'Circular', 'CircularZigZag'], # Additional goals ['Offset', 'Spiral', 'ZigZagOffset', 'Grid', 'Triangle']
+ 'CutPattern': ['Line', 'Circular', 'CircularZigZag', 'ZigZag'], # Additional goals ['Offset', 'Spiral', 'ZigZagOffset', 'Grid', 'Triangle']
'DropCutterDir': ['X', 'Y'],
'HandleMultipleFeatures': ['Collectively', 'Individually'],
'LayerMode': ['Single-pass', 'Multi-pass'],
diff --git a/src/Mod/Path/PathScripts/PathWaterline.py b/src/Mod/Path/PathScripts/PathWaterline.py
index b2d3fdc197..bdc4fda076 100644
--- a/src/Mod/Path/PathScripts/PathWaterline.py
+++ b/src/Mod/Path/PathScripts/PathWaterline.py
@@ -22,11 +22,6 @@
# * USA *
# * *
# ***************************************************************************
-# * *
-# * Additional modifications and contributions beginning 2019 *
-# * by Russell Johnson 2020-03-30 22:27 CST *
-# * *
-# ***************************************************************************
from __future__ import print_function
@@ -83,7 +78,7 @@ class ObjectWaterline(PathOp.ObjectOp):
return PathOp.FeatureTool | PathOp.FeatureDepths | PathOp.FeatureHeights | PathOp.FeatureStepDown | PathOp.FeatureCoolant | PathOp.FeatureBaseFaces
def initOperation(self, obj):
- '''initPocketOp(obj) ...
+ '''initPocketOp(obj) ...
Initialize the operation - property creation and property editor status.'''
self.initOpProperties(obj)
@@ -98,7 +93,7 @@ class ObjectWaterline(PathOp.ObjectOp):
'''initOpProperties(obj) ... create operation specific properties'''
PROPS = [
("App::PropertyBool", "ShowTempObjects", "Debug",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "If true, the temporary path construction objects will be shown.")),
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Show the temporary path construction objects when module is in DEBUG mode.")),
("App::PropertyDistance", "AngularDeflection", "Mesh Conversion",
QtCore.QT_TRANSLATE_NOOP("App::Property", "Smaller values yield a finer, more accurate the mesh. Smaller values increase processing time a lot.")),
@@ -125,27 +120,27 @@ class ObjectWaterline(PathOp.ObjectOp):
("App::PropertyEnumeration", "BoundBox", "Clearing Options",
QtCore.QT_TRANSLATE_NOOP("App::Property", "Select the overall boundary for the operation. ")),
("App::PropertyVectorDistance", "CircularCenterCustom", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "The start point of this path.")),
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the start point for circular cut patterns.")),
("App::PropertyEnumeration", "CircularCenterAt", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose center point to start the ciruclar pattern.")),
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose location of the center point for starting the ciruclar pattern.")),
("App::PropertyEnumeration", "ClearLastLayer", "Clearing Options",
QtCore.QT_TRANSLATE_NOOP("App::Property", "Set to clear last layer in a `Multi-pass` operation.")),
("App::PropertyEnumeration", "CutMode", "Clearing Options",
QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the direction for the cutting tool to engage the material: Climb (ClockWise) or Conventional (CounterClockWise)")),
("App::PropertyEnumeration", "CutPattern", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the geometric clearing pattern to use.")),
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the geometric clearing pattern to use for the operation.")),
("App::PropertyFloat", "CutPatternAngle", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Yaw angle for certain clearing patterns")),
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "The yaw angle used for certain clearing patterns")),
("App::PropertyBool", "CutPatternReversed", "Clearing Options",
QtCore.QT_TRANSLATE_NOOP("App::Property", "Reverse the cut order of the stepover paths. For circular cut patterns, begin at the outside and work toward the center.")),
("App::PropertyDistance", "DepthOffset", "Clearing Options",
QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the Z-axis depth offset from the target surface.")),
("App::PropertyEnumeration", "LayerMode", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "The step down mode for the operation.")),
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Complete the operation in a single pass at depth, or mulitiple passes to final depth.")),
("App::PropertyEnumeration", "ProfileEdges", "Clearing Options",
QtCore.QT_TRANSLATE_NOOP("App::Property", "Profile the edges of the selection.")),
("App::PropertyDistance", "SampleInterval", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the sampling resolution. Smaller values increase processing time.")),
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the sampling resolution. Smaller values quickly increase processing time.")),
("App::PropertyPercent", "StepOver", "Clearing Options",
QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the stepover percentage, based on the tool's diameter.")),
@@ -159,7 +154,7 @@ class ObjectWaterline(PathOp.ObjectOp):
QtCore.QT_TRANSLATE_NOOP("App::Property", "Feedback: three smallest gaps identified in the path geometry.")),
("App::PropertyVectorDistance", "StartPoint", "Start Point",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "The start point of this path")),
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "The custom start point for the path of this operation")),
("App::PropertyBool", "UseStartPoint", "Start Point",
QtCore.QT_TRANSLATE_NOOP("App::Property", "Make True, if specifying a Start Point"))
]
@@ -204,6 +199,14 @@ class ObjectWaterline(PathOp.ObjectOp):
obj.setEditorMode('ProfileEdges', hide)
obj.setEditorMode('InternalFeaturesAdjustment', hide)
obj.setEditorMode('InternalFeaturesCut', hide)
+ obj.setEditorMode('GapSizes', hide)
+ obj.setEditorMode('GapThreshold', hide)
+ obj.setEditorMode('AvoidLastX_Faces', hide)
+ obj.setEditorMode('AvoidLastX_InternalFeatures', hide)
+ obj.setEditorMode('BoundaryAdjustment', hide)
+ obj.setEditorMode('HandleMultipleFeatures', hide)
+ if hasattr(obj, 'EnableRotation'):
+ obj.setEditorMode('EnableRotation', hide)
if obj.CutPattern == 'None':
show = 2
hide = 2
@@ -265,6 +268,7 @@ class ObjectWaterline(PathOp.ObjectOp):
obj.ClearLastLayer = 'Off'
obj.StepOver = 100
obj.CutPatternAngle = 0.0
+ obj.DepthOffset.Value = 0.0
obj.SampleInterval.Value = 1.0
obj.BoundaryAdjustment.Value = 0.0
obj.InternalFeaturesAdjustment.Value = 0.0
@@ -2793,6 +2797,7 @@ class ObjectWaterline(PathOp.ObjectOp):
for ca in range(0, caLen):
area = CUTAREAS[ca]
csHght = area.BoundBox.ZMin
+ csHght += obj.DepthOffset.Value
cont = False
caCnt += 1
if area.Area > 0.0: