Files
create/src/Mod/Path/PathScripts/PathSurface.py
2020-03-18 12:32:45 -05:00

4226 lines
177 KiB
Python

# -*- coding: utf-8 -*-
# ***************************************************************************
# * *
# * Copyright (c) 2016 sliptonic <shopinthewoods@gmail.com> *
# * *
# * 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 <russ4262@gmail.com> 2020-03-18 12:29 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", "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 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.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)
self.addedAllProperties = True
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)
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):
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
obj.AngularDeflection.Value = 0.25
obj.LinearDeflection.Value = job.GeometryTolerance
# 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
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.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.deflection
execTime = time.time() - startTime
PathLog.info('Operation time: {} sec.'.format(execTime))
return True
# Methods for constructing the cut area
def _preProcessModel(self, JOB, obj):
PathLog.debug('_preProcessModel()')
FACES = list()
VOIDS = list()
fShapes = list()
vShapes = list()
preProcEr = translate('PathSurface', 'Error pre-processing Face')
warnFinDep = translate('PathSurface', 'Final Depth might need to be lower. Internal features detected in Face')
GRP = JOB.Model.Group
lenGRP = len(GRP)
# Crete place holders for each base model in Job
for m in range(0, lenGRP):
FACES.append(False)
VOIDS.append(False)
fShapes.append(False)
vShapes.append(False)
# The user has selected subobjects from the base. Pre-Process each.
if obj.Base and len(obj.Base) > 0:
PathLog.debug(' -obj.Base exists. Pre-processing for selected faces.')
(FACES, VOIDS) = self._identifyFacesAndVoids(JOB, obj, FACES, VOIDS)
# Cycle through each base model, processing faces for each
for m in range(0, lenGRP):
base = GRP[m]
(mFS, mVS, mPS) = self._preProcessFacesAndVoids(obj, base, m, FACES, VOIDS)
fShapes[m] = mFS
vShapes[m] = mVS
self.profileShapes[m] = mPS
else:
PathLog.debug(' -No obj.Base data.')
for m in range(0, lenGRP):
self.modelSTLs[m] = True
# Process each model base, as a whole, as needed
# PathLog.debug(' -Pre-processing all models in Job.')
for m in range(0, lenGRP):
if fShapes[m] is False:
PathLog.debug(' -Pre-processing {} as a whole.'.format(GRP[m].Label))
if obj.BoundBox == 'BaseBoundBox':
base = GRP[m]
elif obj.BoundBox == 'Stock':
base = JOB.Stock
pPEB = self._preProcessEntireBase(obj, base, m)
if pPEB is False:
PathLog.error(' -Failed to pre-process base as a whole.')
else:
(fcShp, prflShp) = pPEB
if fcShp is not False:
if fcShp is True:
PathLog.debug(' -fcShp is True.')
fShapes[m] = True
else:
fShapes[m] = [fcShp]
if prflShp is not False:
if fcShp is not False:
PathLog.debug('vShapes[{}]: {}'.format(m, vShapes[m]))
if vShapes[m] is not False:
PathLog.debug(' -Cutting void from base profile shape.')
adjPS = prflShp.cut(vShapes[m][0])
self.profileShapes[m] = [adjPS]
else:
PathLog.debug(' -vShapes[m] is False.')
self.profileShapes[m] = [prflShp]
else:
PathLog.debug(' -Saving base profile shape.')
self.profileShapes[m] = [prflShp]
PathLog.debug('self.profileShapes[{}]: {}'.format(m, self.profileShapes[m]))
# Efor
return (fShapes, vShapes)
def _identifyFacesAndVoids(self, JOB, obj, F, V):
TUPS = list()
GRP = JOB.Model.Group
lenGRP = len(GRP)
# Separate selected faces into (base, face) tuples and flag model(s) for STL creation
for (bs, SBS) in obj.Base:
for sb in SBS:
# Flag model for STL creation
mdlIdx = None
for m in range(0, lenGRP):
if bs is GRP[m]:
self.modelSTLs[m] = True
mdlIdx = m
break
TUPS.append((mdlIdx, bs, sb)) # (model idx, base, sub)
# Apply `AvoidXFaces` value
faceCnt = len(TUPS)
add = faceCnt - obj.AvoidLastX_Faces
for bst in range(0, faceCnt):
(m, base, sub) = TUPS[bst]
shape = getattr(base.Shape, sub)
if isinstance(shape, Part.Face):
faceIdx = int(sub[4:]) - 1
if bst < add:
if F[m] is False:
F[m] = list()
F[m].append((shape, faceIdx))
else:
if V[m] is False:
V[m] = list()
V[m].append((shape, faceIdx))
return (F, V)
def _preProcessFacesAndVoids(self, obj, base, m, FACES, VOIDS):
mFS = False
mVS = False
mPS = False
mIFS = list()
BB = base.Shape.BoundBox
if FACES[m] is not False:
isHole = False
if obj.HandleMultipleFeatures == 'Collectively':
cont = True
fsL = list() # face shape list
ifL = list() # avoid shape list
outFCS = list()
# Get collective envelope slice of selected faces
for (fcshp, fcIdx) in FACES[m]:
fNum = fcIdx + 1
fsL.append(fcshp)
gFW = self._getFaceWires(base, fcshp, fcIdx)
if gFW is False:
PathLog.debug('Failed to get wires from Face{}'.format(fNum))
elif gFW[0] is False:
PathLog.debug('Cannot process Face{}. Check that it has horizontal surface exposure.'.format(fNum))
else:
((otrFace, raised), intWires) = gFW
outFCS.append(otrFace)
if obj.InternalFeaturesCut is False:
if intWires is not False:
for (iFace, rsd) in intWires:
ifL.append(iFace)
PathLog.debug('Attempting to get cross-section of collective faces.')
if len(outFCS) == 0:
PathLog.error('Cannot process selected faces. Check horizontal surface exposure.'.format(fNum))
cont = False
else:
cfsL = Part.makeCompound(outFCS)
# Handle profile edges request
if cont is True and obj.ProfileEdges != 'None':
ofstVal = self._calculateOffsetValue(obj, isHole)
psOfst = self._extractFaceOffset(obj, cfsL, ofstVal)
if psOfst is not False:
mPS = [psOfst]
if obj.ProfileEdges == 'Only':
mFS = True
cont = False
else:
PathLog.error(' -Failed to create profile geometry for selected faces.')
cont = False
if cont is True:
if self.showDebugObjects is True:
T = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCollectiveShape')
T.Shape = cfsL
T.purgeTouched()
self.tempGroup.addObject(T)
ofstVal = self._calculateOffsetValue(obj, isHole)
faceOfstShp = self._extractFaceOffset(obj, cfsL, ofstVal)
if faceOfstShp is False:
PathLog.error(' -Failed to create offset face.')
cont = False
if cont is True:
lenIfL = len(ifL)
if obj.InternalFeaturesCut is False:
if lenIfL == 0:
PathLog.debug(' -No internal features saved.')
else:
if lenIfL == 1:
casL = ifL[0]
else:
casL = Part.makeCompound(ifL)
if self.showDebugObjects is True:
C = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCompoundIntFeat')
C.Shape = casL
C.purgeTouched()
self.tempGroup.addObject(C)
ofstVal = self._calculateOffsetValue(obj, isHole=True)
intOfstShp = self._extractFaceOffset(obj, casL, ofstVal)
mIFS.append(intOfstShp)
# faceOfstShp = faceOfstShp.cut(intOfstShp)
mFS = [faceOfstShp]
# Eif
elif obj.HandleMultipleFeatures == 'Individually':
for (fcshp, fcIdx) in FACES[m]:
cont = True
fsL = list() # face shape list
ifL = list() # avoid shape list
fNum = fcIdx + 1
outerFace = False
gFW = self._getFaceWires(base, fcshp, fcIdx)
if gFW is False:
PathLog.debug('Failed to get wires from Face{}'.format(fNum))
cont = False
elif gFW[0] is False:
PathLog.debug('Cannot process Face{}. Check that it has horizontal surface exposure.'.format(fNum))
cont = False
outerFace = False
else:
((otrFace, raised), intWires) = gFW
outerFace = otrFace
if obj.InternalFeaturesCut is False:
if intWires is not False:
for (iFace, rsd) in intWires:
ifL.append(iFace)
if outerFace is not False:
PathLog.debug('Attempting to create offset face of Face{}'.format(fNum))
if obj.ProfileEdges != 'None':
ofstVal = self._calculateOffsetValue(obj, isHole)
psOfst = self._extractFaceOffset(obj, outerFace, ofstVal)
if psOfst is not False:
if mPS is False:
mPS = list()
mPS.append(psOfst)
if obj.ProfileEdges == 'Only':
if mFS is False:
mFS = list()
mFS.append(True)
cont = False
else:
PathLog.error(' -Failed to create profile geometry for Face{}.'.format(fNum))
cont = False
if cont is True:
ofstVal = self._calculateOffsetValue(obj, isHole)
faceOfstShp = self._extractFaceOffset(obj, outerFace, ofstVal)
lenIfl = len(ifL)
if obj.InternalFeaturesCut is False and lenIfl > 0:
if lenIfl == 1:
casL = ifL[0]
else:
casL = Part.makeCompound(ifL)
ofstVal = self._calculateOffsetValue(obj, isHole=True)
intOfstShp = self._extractFaceOffset(obj, casL, ofstVal)
mIFS.append(intOfstShp)
# faceOfstShp = faceOfstShp.cut(intOfstShp)
if mFS is False:
mFS = list()
mFS.append(faceOfstShp)
# Eif
# Efor
# Eif
# Eif
if len(mIFS) > 0:
if mVS is False:
mVS = list()
for ifs in mIFS:
mVS.append(ifs)
if VOIDS[m] is not False:
PathLog.debug('Processing avoid faces.')
cont = True
isHole = False
outFCS = list()
intFEAT = list()
for (fcshp, fcIdx) in VOIDS[m]:
fNum = fcIdx + 1
gFW = self._getFaceWires(base, fcshp, fcIdx)
if gFW is False:
PathLog.debug('Failed to get wires from avoid Face{}'.format(fNum))
cont = False
else:
((otrFace, raised), intWires) = gFW
outFCS.append(otrFace)
if obj.AvoidLastX_InternalFeatures is False:
if intWires is not False:
for (iFace, rsd) in intWires:
intFEAT.append(iFace)
lenOtFcs = len(outFCS)
if lenOtFcs == 0:
cont = False
else:
if lenOtFcs == 1:
avoid = outFCS[0]
else:
avoid = Part.makeCompound(outFCS)
if self.showDebugObjects is True:
PathLog.debug('*** tmpAvoidArea')
P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpVoidEnvelope')
P.Shape = avoid
# P.recompute()
P.purgeTouched()
self.tempGroup.addObject(P)
if cont is True:
if self.showDebugObjects is True:
PathLog.debug('*** tmpVoidCompound')
P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpVoidCompound')
P.Shape = avoid
# P.recompute()
P.purgeTouched()
self.tempGroup.addObject(P)
ofstVal = self._calculateOffsetValue(obj, isHole, isVoid=True)
avdOfstShp = self._extractFaceOffset(obj, avoid, ofstVal)
if avdOfstShp is False:
PathLog.error('Failed to create collective offset avoid face.')
cont = False
if cont is True:
avdShp = avdOfstShp
if obj.AvoidLastX_InternalFeatures is False and len(intFEAT) > 0:
if len(intFEAT) > 1:
ifc = Part.makeCompound(intFEAT)
else:
ifc = intFEAT[0]
ofstVal = self._calculateOffsetValue(obj, isHole=True)
ifOfstShp = self._extractFaceOffset(obj, ifc, ofstVal)
if ifOfstShp is False:
PathLog.error('Failed to create collective offset avoid internal features.')
else:
avdShp = avdOfstShp.cut(ifOfstShp)
if mVS is False:
mVS = list()
mVS.append(avdShp)
return (mFS, mVS, mPS)
def _getFaceWires(self, base, fcshp, fcIdx):
outFace = False
INTFCS = list()
fNum = fcIdx + 1
# preProcEr = translate('PathSurface', 'Error pre-processing Face')
warnFinDep = translate('PathSurface', 'Final Depth might need to be lower. Internal features detected in Face')
PathLog.debug('_getFaceWires() from Face{}'.format(fNum))
WIRES = self._extractWiresFromFace(base, fcshp)
if WIRES is False:
PathLog.error('Failed to extract wires from Face{}'.format(fNum))
return False
# Process remaining internal features, adding to FCS list
lenW = len(WIRES)
for w in range(0, lenW):
(wire, rsd) = WIRES[w]
PathLog.debug('Processing Wire{} in Face{}. isRaised: {}'.format(w + 1, fNum, rsd))
if wire.isClosed() is False:
PathLog.debug(' -wire is not closed.')
else:
slc = self._flattenWireToFace(wire)
if slc is False:
PathLog.error('FAILED to identify horizontal exposure on Face{}.'.format(fNum))
else:
if w == 0:
outFace = (slc, rsd)
else:
# add to VOIDS so cutter avoids area.
PathLog.warning(warnFinDep + str(fNum) + '.')
INTFCS.append((slc, rsd))
if len(INTFCS) == 0:
return (outFace, False)
else:
return (outFace, INTFCS)
def _preProcessEntireBase(self, obj, base, m):
cont = True
isHole = False
prflShp = False
# Create envelope, extract cross-section and make offset co-planar shape
# baseEnv = PathUtils.getEnvelope(base.Shape, subshape=None, depthparams=self.depthParams)
try:
baseEnv = PathUtils.getEnvelope(partshape=base.Shape, subshape=None, depthparams=self.depthParams) # Produces .Shape
except Exception as ee:
PathLog.error(str(ee))
shell = base.Shape.Shells[0]
solid = Part.makeSolid(shell)
try:
baseEnv = PathUtils.getEnvelope(partshape=solid, subshape=None, depthparams=self.depthParams) # Produces .Shape
except Exception as eee:
PathLog.error(str(eee))
cont = False
time.sleep(0.2)
if cont is True:
csFaceShape = self._getShapeSlice(baseEnv)
if csFaceShape is False:
PathLog.debug('_getShapeSlice(baseEnv) failed')
csFaceShape = self._getCrossSection(baseEnv)
if csFaceShape is False:
PathLog.debug('_getCrossSection(baseEnv) failed')
csFaceShape = self._getSliceFromEnvelope(baseEnv)
if csFaceShape is False:
PathLog.error('Failed to slice baseEnv shape.')
cont = False
if cont is True and obj.ProfileEdges != 'None':
PathLog.debug(' -Attempting profile geometry for model base.')
ofstVal = self._calculateOffsetValue(obj, isHole)
psOfst = self._extractFaceOffset(obj, csFaceShape, ofstVal)
if psOfst is not False:
if obj.ProfileEdges == 'Only':
return (True, psOfst)
prflShp = psOfst
else:
PathLog.error(' -Failed to create profile geometry.')
cont = False
if cont is True:
ofstVal = self._calculateOffsetValue(obj, isHole)
faceOffsetShape = self._extractFaceOffset(obj, csFaceShape, ofstVal)
if faceOffsetShape is False:
PathLog.error('_extractFaceOffset() failed.')
else:
faceOffsetShape.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - faceOffsetShape.BoundBox.ZMin))
return (faceOffsetShape, prflShp)
return False
def _extractWiresFromFace(self, base, fc):
'''_extractWiresFromFace(base, fc) ...
Attempts to return all closed wires within a parent face, including the outer most wire of the parent.
The wires are ordered by area. Each wire is also categorized as a pocket(False) or raised protrusion(True).
'''
PathLog.debug('_extractWiresFromFace()')
WIRES = list()
lenWrs = len(fc.Wires)
PathLog.debug(' -Wire count: {}'.format(lenWrs))
def index0(tup):
return tup[0]
# Cycle through wires in face
for w in range(0, lenWrs):
PathLog.debug(' -Analyzing wire_{}'.format(w + 1))
wire = fc.Wires[w]
checkEdges = False
cont = True
# Check for closed edges (circles, ellipses, etc...)
for E in wire.Edges:
if E.isClosed() is True:
checkEdges = True
break
if checkEdges is True:
PathLog.debug(' -checkEdges is True')
for e in range(0, len(wire.Edges)):
edge = wire.Edges[e]
if edge.isClosed() is True and edge.Mass > 0.01:
PathLog.debug(' -Found closed edge')
raised = False
ip = self._isPocket(base, fc, edge)
if ip is False:
raised = True
ebb = edge.BoundBox
eArea = ebb.XLength * ebb.YLength
F = Part.Face(Part.Wire([edge]))
WIRES.append((eArea, F.Wires[0], raised))
cont = False
if cont is True:
PathLog.debug(' -cont is True')
# If only one wire and not checkEdges, return first wire
if lenWrs == 1:
return [(wire, False)]
raised = False
wbb = wire.BoundBox
wArea = wbb.XLength * wbb.YLength
if w > 0:
ip = self._isPocket(base, fc, wire)
if ip is False:
raised = True
WIRES.append((wArea, Part.Wire(wire.Edges), raised))
nf = len(WIRES)
if nf > 0:
PathLog.debug(' -number of wires found is {}'.format(nf))
if nf == 1:
(area, W, raised) = WIRES[0]
return [(W, raised)]
else:
sortedWIRES = sorted(WIRES, key=index0, reverse=True)
return [(W, raised) for (area, W, raised) in sortedWIRES] # outer, then inner by area size
return False
def _calculateOffsetValue(self, obj, isHole, isVoid=False):
'''_calculateOffsetValue(obj, isHole, isVoid) ... internal function.
Calculate the offset for the Path.Area() function.'''
JOB = PathUtils.findParentJob(obj)
tolrnc = JOB.GeometryTolerance.Value
if isVoid is False:
if isHole is True:
offset = -1 * obj.InternalFeaturesAdjustment.Value
offset += self.radius # (self.radius + (tolrnc / 10.0))
else:
offset = -1 * obj.BoundaryAdjustment.Value
if obj.BoundaryEnforcement is True:
offset += self.radius # (self.radius + (tolrnc / 10.0))
else:
offset -= self.radius # (self.radius + (tolrnc / 10.0))
offset = 0.0 - offset
else:
offset = -1 * obj.BoundaryAdjustment.Value
offset += self.radius # (self.radius + (tolrnc / 10.0))
return offset
def _extractFaceOffset(self, obj, fcShape, offset):
'''_extractFaceOffset(fcShape, offset) ... internal function.
Original _buildPathArea() version copied from PathAreaOp.py module. This version is modified.
Adjustments made based on notes by @sliptonic at this webpage: https://github.com/sliptonic/FreeCAD/wiki/PathArea-notes.'''
PathLog.debug('_extractFaceOffset()')
if fcShape.BoundBox.ZMin != 0.0:
fcShape.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - fcShape.BoundBox.ZMin))
areaParams = {}
areaParams['Offset'] = offset
areaParams['Fill'] = 1
areaParams['Coplanar'] = 0
areaParams['SectionCount'] = 1 # -1 = full(all per depthparams??) sections
areaParams['Reorient'] = True
areaParams['OpenMode'] = 0
areaParams['MaxArcPoints'] = 400 # 400
areaParams['Project'] = True
area = Path.Area() # Create instance of Area() class object
# area.setPlane(PathUtils.makeWorkplane(fcShape)) # Set working plane
area.setPlane(PathUtils.makeWorkplane(self.wpc)) # Set working plane to normal at Z=1
area.add(fcShape)
area.setParams(**areaParams) # set parameters
# Save parameters for debugging
# obj.AreaParams = str(area.getParams())
# PathLog.debug("Area with params: {}".format(area.getParams()))
offsetShape = area.getShape()
wCnt = len(offsetShape.Wires)
if wCnt == 0:
return False
elif wCnt == 1:
ofstFace = Part.Face(offsetShape.Wires[0])
else:
W = list()
for wr in offsetShape.Wires:
W.append(Part.Face(wr))
ofstFace = Part.makeCompound(W)
return ofstFace # offsetShape
def _isPocket(self, b, f, w):
'''_isPocket(b, f, w)...
Attempts to determing if the wire(w) in face(f) of base(b) is a pocket or raised protrusion.
Returns True if pocket, False if raised protrusion.'''
e = w.Edges[0]
for fi in range(0, len(b.Shape.Faces)):
face = b.Shape.Faces[fi]
for ei in range(0, len(face.Edges)):
edge = face.Edges[ei]
if e.isSame(edge) is True:
if f is face:
# Alternative: run loop to see if all edges are same
pass # same source face, look for another
else:
if face.CenterOfMass.z < f.CenterOfMass.z:
return True
return False
def _flattenWireToFace(self, wire):
PathLog.debug('_flattenWireToFace()')
if wire.isClosed() is False:
PathLog.debug(' -wire.isClosed() is False')
return False
# If wire is planar horizontal, convert to a face and return
if wire.BoundBox.ZLength == 0.0:
slc = Part.Face(wire)
return slc
# Attempt to create a new wire for manipulation, if not, use original
newWire = Part.Wire(wire.Edges)
if newWire.isClosed() is True:
nWire = newWire
else:
PathLog.debug(' -newWire.isClosed() is False')
nWire = wire
# Attempt extrusion, and then try a manual slice and then cross-section
ext = self._getExtrudedShape(nWire)
if ext is False:
PathLog.debug('_getExtrudedShape() failed')
else:
slc = self._getShapeSlice(ext)
if slc is not False:
return slc
cs = self._getCrossSection(ext, True)
if cs is not False:
return cs
# Attempt creating an envelope, and then try a manual slice and then cross-section
env = self._getShapeEnvelope(nWire)
if env is False:
PathLog.debug('_getShapeEnvelope() failed')
else:
slc = self._getShapeSlice(env)
if slc is not False:
return slc
cs = self._getCrossSection(env, True)
if cs is not False:
return cs
# Attempt creating a projection
slc = self._getProjectedFace(nWire)
if slc is False:
PathLog.debug('_getProjectedFace() failed')
else:
return slc
return False
def _getExtrudedShape(self, wire):
PathLog.debug('_getExtrudedShape()')
wBB = wire.BoundBox
extFwd = math.floor(2.0 * wBB.ZLength) + 10.0
try:
# slower, but renders collective faces correctly. Method 5 in TESTING
shell = wire.extrude(FreeCAD.Vector(0.0, 0.0, extFwd))
except Exception as ee:
PathLog.error(' -extrude wire failed: \n{}'.format(ee))
return False
SHP = Part.makeSolid(shell)
return SHP
def _getShapeSlice(self, shape):
PathLog.debug('_getShapeSlice()')
bb = shape.BoundBox
mid = (bb.ZMin + bb.ZMax) / 2.0
xmin = bb.XMin - 1.0
xmax = bb.XMax + 1.0
ymin = bb.YMin - 1.0
ymax = bb.YMax + 1.0
p1 = FreeCAD.Vector(xmin, ymin, mid)
p2 = FreeCAD.Vector(xmax, ymin, mid)
p3 = FreeCAD.Vector(xmax, ymax, mid)
p4 = FreeCAD.Vector(xmin, ymax, mid)
e1 = Part.makeLine(p1, p2)
e2 = Part.makeLine(p2, p3)
e3 = Part.makeLine(p3, p4)
e4 = Part.makeLine(p4, p1)
face = Part.Face(Part.Wire([e1, e2, e3, e4]))
fArea = face.BoundBox.XLength * face.BoundBox.YLength # face.Wires[0].Area
sArea = shape.BoundBox.XLength * shape.BoundBox.YLength
midArea = (fArea + sArea) / 2.0
slcShp = shape.common(face)
slcArea = slcShp.BoundBox.XLength * slcShp.BoundBox.YLength
if slcArea < midArea:
for W in slcShp.Wires:
if W.isClosed() is False:
PathLog.debug(' -wire.isClosed() is False')
return False
if len(slcShp.Wires) == 1:
wire = slcShp.Wires[0]
slc = Part.Face(wire)
slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin))
return slc
else:
fL = list()
for W in slcShp.Wires:
slc = Part.Face(W)
slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin))
fL.append(slc)
comp = Part.makeCompound(fL)
if self.showDebugObjects is True:
PathLog.debug('*** tmpSliceCompound')
P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpSliceCompound')
P.Shape = comp
# P.recompute()
P.purgeTouched()
self.tempGroup.addObject(P)
return comp
PathLog.debug(' -slcArea !< midArea')
PathLog.debug(' -slcShp.Edges count: {}. Might be a vertically oriented face.'.format(len(slcShp.Edges)))
return False
def _getProjectedFace(self, wire):
PathLog.debug('_getProjectedFace()')
F = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpProjectionWire')
F.Shape = wire
F.purgeTouched()
self.tempGroup.addObject(F)
try:
prj = Draft.makeShape2DView(F, FreeCAD.Vector(0, 0, 1))
prj.recompute()
prj.purgeTouched()
self.tempGroup.addObject(prj)
except Exception as ee:
PathLog.error(str(ee))
return False
else:
pWire = Part.Wire(prj.Shape.Edges)
if pWire.isClosed() is False:
# PathLog.debug(' -pWire.isClosed() is False')
return False
slc = Part.Face(pWire)
slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin))
return slc
return False
def _getCrossSection(self, shape, withExtrude=False):
PathLog.debug('_getCrossSection()')
wires = list()
bb = shape.BoundBox
mid = (bb.ZMin + bb.ZMax) / 2.0
for i in shape.slice(FreeCAD.Vector(0, 0, 1), mid):
wires.append(i)
if len(wires) > 0:
comp = Part.Compound(wires) # produces correct cross-section wire !
comp.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - comp.BoundBox.ZMin))
csWire = comp.Wires[0]
if csWire.isClosed() is False:
PathLog.debug(' -comp.Wires[0] is not closed')
return False
if withExtrude is True:
ext = self._getExtrudedShape(csWire)
CS = self._getShapeSlice(ext)
if CS is False:
return False
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=obj.LinearDeflection.Value,
AngularDeflection=obj.AngularDeflection.Value,
Relative=False)
if self.modelSTLs[m] is True:
stl = ocl.STLSurf()
if obj.Algorithm == 'OCL Dropcutter':
for f in mesh.Facets:
p = f.Points[0]
q = f.Points[1]
r = f.Points[2]
t = ocl.Triangle(ocl.Point(p[0], p[1], p[2]),
ocl.Point(q[0], q[1], q[2]),
ocl.Point(r[0], r[1], r[2]))
stl.addTriangle(t)
self.modelSTLs[m] = stl
elif obj.Algorithm == 'OCL Waterline':
for f in mesh.Facets:
p = f.Points[0]
q = f.Points[1]
r = f.Points[2]
t = ocl.Triangle(ocl.Point(p[0], p[1], p[2] + obj.DepthOffset.Value),
ocl.Point(q[0], q[1], q[2] + obj.DepthOffset.Value),
ocl.Point(r[0], r[1], r[2] + obj.DepthOffset.Value))
stl.addTriangle(t)
self.modelSTLs[m] = stl
return
def _makeSafeSTL(self, JOB, obj, mdlIdx, faceShapes, voidShapes):
'''_makeSafeSTL(JOB, obj, mdlIdx, faceShapes, voidShapes)...
Creates and OCL.stl object with combined data with waste stock,
model, and avoided faces. Travel lines can be checked against this
STL object to determine minimum travel height to clear stock and model.'''
PathLog.debug('_makeSafeSTL()')
fuseShapes = list()
Mdl = JOB.Model.Group[mdlIdx]
FCAD = FreeCAD.ActiveDocument
mBB = Mdl.Shape.BoundBox
sBB = JOB.Stock.Shape.BoundBox
# add Model shape to safeSTL shape
fuseShapes.append(Mdl.Shape)
if obj.BoundBox == 'BaseBoundBox':
cont = False
extFwd = (sBB.ZLength)
zmin = mBB.ZMin
zmax = mBB.ZMin + extFwd
stpDwn = (zmax - zmin) / 4.0
dep_par = PathUtils.depth_params(zmax + 5.0, zmax + 3.0, zmax, stpDwn, 0.0, zmin)
try:
envBB = PathUtils.getEnvelope(partshape=Mdl.Shape, depthparams=dep_par) # Produces .Shape
cont = True
except Exception as ee:
PathLog.error(str(ee))
shell = Mdl.Shape.Shells[0]
solid = Part.makeSolid(shell)
try:
envBB = PathUtils.getEnvelope(partshape=solid, depthparams=dep_par) # Produces .Shape
cont = True
except Exception as eee:
PathLog.error(str(eee))
if cont is True:
stckWst = JOB.Stock.Shape.cut(envBB)
if obj.BoundaryAdjustment > 0.0:
cmpndFS = Part.makeCompound(faceShapes)
baBB = PathUtils.getEnvelope(partshape=cmpndFS, depthparams=self.depthParams) # Produces .Shape
adjStckWst = stckWst.cut(baBB)
else:
adjStckWst = stckWst
fuseShapes.append(adjStckWst)
else:
PathLog.warning('Path transitions might not avoid the model. Verify paths.')
time.sleep(0.3)
else:
# If boundbox is Job.Stock, add hidden pad under stock as base plate
toolDiam = self.cutter.getDiameter()
zMin = JOB.Stock.Shape.BoundBox.ZMin
xMin = JOB.Stock.Shape.BoundBox.XMin - toolDiam
yMin = JOB.Stock.Shape.BoundBox.YMin - toolDiam
bL = JOB.Stock.Shape.BoundBox.XLength + (2 * toolDiam)
bW = JOB.Stock.Shape.BoundBox.YLength + (2 * toolDiam)
bH = 1.0
crnr = FreeCAD.Vector(xMin, yMin, zMin - 1.0)
B = Part.makeBox(bL, bW, bH, crnr, FreeCAD.Vector(0, 0, 1))
fuseShapes.append(B)
if voidShapes is not False:
voidComp = Part.makeCompound(voidShapes)
voidEnv = PathUtils.getEnvelope(partshape=voidComp, depthparams=self.depthParams) # Produces .Shape
fuseShapes.append(voidEnv)
f0 = fuseShapes.pop(0)
if len(fuseShapes) > 0:
fused = f0.fuse(fuseShapes)
else:
fused = f0
if self.showDebugObjects is True:
T = FreeCAD.ActiveDocument.addObject('Part::Feature', 'safeSTLShape')
T.Shape = fused
T.purgeTouched()
self.tempGroup.addObject(T)
# Extract mesh from fusion
meshFuse = MeshPart.meshFromShape(Shape=fused,
LinearDeflection=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]
q = f.Points[1]
r = f.Points[2]
t = ocl.Triangle(ocl.Point(p[0], p[1], p[2]),
ocl.Point(q[0], q[1], q[2]),
ocl.Point(r[0], r[1], r[2]))
stl.addTriangle(t)
self.safeSTLs[mdlIdx] = stl
def _processCutAreas(self, JOB, obj, mdlIdx, FCS, VDS):
'''_processCutAreas(JOB, obj, mdlIdx, FCS, VDS)...
This method applies any avoided faces or regions to the selected faces.
It then calls the correct scan method depending on the Algorithm and ScanType properties.'''
PathLog.debug('_processCutAreas()')
final = list()
base = JOB.Model.Group[mdlIdx]
# Process faces Collectively or Individually
if obj.HandleMultipleFeatures == 'Collectively':
if FCS is True:
COMP = False
else:
ADD = Part.makeCompound(FCS)
if VDS is not False:
DEL = Part.makeCompound(VDS)
COMP = ADD.cut(DEL)
else:
COMP = ADD
if obj.Algorithm == 'OCL Waterline':
final.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid}))
final.extend(self._waterlineOp(JOB, obj, mdlIdx, COMP)) # independent method set for Waterline
elif obj.ScanType == 'Planar':
final.extend(self._processPlanarOp(JOB, obj, mdlIdx, COMP, 0))
elif obj.ScanType == 'Rotational':
final.extend(self._processRotationalOp(obj, base, COMP))
elif obj.HandleMultipleFeatures == 'Individually':
for fsi in range(0, len(FCS)):
fShp = FCS[fsi]
# self.deleteOpVariables(all=False)
self.resetOpVariables(all=False)
if fShp is True:
COMP = False
else:
ADD = Part.makeCompound([fShp])
if VDS is not False:
DEL = Part.makeCompound(VDS)
COMP = ADD.cut(DEL)
else:
COMP = ADD
if obj.Algorithm == 'OCL Waterline':
final.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid}))
final.extend(self._waterlineOp(JOB, obj, mdlIdx, COMP)) # independent method set for Waterline
elif obj.ScanType == 'Planar':
final.extend(self._processPlanarOp(JOB, obj, mdlIdx, COMP, fsi))
elif obj.ScanType == 'Rotational':
final.extend(self._processRotationalOp(JOB, obj, mdlIdx, COMP))
COMP = None
# Eif
return final
# Methods for creating path geometry
def _processPlanarOp(self, JOB, obj, mdlIdx, cmpdShp, fsi):
'''_processPlanarOp(JOB, obj, mdlIdx, cmpdShp)...
This method compiles the main components for the procedural portion of a planar operation (non-rotational).
It creates the OCL PathDropCutter objects: model and safeTravel.
It makes the necessary facial geometries for the actual cut area.
It calls the correct Single or Multi-pass method as needed.
It returns the gcode for the operation. '''
PathLog.debug('_processPlanarOp()')
final = list()
SCANDATA = list()
# base = JOB.Model.Group[mdlIdx]
# Compute number and size of stepdowns, and final depth
if obj.LayerMode == 'Single-pass':
depthparams = [obj.FinalDepth.Value]
elif obj.LayerMode == 'Multi-pass':
depthparams = [i for i in self.depthParams]
lenDP = len(depthparams)
# Prepare PathDropCutter objects with STL data
pdc = self._planarGetPDC(self.modelSTLs[mdlIdx], depthparams[lenDP - 1], obj.SampleInterval.Value)
safePDC = self._planarGetPDC(self.safeSTLs[mdlIdx],
depthparams[lenDP - 1], obj.SampleInterval.Value, useSafeCutter=False)
profScan = list()
if obj.ProfileEdges != 'None':
prflShp = self.profileShapes[mdlIdx][fsi]
if prflShp is False:
PathLog.error('No profile shape is False.')
return list()
if self.showDebugObjects is True:
P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpNewProfileShape')
P.Shape = prflShp
# P.recompute()
P.purgeTouched()
self.tempGroup.addObject(P)
# get offset path geometry and perform OCL scan with that geometry
pathOffsetGeom = self._planarMakeProfileGeom(obj, prflShp)
if pathOffsetGeom is False:
PathLog.error('No profile geometry returned.')
return list()
profScan = [self._planarPerformOclScan(obj, pdc, pathOffsetGeom, offsetPoints=True)]
geoScan = list()
if obj.ProfileEdges != 'Only':
if self.showDebugObjects is True:
F = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCutArea')
F.Shape = cmpdShp
# F.recompute()
F.purgeTouched()
self.tempGroup.addObject(F)
# get internal path geometry and perform OCL scan with that geometry
pathGeom = self._planarMakePathGeom(obj, cmpdShp)
if pathGeom is False:
PathLog.error('No path geometry returned.')
return list()
geoScan = self._planarPerformOclScan(obj, pdc, pathGeom, offsetPoints=False)
if obj.ProfileEdges == 'Only': # ['None', 'Only', 'First', 'Last']
SCANDATA.extend(profScan)
if obj.ProfileEdges == 'None':
SCANDATA.extend(geoScan)
if obj.ProfileEdges == 'First':
SCANDATA.extend(profScan)
SCANDATA.extend(geoScan)
if obj.ProfileEdges == 'Last':
SCANDATA.extend(geoScan)
SCANDATA.extend(profScan)
# Apply depth offset
if obj.DepthOffset.Value != 0.0:
self._planarApplyDepthOffset(SCANDATA, obj.DepthOffset.Value)
if len(SCANDATA) == 0:
PathLog.error('No scan data to convert to Gcode.')
return list()
# If cut pattern is `Circular`, there are zero(almost zero) straight lines to optimize
# Store initial `OptimizeLinearPaths` value for later restoration
self.preOLP = obj.OptimizeLinearPaths
if obj.CutPattern in ['Circular', 'CircularZigZag']:
obj.OptimizeLinearPaths = False
# Process OCL scan data
if obj.LayerMode == 'Single-pass':
final.extend(self._planarDropCutSingle(JOB, obj, pdc, safePDC, depthparams, SCANDATA))
elif obj.LayerMode == 'Multi-pass':
final.extend(self._planarDropCutMulti(JOB, obj, pdc, safePDC, depthparams, SCANDATA))
# If cut pattern is `Circular`, restore initial OLP value
if obj.CutPattern in ['Circular', 'CircularZigZag']:
obj.OptimizeLinearPaths = self.preOLP
# Raise to safe height between individual faces.
if obj.HandleMultipleFeatures == 'Individually':
final.insert(0, Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid}))
return final
def _planarMakePathGeom(self, obj, faceShp):
'''_planarMakePathGeom(obj, faceShp)...
Creates the line/arc cut pattern geometry and returns the intersection with the received faceShp.
The resulting intersecting line/arc geometries are then converted to lines or arcs for OCL.'''
PathLog.debug('_planarMakePathGeom()')
GeoSet = list()
# Apply drop cutter extra offset and set the max and min XY area of the operation
xmin = faceShp.BoundBox.XMin
xmax = faceShp.BoundBox.XMax
ymin = faceShp.BoundBox.YMin
ymax = faceShp.BoundBox.YMax
zmin = faceShp.BoundBox.ZMin
zmax = faceShp.BoundBox.ZMax
# Compute weighted center of mass of all faces combined
fCnt = 0
totArea = 0.0
zeroCOM = FreeCAD.Vector(0.0, 0.0, 0.0)
for F in faceShp.Faces:
comF = F.CenterOfMass
areaF = F.Area
totArea += areaF
fCnt += 1
zeroCOM = zeroCOM.add(FreeCAD.Vector(comF.x, comF.y, 0.0).multiply(areaF))
if fCnt == 0:
PathLog.error(translate('PathSurface', 'Cannot calculate the Center Of Mass. Using Center of Boundbox.'))
zeroCOM = FreeCAD.Vector((xmin + xmax) / 2.0, (ymin + ymax) / 2.0, 0.0)
else:
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 _planarMakeProfileGeom(self, obj, subShp):
PathLog.debug('_planarMakeProfileGeom()')
offsetLists = list()
dist = obj.SampleInterval.Value / 5.0
defl = obj.SampleInterval.Value / 5.0
# Reference https://forum.freecadweb.org/viewtopic.php?t=28861#p234939
for fc in subShp.Faces:
# Reverse order of wires in each face - inside to outside
for w in range(len(fc.Wires) - 1, -1, -1):
W = fc.Wires[w]
PNTS = W.discretize(Distance=dist)
# PNTS = W.discretize(Deflection=defl)
if self.CutClimb is True:
PNTS.reverse()
offsetLists.append(PNTS)
return offsetLists
def _planarPerformOclScan(self, obj, pdc, pathGeom, offsetPoints=False):
'''_planarPerformOclScan(obj, pdc, pathGeom, offsetPoints=False)...
Switching fuction for calling the appropriate path-geometry to OCL points conversion fucntion
for the various cut patterns.'''
PathLog.debug('_planarPerformOclScan()')
SCANS = list()
if offsetPoints is True:
PNTSET = self._pathGeomToOffsetPointSet(obj, pathGeom)
for D in PNTSET:
stpOvr = list()
ofst = list()
for I in D:
if I == 'BRK':
stpOvr.append(ofst)
stpOvr.append(I)
ofst = list()
else:
# D format is ((p1, p2), (p3, p4))
(A, B) = I
ofst.extend(self._planarDropCutScan(pdc, A, B))
if len(ofst) > 0:
stpOvr.append(ofst)
SCANS.extend(stpOvr)
elif obj.CutPattern == 'Line':
stpOvr = list()
PNTSET = self._pathGeomToLinesPointSet(obj, pathGeom)
for D in PNTSET:
for I in D:
if I == 'BRK':
stpOvr.append(I)
else:
# D format is ((p1, p2), (p3, p4))
(A, B) = I
stpOvr.append(self._planarDropCutScan(pdc, A, B))
SCANS.append(stpOvr)
stpOvr = list()
elif obj.CutPattern == 'ZigZag':
stpOvr = list()
PNTSET = self._pathGeomToZigzagPointSet(obj, pathGeom)
for (dirFlg, LNS) in PNTSET:
for SEG in LNS:
if SEG == 'BRK':
stpOvr.append(SEG)
else:
# D format is ((p1, p2), (p3, p4))
(A, B) = SEG
stpOvr.append(self._planarDropCutScan(pdc, A, B))
SCANS.append(stpOvr)
stpOvr = list()
elif obj.CutPattern in ['Circular', 'CircularZigZag']:
# PNTSET is list, by stepover.
# Each stepover is a list containing arc/loop descriptions, (sp, ep, cp)
PNTSET = self._pathGeomToArcPointSet(obj, pathGeom)
for so in range(0, len(PNTSET)):
stpOvr = list()
erFlg = False
(aTyp, dirFlg, ARCS) = PNTSET[so]
if dirFlg == 1: # 1
cMode = True
else:
cMode = False
for a in range(0, len(ARCS)):
Arc = ARCS[a]
if Arc == 'BRK':
stpOvr.append('BRK')
else:
scan = self._planarCircularDropCutScan(pdc, Arc, cMode)
if scan is False:
erFlg = True
else:
if aTyp == 'L':
scan.append(FreeCAD.Vector(scan[0].x, scan[0].y, scan[0].z))
stpOvr.append(scan)
if erFlg is False:
SCANS.append(stpOvr)
return SCANS
def _pathGeomToOffsetPointSet(self, obj, compGeoShp):
'''_pathGeomToOffsetPointSet(obj, compGeoShp)...
Convert a compound set of 3D profile segmented wires to 2D segments, applying linear optimization.'''
PathLog.debug('_pathGeomToOffsetPointSet()')
LINES = list()
optimize = obj.OptimizeLinearPaths
ofstCnt = len(compGeoShp)
# Cycle through offeset loops
for ei in range(0, ofstCnt):
OS = compGeoShp[ei]
lenOS = len(OS)
if ei > 0:
LINES.append('BRK')
fp = FreeCAD.Vector(OS[0].x, OS[0].y, OS[0].z)
OS.append(fp)
# Cycle through points in each loop
prev = OS[0]
pnt = OS[1]
for v in range(1, lenOS):
nxt = OS[v + 1]
if optimize is True:
iPOL = self.isPointOnLine(prev, nxt, pnt)
if iPOL is True:
pnt = nxt
else:
tup = ((prev.x, prev.y), (pnt.x, pnt.y))
LINES.append(tup)
prev = pnt
pnt = nxt
else:
tup = ((prev.x, prev.y), (pnt.x, pnt.y))
LINES.append(tup)
prev = pnt
pnt = nxt
if iPOL is True:
tup = ((prev.x, prev.y), (pnt.x, pnt.y))
LINES.append(tup)
# Efor
return [LINES]
def _pathGeomToLinesPointSet(self, obj, compGeoShp):
'''_pathGeomToLinesPointSet(obj, compGeoShp)...
Convert a compound set of sequential line segments to directionally-oriented collinear groupings.'''
PathLog.debug('_pathGeomToLinesPointSet()')
# Extract intersection line segments for return value as list()
LINES = list()
inLine = list()
chkGap = False
lnCnt = 0
ec = len(compGeoShp.Edges)
cutClimb = self.CutClimb
toolDiam = 2.0 * self.radius
cpa = obj.CutPatternAngle
edg0 = compGeoShp.Edges[0]
p1 = (edg0.Vertexes[0].X, edg0.Vertexes[0].Y)
p2 = (edg0.Vertexes[1].X, edg0.Vertexes[1].Y)
if cutClimb is True:
tup = (p2, p1)
lst = FreeCAD.Vector(p1[0], p1[1], 0.0)
else:
tup = (p1, p2)
lst = FreeCAD.Vector(p2[0], p2[1], 0.0)
inLine.append(tup)
sp = FreeCAD.Vector(p1[0], p1[1], 0.0) # start point
for ei in range(1, ec):
chkGap = False
edg = compGeoShp.Edges[ei] # Get edge for vertexes
v1 = (edg.Vertexes[0].X, edg.Vertexes[0].Y) # vertex 0
v2 = (edg.Vertexes[1].X, edg.Vertexes[1].Y) # vertex 1
ep = FreeCAD.Vector(v2[0], v2[1], 0.0) # end point
cp = FreeCAD.Vector(v1[0], v1[1], 0.0) # check point (first / middle point)
iC = self.isPointOnLine(sp, ep, cp)
if iC is True:
inLine.append('BRK')
chkGap = True
else:
if cutClimb is True:
inLine.reverse()
LINES.append(inLine) # Save inLine segments
lnCnt += 1
inLine = list() # reset collinear container
if cutClimb is True:
sp = cp # FreeCAD.Vector(v1[0], v1[1], 0.0)
else:
sp = ep
if cutClimb is True:
tup = (v2, v1)
if chkGap is True:
gap = abs(toolDiam - lst.sub(ep).Length)
lst = cp
else:
tup = (v1, v2)
if chkGap is True:
gap = abs(toolDiam - lst.sub(cp).Length)
lst = ep
if chkGap is True:
if gap < obj.GapThreshold.Value:
b = inLine.pop() # pop off 'BRK' marker
(vA, vB) = inLine.pop() # pop off previous line segment for combining with current
tup = (vA, tup[1])
self.closedGap = True
else:
# PathLog.debug('---- Gap: {} mm'.format(gap))
gap = round(gap, 6)
if gap < self.gaps[0]:
self.gaps.insert(0, gap)
self.gaps.pop()
inLine.append(tup)
# Efor
lnCnt += 1
if cutClimb is True:
inLine.reverse()
LINES.append(inLine) # Save inLine segments
# Handle last inLine set, reversing it.
if obj.CutPatternReversed is True:
if cpa != 0.0 and cpa % 90.0 == 0.0:
F = LINES.pop(0)
rev = list()
for iL in F:
if iL == 'BRK':
rev.append(iL)
else:
(p1, p2) = iL
rev.append((p2, p1))
rev.reverse()
LINES.insert(0, rev)
isEven = lnCnt % 2
if isEven == 0:
PathLog.debug('Line count is ODD.')
else:
PathLog.debug('Line count is even.')
return LINES
def _pathGeomToZigzagPointSet(self, obj, compGeoShp):
'''_pathGeomToZigzagPointSet(obj, compGeoShp)...
Convert a compound set of sequential line segments to directionally-oriented collinear groupings
with a ZigZag directional indicator included for each collinear group.'''
PathLog.debug('_pathGeomToZigzagPointSet()')
# Extract intersection line segments for return value as list()
LINES = list()
inLine = list()
lnCnt = 0
chkGap = False
ec = len(compGeoShp.Edges)
toolDiam = 2.0 * self.radius
if self.CutClimb is True:
dirFlg = -1
else:
dirFlg = 1
edg0 = compGeoShp.Edges[0]
p1 = (edg0.Vertexes[0].X, edg0.Vertexes[0].Y)
p2 = (edg0.Vertexes[1].X, edg0.Vertexes[1].Y)
if dirFlg == 1:
tup = (p1, p2)
lst = FreeCAD.Vector(p2[0], p2[1], 0.0)
sp = FreeCAD.Vector(p1[0], p1[1], 0.0) # start point
else:
tup = (p2, p1)
lst = FreeCAD.Vector(p1[0], p1[1], 0.0)
sp = FreeCAD.Vector(p2[0], p2[1], 0.0) # start point
inLine.append(tup)
otr = lst
for ei in range(1, ec):
edg = compGeoShp.Edges[ei]
v1 = (edg.Vertexes[0].X, edg.Vertexes[0].Y)
v2 = (edg.Vertexes[1].X, edg.Vertexes[1].Y)
cp = FreeCAD.Vector(v1[0], v1[1], 0.0) # check point (start point of segment)
ep = FreeCAD.Vector(v2[0], v2[1], 0.0) # end point
iC = self.isPointOnLine(sp, ep, cp)
if iC is True:
inLine.append('BRK')
chkGap = True
gap = abs(toolDiam - lst.sub(cp).Length)
else:
chkGap = False
if dirFlg == -1:
inLine.reverse()
LINES.append((dirFlg, inLine))
lnCnt += 1
dirFlg = -1 * dirFlg # Change zig to zag
inLine = list() # reset collinear container
sp = cp # FreeCAD.Vector(v1[0], v1[1], 0.0)
otr = ep
lst = ep
if dirFlg == 1:
tup = (v1, v2)
else:
tup = (v2, v1)
if chkGap is True:
if gap < obj.GapThreshold.Value:
b = inLine.pop() # pop off 'BRK' marker
(vA, vB) = inLine.pop() # pop off previous line segment for combining with current
if dirFlg == 1:
tup = (vA, tup[1])
else:
#tup = (vA, tup[1])
#tup = (tup[1], vA)
tup = (tup[0], vB)
self.closedGap = True
else:
gap = round(gap, 6)
if gap < self.gaps[0]:
self.gaps.insert(0, gap)
self.gaps.pop()
inLine.append(tup)
# Efor
lnCnt += 1
# Fix directional issue with LAST line when line count is even
isEven = lnCnt % 2
if isEven == 0: # Changed to != with 90 degree CutPatternAngle
PathLog.debug('Line count is even.')
else:
PathLog.debug('Line count is ODD.')
dirFlg = -1 * dirFlg
if obj.CutPatternReversed is False:
if self.CutClimb is True:
dirFlg = -1 * dirFlg
if obj.CutPatternReversed is True:
dirFlg = -1 * dirFlg
# Handle last inLine list
if dirFlg == 1:
rev = list()
for iL in inLine:
if iL == 'BRK':
rev.append(iL)
else:
(p1, p2) = iL
rev.append((p2, p1))
if obj.CutPatternReversed is False:
rev.reverse()
else:
rev2 = list()
for iL in rev:
if iL == 'BRK':
rev2.append(iL)
else:
(p1, p2) = iL
rev2.append((p2, p1))
rev2.reverse()
rev = rev2
LINES.append((dirFlg, rev))
else:
LINES.append((dirFlg, inLine))
return LINES
def _pathGeomToArcPointSet(self, obj, compGeoShp):
'''_pathGeomToArcPointSet(obj, compGeoShp)...
Convert a compound set of arcs/circles to a set of directionally-oriented arc end points
and the corresponding center point.'''
# Extract intersection line segments for return value as list()
PathLog.debug('_pathGeomToArcPointSet()')
ARCS = list()
stpOvrEI = list()
segEI = list()
isSame = False
sameRad = None
COM = self.tmpCOM
toolDiam = 2.0 * self.radius
ec = len(compGeoShp.Edges)
def gapDist(sp, ep):
X = (ep[0] - sp[0])**2
Y = (ep[1] - sp[1])**2
Z = (ep[2] - sp[2])**2
# return math.sqrt(X + Y + Z)
return math.sqrt(X + Y) # the 'z' value is zero in both points
# Separate arc data into Loops and Arcs
for ei in range(0, ec):
edg = compGeoShp.Edges[ei]
if edg.Closed is True:
stpOvrEI.append(('L', ei, False))
else:
if isSame is False:
segEI.append(ei)
isSame = True
pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0)
sameRad = pnt.sub(COM).Length
else:
# Check if arc is co-radial to current SEGS
pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0)
if abs(sameRad - pnt.sub(COM).Length) > 0.00001:
isSame = False
if isSame is True:
segEI.append(ei)
else:
# Move co-radial arc segments
stpOvrEI.append(['A', segEI, False])
# Start new list of arc segments
segEI = [ei]
isSame = True
pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0)
sameRad = pnt.sub(COM).Length
# Process trailing `segEI` data, if available
if isSame is True:
stpOvrEI.append(['A', segEI, False])
# Identify adjacent arcs with y=0 start/end points that connect
for so in range(0, len(stpOvrEI)):
SO = stpOvrEI[so]
if SO[0] == 'A':
startOnAxis = list()
endOnAxis = list()
EI = SO[1] # list of corresponding compGeoShp.Edges indexes
# Identify startOnAxis and endOnAxis arcs
for i in range(0, len(EI)):
ei = EI[i] # edge index
E = compGeoShp.Edges[ei] # edge object
if abs(COM.y - E.Vertexes[0].Y) < 0.00001:
startOnAxis.append((i, ei, E.Vertexes[0]))
elif abs(COM.y - E.Vertexes[1].Y) < 0.00001:
endOnAxis.append((i, ei, E.Vertexes[1]))
# Look for connections between startOnAxis and endOnAxis arcs. Consolidate data when connected
lenSOA = len(startOnAxis)
lenEOA = len(endOnAxis)
if lenSOA > 0 and lenEOA > 0:
delIdxs = list()
lstFindIdx = 0
for soa in range(0, lenSOA):
(iS, eiS, vS) = startOnAxis[soa]
for eoa in range(0, len(endOnAxis)):
(iE, eiE, vE) = endOnAxis[eoa]
dist = vE.X - vS.X
if abs(dist) < 0.00001: # They connect on axis at same radius
SO[2] = (eiE, eiS)
break
elif dist > 0:
break # stop searching
# Eif
# Eif
# Efor
# Construct arc data tuples for OCL
dirFlg = 1
# cutPat = obj.CutPattern
if self.CutClimb is False: # True yields Climb when set to Conventional
dirFlg = -1
# Cycle through stepOver data
for so in range(0, len(stpOvrEI)):
SO = stpOvrEI[so]
if SO[0] == 'L': # L = Loop/Ring/Circle
lei = SO[1] # loop Edges index
v1 = compGeoShp.Edges[lei].Vertexes[0]
space = obj.SampleInterval.Value / 2.0
p1 = FreeCAD.Vector(v1.X, v1.Y, v1.Z)
sp = (v1.X, v1.Y, 0.0)
rad = p1.sub(COM).Length
tolrncAng = math.asin(space/rad)
X = COM.x + (rad * math.cos(tolrncAng))
Y = v1.Y - space # rad * math.sin(tolrncAng)
sp = (v1.X, v1.Y, 0.0)
ep = (X, Y, 0.0)
cp = (COM.x, COM.y, 0.0)
if dirFlg == 1:
arc = (sp, ep, cp)
else:
arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction))
ARCS.append(('L', dirFlg, [arc]))
else: # SO[0] == 'A' A = Arc
PRTS = list()
EI = SO[1] # list of corresponding Edges indexes
CONN = SO[2] # list of corresponding connected edges tuples (iE, iS)
chkGap = False
lst = None
if CONN is not False:
(iE, iS) = CONN
v1 = compGeoShp.Edges[iE].Vertexes[0]
v2 = compGeoShp.Edges[iS].Vertexes[1]
sp = (v1.X, v1.Y, 0.0)
ep = (v2.X, v2.Y, 0.0)
cp = (COM.x, COM.y, 0.0)
if dirFlg == 1:
arc = (sp, ep, cp)
lst = ep
else:
arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction))
lst = sp
PRTS.append(arc)
# Pop connected edge index values from arc segments index list
iEi = EI.index(iE)
iSi = EI.index(iS)
EI.pop(iEi)
EI.pop(iSi)
if len(EI) > 0:
PRTS.append('BRK')
chkGap = True
cnt = 0
for ei in EI:
if cnt > 0:
PRTS.append('BRK')
chkGap = True
v1 = compGeoShp.Edges[ei].Vertexes[0]
v2 = compGeoShp.Edges[ei].Vertexes[1]
sp = (v1.X, v1.Y, 0.0)
ep = (v2.X, v2.Y, 0.0)
cp = (COM.x, COM.y, 0.0)
if dirFlg == 1:
arc = (sp, ep, cp)
if chkGap is True:
gap = abs(toolDiam - gapDist(lst, sp)) # abs(toolDiam - lst.sub(sp).Length)
lst = ep
else:
arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction))
if chkGap is True:
gap = abs(toolDiam - gapDist(lst, ep)) # abs(toolDiam - lst.sub(ep).Length)
lst = sp
if chkGap is True:
if gap < obj.GapThreshold.Value:
b = PRTS.pop() # pop off 'BRK' marker
(vA, vB, vC) = PRTS.pop() # pop off previous arc segment for combining with current
arc = (vA, arc[1], vC)
self.closedGap = True
else:
# PathLog.debug('---- Gap: {} mm'.format(gap))
gap = round(gap, 6)
if gap < self.gaps[0]:
self.gaps.insert(0, gap)
self.gaps.pop()
PRTS.append(arc)
cnt += 1
if dirFlg == -1:
PRTS.reverse()
ARCS.append(('A', dirFlg, PRTS))
# Eif
if obj.CutPattern == 'CircularZigZag':
dirFlg = -1 * dirFlg
# Efor
return ARCS
def _planarDropCutScan(self, pdc, A, B):
PNTS = list()
(x1, y1) = A
(x2, y2) = B
path = ocl.Path() # create an empty path object
p1 = ocl.Point(x1, y1, 0) # start-point of line
p2 = ocl.Point(x2, y2, 0) # end-point of line
lo = ocl.Line(p1, p2) # line-object
path.append(lo) # add the line to the path
pdc.setPath(path)
pdc.run() # run dropcutter algorithm on path
CLP = pdc.getCLPoints()
for p in CLP:
PNTS.append(FreeCAD.Vector(p.x, p.y, p.z))
return PNTS # pdc.getCLPoints()
def _planarCircularDropCutScan(self, pdc, Arc, cMode):
PNTS = list()
path = ocl.Path() # create an empty path object
(sp, ep, cp) = Arc
# process list of segment tuples (vect, vect)
path = ocl.Path() # create an empty path object
p1 = ocl.Point(sp[0], sp[1], 0) # start point of arc
p2 = ocl.Point(ep[0], ep[1], 0) # end point of arc
C = ocl.Point(cp[0], cp[1], 0) # center point of arc
ao = ocl.Arc(p1, p2, C, cMode) # arc object
path.append(ao) # add the arc to the path
pdc.setPath(path)
pdc.run() # run dropcutter algorithm on path
CLP = pdc.getCLPoints()
# Convert OCL object data to FreeCAD vectors
for p in CLP:
PNTS.append(FreeCAD.Vector(p.x, p.y, p.z))
return PNTS
# Main planar scan functions
def _planarDropCutSingle(self, JOB, obj, pdc, safePDC, depthparams, SCANDATA):
PathLog.debug('_planarDropCutSingle()')
GCODE = [Path.Command('N (Beginning of Single-pass layer.)', {})]
tolrnc = JOB.GeometryTolerance.Value
prevDepth = obj.SafeHeight.Value
lenDP = len(depthparams)
lenSCANDATA = len(SCANDATA)
gDIR = ['G3', 'G2']
if self.CutClimb is True:
gDIR = ['G2', 'G3']
# Set `ProfileEdges` specific trigger indexes
peIdx = lenSCANDATA # off by default
if obj.ProfileEdges == 'Only':
peIdx = -1
elif obj.ProfileEdges == 'First':
peIdx = 0
elif obj.ProfileEdges == 'Last':
peIdx = lenSCANDATA - 1
# Send cutter to x,y position of first point on first line
first = SCANDATA[0][0][0] # [step][item][point]
GCODE.append(Path.Command('G0', {'X': first.x, 'Y': first.y, 'F': self.horizRapid}))
# Cycle through step-over sections (line segments or arcs)
odd = True
lstStpEnd = None
prevDepth = obj.SafeHeight.Value # Not used for Single-pass
for so in range(0, lenSCANDATA):
cmds = list()
PRTS = SCANDATA[so]
lenPRTS = len(PRTS)
first = PRTS[0][0] # first point of arc/line stepover group
start = PRTS[0][0] # will change with each line/arc segment
last = None
cmds.append(Path.Command('N (Begin step {}.)'.format(so), {}))
if so > 0:
if obj.CutPattern == 'CircularZigZag':
if odd is True:
odd = False
else:
odd = True
minTrnsHght = self._getMinSafeTravelHeight(safePDC, lstStpEnd, first) # Check safe travel height against fullSTL
# cmds.append(Path.Command('N (Transition: last, first: {}, {}: minSTH: {})'.format(lstStpEnd, first, minTrnsHght), {}))
cmds.extend(self._stepTransitionCmds(obj, lstStpEnd, first, minTrnsHght, tolrnc))
# Override default `OptimizeLinearPaths` behavior to allow `ProfileEdges` optimization
if so == peIdx or peIdx == -1:
obj.OptimizeLinearPaths = self.preOLP
# Cycle through current step-over parts
for i in range(0, lenPRTS):
prt = PRTS[i]
lenPrt = len(prt)
if prt == 'BRK':
nxtStart = PRTS[i + 1][0]
minSTH = self._getMinSafeTravelHeight(safePDC, last, nxtStart) # Check safe travel height against fullSTL
cmds.append(Path.Command('N (Break)', {}))
cmds.extend(self._breakCmds(obj, last, nxtStart, minSTH, tolrnc))
else:
cmds.append(Path.Command('N (part {}.)'.format(i + 1), {}))
start = prt[0]
last = prt[lenPrt - 1]
if so == peIdx or peIdx == -1:
cmds.extend(self._planarSinglepassProcess(obj, prt))
elif obj.CutPattern in ['Circular', 'CircularZigZag'] and obj.CircularUseG2G3 is True and lenPrt > 2:
(rtnVal, gcode) = self._arcsToG2G3(prt, lenPrt, odd, gDIR, tolrnc)
if rtnVal is True:
cmds.extend(gcode)
else:
cmds.extend(self._planarSinglepassProcess(obj, prt))
else:
cmds.extend(self._planarSinglepassProcess(obj, prt))
cmds.append(Path.Command('N (End of step {}.)'.format(so), {}))
GCODE.extend(cmds) # save line commands
lstStpEnd = last
# Return `OptimizeLinearPaths` to disabled
if so == peIdx or peIdx == -1:
if obj.CutPattern in ['Circular', 'CircularZigZag']:
obj.OptimizeLinearPaths = False
# Efor
return GCODE
def _planarSinglepassProcess(self, obj, PNTS):
output = []
optimize = obj.OptimizeLinearPaths
lenPNTS = len(PNTS)
lstIdx = lenPNTS - 1
lop = None
onLine = False
# Initialize first three points
nxt = None
pnt = PNTS[0]
prev = FreeCAD.Vector(-442064564.6, 258539656553.27, 3538553425.847)
# Add temp end point
PNTS.append(FreeCAD.Vector(-4895747464.6, -25855763553.2, 35865763425))
# Begin processing ocl points list into gcode
for i in range(0, lenPNTS):
# Calculate next point for consideration with current point
nxt = PNTS[i + 1]
# Process point
if optimize is True:
iPOL = self.isPointOnLine(prev, nxt, pnt)
if iPOL is True:
onLine = True
else:
onLine = False
output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed}))
else:
output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed}))
# Rotate point data
if onLine is False:
prev = pnt
pnt = nxt
# Efor
temp = PNTS.pop() # Remove temp end point
return output
def _planarDropCutMulti(self, JOB, obj, pdc, safePDC, depthparams, SCANDATA):
GCODE = [Path.Command('N (Beginning of Multi-pass layers.)', {})]
tolrnc = JOB.GeometryTolerance.Value
lenDP = len(depthparams)
prevDepth = depthparams[0]
lenSCANDATA = len(SCANDATA)
gDIR = ['G3', 'G2']
if self.CutClimb is True:
gDIR = ['G2', 'G3']
# Set `ProfileEdges` specific trigger indexes
peIdx = lenSCANDATA # off by default
if obj.ProfileEdges == 'Only':
peIdx = -1
elif obj.ProfileEdges == 'First':
peIdx = 0
elif obj.ProfileEdges == 'Last':
peIdx = lenSCANDATA - 1
# Process each layer in depthparams
prvLyrFirst = None
prvLyrLast = None
actvLyrs = 0
for lyr in range(0, lenDP):
odd = True # ZigZag directional switch
lyrHasCmds = False
lstStpEnd = None
actvSteps = 0
LYR = list()
prvStpFirst = None
prvStpLast = None
lyrDep = depthparams[lyr]
# Cycle through step-over sections (line segments or arcs)
for so in range(0, len(SCANDATA)):
SO = SCANDATA[so]
lenSO = len(SO)
# Pre-process step-over parts for layer depth and holds
ADJPRTS = list()
LMAX = list()
soHasPnts = False
brkFlg = False
for i in range(0, lenSO):
prt = SO[i]
lenPrt = len(prt)
if prt == 'BRK':
if brkFlg is True:
ADJPRTS.append(prt)
LMAX.append(prt)
brkFlg = False
else:
(PTS, lMax) = self._planarMultipassPreProcess(obj, prt, prevDepth, lyrDep)
if len(PTS) > 0:
ADJPRTS.append(PTS)
soHasPnts = True
brkFlg = True
LMAX.append(lMax)
# Efor
lenAdjPrts = len(ADJPRTS)
# Process existing parts within current step over
prtsHasCmds = False
stepHasCmds = False
prtsCmds = list()
stpOvrCmds = list()
transCmds = list()
if soHasPnts is True:
first = ADJPRTS[0][0] # first point of arc/line stepover group
# Manage step over transition and CircularZigZag direction
if so > 0:
# Control ZigZag direction
if obj.CutPattern == 'CircularZigZag':
if odd is True:
odd = False
else:
odd = True
# Control step over transition
minTrnsHght = self._getMinSafeTravelHeight(safePDC, prvStpLast, first, minDep=None) # Check safe travel height against fullSTL
transCmds.append(Path.Command('N (--Step {} transition)'.format(so), {}))
transCmds.extend(self._stepTransitionCmds(obj, prvStpLast, first, minTrnsHght, tolrnc))
# Override default `OptimizeLinearPaths` behavior to allow `ProfileEdges` optimization
if so == peIdx or peIdx == -1:
obj.OptimizeLinearPaths = self.preOLP
# Cycle through current step-over parts
for i in range(0, lenAdjPrts):
prt = ADJPRTS[i]
lenPrt = len(prt)
if prt == 'BRK' and prtsHasCmds is True:
nxtStart = ADJPRTS[i + 1][0]
minSTH = self._getMinSafeTravelHeight(safePDC, last, nxtStart, minDep=None) # Check safe travel height against fullSTL
prtsCmds.append(Path.Command('N (--Break)', {}))
prtsCmds.extend(self._breakCmds(obj, last, nxtStart, minSTH, tolrnc))
else:
segCmds = False
prtsCmds.append(Path.Command('N (part {})'.format(i + 1), {}))
last = prt[lenPrt - 1]
if so == peIdx or peIdx == -1:
segCmds = self._planarSinglepassProcess(obj, prt)
elif obj.CutPattern in ['Circular', 'CircularZigZag'] and obj.CircularUseG2G3 is True and lenPrt > 2:
(rtnVal, gcode) = self._arcsToG2G3(prt, lenPrt, odd, gDIR, tolrnc)
if rtnVal is True:
segCmds = gcode
else:
segCmds = self._planarSinglepassProcess(obj, prt)
else:
segCmds = self._planarSinglepassProcess(obj, prt)
if segCmds is not False:
prtsCmds.extend(segCmds)
prtsHasCmds = True
prvStpLast = last
# Eif
# Efor
# Eif
# Return `OptimizeLinearPaths` to disabled
if so == peIdx or peIdx == -1:
if obj.CutPattern in ['Circular', 'CircularZigZag']:
obj.OptimizeLinearPaths = False
# Compile step over(prts) commands
if prtsHasCmds is True:
stepHasCmds = True
actvSteps += 1
prvStpFirst = first
stpOvrCmds.extend(transCmds)
stpOvrCmds.append(Path.Command('N (Begin step {}.)'.format(so), {}))
stpOvrCmds.append(Path.Command('G0', {'X': first.x, 'Y': first.y, 'F': self.horizRapid}))
stpOvrCmds.extend(prtsCmds)
stpOvrCmds.append(Path.Command('N (End of step {}.)'.format(so), {}))
# Layer transition at first active step over in current layer
if actvSteps == 1:
prvLyrFirst = first
LYR.append(Path.Command('N (Layer {} begins)'.format(lyr), {}))
if lyr > 0:
LYR.append(Path.Command('N (Layer transition)', {}))
LYR.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid}))
LYR.append(Path.Command('G0', {'X': first.x, 'Y': first.y, 'F': self.horizRapid}))
if stepHasCmds is True:
lyrHasCmds = True
LYR.extend(stpOvrCmds)
# Eif
# Close layer, saving commands, if any
if lyrHasCmds is True:
prvLyrLast = last
GCODE.extend(LYR) # save line commands
GCODE.append(Path.Command('N (End of layer {})'.format(lyr), {}))
# Set previous depth
prevDepth = lyrDep
# Efor
PathLog.debug('Multi-pass op has {} layers (step downs).'.format(lyr + 1))
return GCODE
def _planarMultipassPreProcess(self, obj, LN, prvDep, layDep):
ALL = list()
PTS = list()
brkFlg = False
optLinTrans = obj.OptimizeStepOverTransitions
safe = math.ceil(obj.SafeHeight.Value)
if optLinTrans is True:
for P in LN:
ALL.append(P)
# Handle layer depth AND hold points
if P.z <= layDep:
PTS.append(FreeCAD.Vector(P.x, P.y, layDep))
elif P.z > prvDep:
PTS.append(FreeCAD.Vector(P.x, P.y, safe))
else:
PTS.append(FreeCAD.Vector(P.x, P.y, P.z))
# Efor
else:
for P in LN:
ALL.append(P)
# Handle layer depth only
if P.z <= layDep:
PTS.append(FreeCAD.Vector(P.x, P.y, layDep))
else:
PTS.append(FreeCAD.Vector(P.x, P.y, P.z))
# Efor
if optLinTrans is True:
# Remove leading and trailing Hold Points
popList = list()
for i in range(0, len(PTS)): # identify leading string
if PTS[i].z == safe:
popList.append(i)
else:
break
popList.sort(reverse=True)
for p in popList: # Remove hold points
PTS.pop(p)
ALL.pop(p)
popList = list()
for i in range(len(PTS) - 1, -1, -1): # identify trailing string
if PTS[i].z == safe:
popList.append(i)
else:
break
popList.sort(reverse=True)
for p in popList: # Remove hold points
PTS.pop(p)
ALL.pop(p)
# Determine max Z height for remaining points on line
lMax = obj.FinalDepth.Value
if len(ALL) > 0:
lMax = ALL[0].z
for P in ALL:
if P.z > lMax:
lMax = P.z
return (PTS, lMax)
def _planarMultipassProcess(self, obj, PNTS, lMax):
output = list()
optimize = obj.OptimizeLinearPaths
safe = math.ceil(obj.SafeHeight.Value)
lenPNTS = len(PNTS)
lastPNTS = lenPNTS - 1
prcs = True
onHold = False
onLine = False
clrScnLn = lMax + 2.0
# Initialize first three points
nxt = None
pnt = PNTS[0]
prev = FreeCAD.Vector(-442064564.6, 258539656553.27, 3538553425.847)
# Add temp end point
PNTS.append(FreeCAD.Vector(-4895747464.6, -25855763553.2, 35865763425))
# Begin processing ocl points list into gcode
for i in range(0, lenPNTS):
prcs = True
nxt = PNTS[i + 1]
if pnt.z == safe:
prcs = False
if onHold is False:
onHold = True
output.append( Path.Command('N (Start hold)', {}) )
output.append( Path.Command('G0', {'Z': clrScnLn, 'F': self.vertRapid}) )
else:
if onHold is True:
onHold = False
output.append( Path.Command('N (End hold)', {}) )
output.append( Path.Command('G0', {'X': pnt.x, 'Y': pnt.y, 'F': self.horizRapid}) )
# Process point
if prcs is True:
if optimize is True:
iPOL = self.isPointOnLine(prev, nxt, pnt)
if iPOL is True:
onLine = True
else:
onLine = False
output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed}))
else:
output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed}))
# Rotate point data
if onLine is False:
prev = pnt
pnt = nxt
# Efor
temp = PNTS.pop() # Remove temp end point
return output
def _stepTransitionCmds(self, obj, lstPnt, first, minSTH, tolrnc):
cmds = list()
rtpd = False
horizGC = 'G0'
hSpeed = self.horizRapid
height = obj.SafeHeight.Value
if obj.CutPattern in ['Line', 'Circular']:
if obj.OptimizeStepOverTransitions is True:
height = minSTH + 2.0
# if obj.LayerMode == 'Multi-pass':
# rtpd = minSTH
elif obj.CutPattern in ['ZigZag', 'CircularZigZag']:
if obj.OptimizeStepOverTransitions is True:
zChng = first.z - lstPnt.z
# PathLog.debug('first.z: {}'.format(first.z))
# PathLog.debug('lstPnt.z: {}'.format(lstPnt.z))
# PathLog.debug('zChng: {}'.format(zChng))
# PathLog.debug('minSTH: {}'.format(minSTH))
if abs(zChng) < tolrnc: # transitions to same Z height
PathLog.debug('abs(zChng) < tolrnc')
if (minSTH - first.z) > tolrnc:
PathLog.debug('(minSTH - first.z) > tolrnc')
height = minSTH + 2.0
else:
PathLog.debug('ELSE (minSTH - first.z) > tolrnc')
horizGC = 'G1'
height = first.z
elif (minSTH + (2.0 * tolrnc)) >= max(first.z, lstPnt.z):
height = False # allow end of Zig to cut to beginning of Zag
# Create raise, shift, and optional lower commands
if height is not False:
cmds.append(Path.Command('G0', {'Z': height, 'F': self.vertRapid}))
cmds.append(Path.Command(horizGC, {'X': first.x, 'Y': first.y, 'F': hSpeed}))
if rtpd is not False: # ReturnToPreviousDepth
cmds.append(Path.Command('G0', {'Z': rtpd, 'F': self.vertRapid}))
return cmds
def _breakCmds(self, obj, lstPnt, first, minSTH, tolrnc):
cmds = list()
rtpd = False
horizGC = 'G0'
hSpeed = self.horizRapid
height = obj.SafeHeight.Value
if obj.CutPattern in ['Line', 'Circular']:
if obj.OptimizeStepOverTransitions is True:
height = minSTH + 2.0
elif obj.CutPattern in ['ZigZag', 'CircularZigZag']:
if obj.OptimizeStepOverTransitions is True:
zChng = first.z - lstPnt.z
if abs(zChng) < tolrnc: # transitions to same Z height
if (minSTH - first.z) > tolrnc:
height = minSTH + 2.0
else:
height = first.z + 2.0 # first.z
cmds.append(Path.Command('G0', {'Z': height, 'F': self.vertRapid}))
cmds.append(Path.Command(horizGC, {'X': first.x, 'Y': first.y, 'F': hSpeed}))
if rtpd is not False: # ReturnToPreviousDepth
cmds.append(Path.Command('G0', {'Z': rtpd, 'F': self.vertRapid}))
return cmds
def _arcsToG2G3(self, LN, numPts, odd, gDIR, tolrnc):
cmds = list()
strtPnt = LN[0]
endPnt = LN[numPts - 1]
strtHght = strtPnt.z
coPlanar = True
isCircle = False
inrPnt = None
gdi = 0
if odd is True:
gdi = 1
# Test if pnt set is circle
if abs(strtPnt.x - endPnt.x) < tolrnc:
if abs(strtPnt.y - endPnt.y) < tolrnc:
if abs(strtPnt.z - endPnt.z) < tolrnc:
isCircle = True
isCircle = False
if isCircle is True:
# convert LN to G2/G3 arc, consolidating GCode
# https://wiki.shapeoko.com/index.php/G-Code#G2_-_clockwise_arc
# https://www.cnccookbook.com/cnc-g-code-arc-circle-g02-g03/
# Dividing circle into two arcs allows for G2/G3 on inclined surfaces
# ijk = self.tmpCOM - strtPnt # vector from start to center
ijk = self.tmpCOM - strtPnt # vector from start to center
xyz = self.tmpCOM.add(ijk) # end point
cmds.append(Path.Command('G1', {'X': strtPnt.x, 'Y': strtPnt.y, 'Z': strtPnt.z, 'F': self.horizFeed}))
cmds.append(Path.Command(gDIR[gdi], {'X': xyz.x, 'Y': xyz.y, 'Z': xyz.z,
'I': ijk.x, 'J': ijk.y, 'K': ijk.z, # leave same xyz.z height
'F': self.horizFeed}))
cmds.append(Path.Command('G1', {'X': xyz.x, 'Y': xyz.y, 'Z': xyz.z, 'F': self.horizFeed}))
ijk = self.tmpCOM - xyz # vector from start to center
rst = strtPnt # end point
cmds.append(Path.Command(gDIR[gdi], {'X': rst.x, 'Y': rst.y, 'Z': rst.z,
'I': ijk.x, 'J': ijk.y, 'K': ijk.z, # leave same xyz.z height
'F': self.horizFeed}))
cmds.append(Path.Command('G1', {'X': strtPnt.x, 'Y': strtPnt.y, 'Z': strtPnt.z, 'F': self.horizFeed}))
else:
for pt in LN:
if abs(pt.z - strtHght) > tolrnc: # test for horizontal coplanar
coPlanar = False
break
if coPlanar is True:
# ijk = self.tmpCOM - strtPnt
ijk = self.tmpCOM.sub(strtPnt) # vector from start to center
xyz = endPnt
cmds.append(Path.Command('G1', {'X': strtPnt.x, 'Y': strtPnt.y, 'Z': strtPnt.z, 'F': self.horizFeed}))
cmds.append(Path.Command(gDIR[gdi], {'X': xyz.x, 'Y': xyz.y, 'Z': xyz.z,
'I': ijk.x, 'J': ijk.y, 'K': ijk.z, # leave same xyz.z height
'F': self.horizFeed}))
cmds.append(Path.Command('G1', {'X': endPnt.x, 'Y': endPnt.y, 'Z': endPnt.z, 'F': self.horizFeed}))
return (coPlanar, cmds)
def _planarApplyDepthOffset(self, SCANDATA, DepthOffset):
PathLog.debug('Applying DepthOffset value: {}'.format(DepthOffset))
lenScans = len(SCANDATA)
for s in range(0, lenScans):
SO = SCANDATA[s] # StepOver
numParts = len(SO)
for prt in range(0, numParts):
PRT = SO[prt]
if PRT != 'BRK':
numPts = len(PRT)
for pt in range(0, numPts):
SCANDATA[s][prt][pt].z += DepthOffset
def _planarGetPDC(self, stl, finalDep, SampleInterval, useSafeCutter=False):
pdc = ocl.PathDropCutter() # create a pdc [PathDropCutter] object
pdc.setSTL(stl) # add stl model
if useSafeCutter is True:
pdc.setCutter(self.safeCutter) # add safeCutter
else:
pdc.setCutter(self.cutter) # add cutter
pdc.setZ(finalDep) # set minimumZ (final / target depth value)
pdc.setSampling(SampleInterval) # set sampling size
return pdc
# Main rotational scan functions
def _processRotationalOp(self, JOB, obj, mdlIdx, compoundFaces=None):
PathLog.debug('_processRotationalOp(self, obj, mdlIdx, compoundFaces=None)')
initIdx = 0.0
final = list()
JOB = PathUtils.findParentJob(obj)
base = JOB.Model.Group[mdlIdx]
bb = self.boundBoxes[mdlIdx]
stl = self.modelSTLs[mdlIdx]
# Rotate model to initial index
initIdx = obj.CutterTilt + obj.StartIndex
if initIdx != 0.0:
self.basePlacement = FreeCAD.ActiveDocument.getObject(base.Name).Placement
if obj.RotationAxis == 'X':
base.Placement = FreeCAD.Placement(FreeCAD.Vector(0.0, 0.0, 0.0), FreeCAD.Rotation(FreeCAD.Vector(1.0, 0.0, 0.0), initIdx))
else:
base.Placement = FreeCAD.Placement(FreeCAD.Vector(0.0, 0.0, 0.0), FreeCAD.Rotation(FreeCAD.Vector(0.0, 1.0, 0.0), initIdx))
# Prepare global holdpoint container
if self.holdPoint is None:
self.holdPoint = ocl.Point(float("inf"), float("inf"), float("inf"))
if self.layerEndPnt is None:
self.layerEndPnt = ocl.Point(float("inf"), float("inf"), float("inf"))
# Avoid division by zero in rotational scan calculations
if obj.FinalDepth.Value <= 0.0:
zero = obj.SampleInterval.Value # 0.00001
self.FinalDepth = zero
obj.FinalDepth.Value = 0.0
else:
self.FinalDepth = obj.FinalDepth.Value
# Determine boundbox radius based upon xzy limits data
if math.fabs(bb.ZMin) > math.fabs(bb.ZMax):
vlim = bb.ZMin
else:
vlim = bb.ZMax
if obj.RotationAxis == 'X':
# Rotation is around X-axis, cutter moves along same axis
if math.fabs(bb.YMin) > math.fabs(bb.YMax):
hlim = bb.YMin
else:
hlim = bb.YMax
else:
# Rotation is around Y-axis, cutter moves along same axis
if math.fabs(bb.XMin) > math.fabs(bb.XMax):
hlim = bb.XMin
else:
hlim = bb.XMax
# Compute max radius of stock, as it rotates, and rotational clearance & safe heights
self.bbRadius = math.sqrt(hlim**2 + vlim**2)
self.clearHeight = self.bbRadius + JOB.SetupSheet.ClearanceHeightOffset.Value
self.safeHeight = self.bbRadius + JOB.SetupSheet.ClearanceHeightOffset.Value
final = self._rotationalDropCutterOp(obj, stl, bb)
return final
def _rotationalDropCutterOp(self, obj, stl, bb):
self.resetTolerance = 0.0000001 # degrees
self.layerEndzMax = 0.0
commands = []
scanLines = []
advances = []
iSTG = []
rSTG = []
rings = []
lCnt = 0
rNum = 0
# stepDeg = 1.1
# layCircum = 1.1
# begIdx = 0.0
# endIdx = 0.0
# arc = 0.0
# sumAdv = 0.0
bbRad = self.bbRadius
def invertAdvances(advances):
idxs = [1.1]
for adv in advances:
idxs.append(-1 * adv)
idxs.pop(0)
return idxs
def linesToPointRings(scanLines):
rngs = []
numPnts = len(scanLines[0]) # Number of points per line along axis, at obj.SampleInterval.Value spacing
for line in scanLines: # extract circular set(ring) of points from scan lines
if len(line) != numPnts:
PathLog.debug('Error: line lengths not equal')
return rngs
for num in range(0, numPnts):
rngs.append([1.1]) # Initiate new ring
for line in scanLines: # extract circular set(ring) of points from scan lines
rngs[num].append(line[num])
rngs[num].pop(0)
return rngs
def indexAdvances(arc, stepDeg):
indexes = [0.0]
numSteps = int(math.floor(arc / stepDeg))
for ns in range(0, numSteps):
indexes.append(stepDeg)
travel = sum(indexes)
if arc == 360.0:
indexes.insert(0, 0.0)
else:
indexes.append(arc - travel)
return indexes
# Compute number and size of stepdowns, and final depth
if obj.LayerMode == 'Single-pass':
depthparams = [self.FinalDepth]
else:
dep_par = PathUtils.depth_params(self.clearHeight, self.safeHeight, self.bbRadius, obj.StepDown.Value, 0.0, self.FinalDepth)
depthparams = [i for i in dep_par]
prevDepth = depthparams[0]
lenDP = len(depthparams)
# Set drop cutter extra offset
cdeoX = obj.DropCutterExtraOffset.x
cdeoY = obj.DropCutterExtraOffset.y
# Set updated bound box values and redefine the new min/mas XY area of the operation based on greatest point radius of model
bb.ZMin = -1 * bbRad
bb.ZMax = bbRad
if obj.RotationAxis == 'X':
bb.YMin = -1 * bbRad
bb.YMax = bbRad
ymin = 0.0
ymax = 0.0
xmin = bb.XMin - cdeoX
xmax = bb.XMax + cdeoX
else:
bb.XMin = -1 * bbRad
bb.XMax = bbRad
ymin = bb.YMin - cdeoY
ymax = bb.YMax + cdeoY
xmin = 0.0
xmax = 0.0
# Calculate arc
begIdx = obj.StartIndex
endIdx = obj.StopIndex
if endIdx < begIdx:
begIdx -= 360.0
arc = endIdx - begIdx
# Begin gcode operation with raising cutter to safe height
commands.append(Path.Command('G0', {'Z': self.safeHeight, 'F': self.vertRapid}))
# Complete rotational scans at layer and translate into gcode
for layDep in depthparams:
t_before = time.time()
# Compute circumference and step angles for current layer
layCircum = 2 * math.pi * layDep
if lenDP == 1:
layCircum = 2 * math.pi * bbRad
# Set axial feed rates
self.axialFeed = 360 / layCircum * self.horizFeed
self.axialRapid = 360 / layCircum * self.horizRapid
# Determine step angle.
if obj.RotationAxis == obj.DropCutterDir: # Same == indexed
stepDeg = (self.cutOut / layCircum) * 360.0
else:
stepDeg = (obj.SampleInterval.Value / layCircum) * 360.0
# Limit step angle and determine rotational index angles [indexes].
if stepDeg > 120.0:
stepDeg = 120.0
advances = indexAdvances(arc, stepDeg) # Reset for each step down layer
# Perform rotational indexed scans to layer depth
if obj.RotationAxis == obj.DropCutterDir: # Same == indexed OR parallel
sample = obj.SampleInterval.Value
else:
sample = self.cutOut
scanLines = self._indexedDropCutScan(obj, stl, advances, xmin, ymin, xmax, ymax, layDep, sample)
# Complete rotation if necessary
if arc == 360.0:
advances.append(360.0 - sum(advances))
advances.pop(0)
zero = scanLines.pop(0)
scanLines.append(zero)
# Translate OCL scans into gcode
if obj.RotationAxis == obj.DropCutterDir: # Same == indexed (cutter runs parallel to axis)
# Invert advances if RotationAxis == Y
if obj.RotationAxis == 'Y':
advances = invertAdvances(advances)
# Translate scan to gcode
# sumAdv = 0.0
sumAdv = begIdx
for sl in range(0, len(scanLines)):
sumAdv += advances[sl]
# Translate scan to gcode
iSTG = self._indexedScanToGcode(obj, sl, scanLines[sl], sumAdv, prevDepth, layDep, lenDP)
commands.extend(iSTG)
# Add rise to clear height before beginning next index in CutPattern: Line
# if obj.CutPattern == 'Line':
# commands.append(Path.Command('G0', {'Z': self.clearHeight, 'F': self.vertRapid}))
# Raise cutter to safe height after each index cut
commands.append(Path.Command('G0', {'Z': self.clearHeight, 'F': self.vertRapid}))
# Eol
else:
if self.CutClimb is False:
advances = invertAdvances(advances)
advances.reverse()
scanLines.reverse()
# Invert advances if RotationAxis == Y
if obj.RotationAxis == 'Y':
advances = invertAdvances(advances)
# Begin gcode operation with raising cutter to safe height
commands.append(Path.Command('G0', {'Z': self.clearHeight, 'F': self.vertRapid}))
# Convert rotational scans into gcode
rings = linesToPointRings(scanLines)
rNum = 0
for rng in rings:
rSTG = self._rotationalScanToGcode(obj, rng, rNum, prevDepth, layDep, advances)
commands.extend(rSTG)
if arc != 360.0:
clrZ = self.layerEndzMax + self.SafeHeightOffset
commands.append(Path.Command('G0', {'Z': clrZ, 'F': self.vertRapid}))
rNum += 1
# Eol
# Add rise to clear height before beginning next index in CutPattern: Line
# if obj.CutPattern == 'Line':
# commands.append(Path.Command('G0', {'Z': self.clearHeight, 'F': self.vertRapid}))
prevDepth = layDep
lCnt += 1 # increment layer count
PathLog.debug("--Layer " + str(lCnt) + ": " + str(len(advances)) + " OCL scans and gcode in " + str(time.time() - t_before) + " s")
time.sleep(0.2)
# Eol
return commands
def _indexedDropCutScan(self, obj, stl, advances, xmin, ymin, xmax, ymax, layDep, sample):
cutterOfst = 0.0
# radsRot = 0.0
# reset = 0.0
iCnt = 0
Lines = []
result = None
pdc = ocl.PathDropCutter() # create a pdc
pdc.setCutter(self.cutter)
pdc.setZ(layDep) # set minimumZ (final / ta9rget depth value)
pdc.setSampling(sample)
# if self.useTiltCutter == True:
if obj.CutterTilt != 0.0:
cutterOfst = layDep * math.sin(math.radians(obj.CutterTilt))
PathLog.debug("CutterTilt: cutterOfst is " + str(cutterOfst))
sumAdv = 0.0
for adv in advances:
sumAdv += adv
if adv > 0.0:
# Rotate STL object using OCL method
radsRot = math.radians(adv)
if obj.RotationAxis == 'X':
stl.rotate(radsRot, 0.0, 0.0)
else:
stl.rotate(0.0, radsRot, 0.0)
# Set STL after rotation is made
pdc.setSTL(stl)
# add Line objects to the path in this loop
if obj.RotationAxis == 'X':
p1 = ocl.Point(xmin, cutterOfst, 0.0) # start-point of line
p2 = ocl.Point(xmax, cutterOfst, 0.0) # end-point of line
else:
p1 = ocl.Point(cutterOfst, ymin, 0.0) # start-point of line
p2 = ocl.Point(cutterOfst, ymax, 0.0) # end-point of line
# Create line object
if obj.RotationAxis == obj.DropCutterDir: # parallel cut
if obj.CutPattern == 'ZigZag':
if (iCnt % 2 == 0.0): # even
lo = ocl.Line(p1, p2)
else: # odd
lo = ocl.Line(p2, p1)
elif obj.CutPattern == 'Line':
if self.CutClimb is True:
lo = ocl.Line(p2, p1)
else:
lo = ocl.Line(p1, p2)
else:
lo = ocl.Line(p1, p2) # line-object
path = ocl.Path() # create an empty path object
path.append(lo) # add the line to the path
pdc.setPath(path) # set path
pdc.run() # run drop-cutter on the path
result = pdc.getCLPoints()
Lines.append(result) # request the list of points
iCnt += 1
# End loop
# Rotate STL object back to original position using OCL method
reset = -1 * math.radians(sumAdv - self.resetTolerance)
if obj.RotationAxis == 'X':
stl.rotate(reset, 0.0, 0.0)
else:
stl.rotate(0.0, reset, 0.0)
self.resetTolerance = 0.0
return Lines
def _indexedScanToGcode(self, obj, li, CLP, idxAng, prvDep, layerDepth, numDeps):
# generate the path commands
output = []
optimize = obj.OptimizeLinearPaths
holdCount = 0
holdStart = False
holdStop = False
zMax = prvDep
lenCLP = len(CLP)
lastCLP = lenCLP - 1
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 = CLP[0].x
pnt.y = CLP[0].y
pnt.z = CLP[0].z + float(obj.DepthOffset.Value)
# Rotate to correct index location
if obj.RotationAxis == 'X':
output.append(Path.Command('G0', {'A': idxAng, 'F': self.axialFeed}))
else:
output.append(Path.Command('G0', {'B': idxAng, 'F': self.axialFeed}))
if li > 0:
if pnt.z > self.layerEndPnt.z:
clrZ = pnt.z + 2.0
output.append(Path.Command('G1', {'Z': clrZ, 'F': self.vertRapid}))
else:
output.append(Path.Command('G0', {'Z': self.clearHeight, '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}))
for i in range(0, lenCLP):
if i < lastCLP:
nxt.x = CLP[i + 1].x
nxt.y = CLP[i + 1].y
nxt.z = CLP[i + 1].z + float(obj.DepthOffset.Value)
else:
optimize = False
# Update zMax values
if pnt.z > zMax:
zMax = pnt.z
if obj.LayerMode == 'Multi-pass':
# if z travels above previous layer, start/continue hold high cycle
if pnt.z > prvDep and optimize is True:
if self.onHold is False:
holdStart = True
self.onHold = True
if self.onHold is True:
if holdStart is True:
# go to current coordinate
output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed}))
# Save holdStart coordinate and prvDep values
self.holdPoint.x = pnt.x
self.holdPoint.y = pnt.y
self.holdPoint.z = pnt.z
holdCount += 1 # Increment hold count
holdStart = False # cancel holdStart
# hold cutter high until Z value drops below prvDep
if pnt.z <= prvDep:
holdStop = True
if holdStop is True:
# Send hold and current points to
zMax += 2.0
for cmd in self.holdStopCmds(obj, zMax, prvDep, pnt, "Hold Stop: in-line"):
output.append(cmd)
# reset necessary hold related settings
zMax = prvDep
holdStop = False
self.onHold = False
self.holdPoint = ocl.Point(float("inf"), float("inf"), float("inf"))
if self.onHold is False:
if not optimize or not self.isPointOnLine(FreeCAD.Vector(prev.x, prev.y, prev.z), FreeCAD.Vector(nxt.x, nxt.y, nxt.z), FreeCAD.Vector(pnt.x, pnt.y, pnt.z)):
output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed}))
# elif i == lastCLP:
# output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed}))
# Rotate point data
prev.x = pnt.x
prev.y = pnt.y
prev.z = pnt.z
pnt.x = nxt.x
pnt.y = nxt.y
pnt.z = nxt.z
output.append(Path.Command('N (End index angle ' + str(round(idxAng, 4)) + ')', {}))
# 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
def _rotationalScanToGcode(self, obj, RNG, rN, prvDep, layDep, advances):
'''_rotationalScanToGcode(obj, RNG, rN, prvDep, layDep, advances) ...
Convert rotational scan data to gcode path commands.'''
output = []
nxtAng = 0
zMax = 0.0
# 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"))
begIdx = obj.StartIndex
endIdx = obj.StopIndex
if endIdx < begIdx:
begIdx -= 360.0
# Rotate to correct index location
axisOfRot = 'A'
if obj.RotationAxis == 'Y':
axisOfRot = 'B'
# Create first point
ang = 0.0 + obj.CutterTilt
pnt.x = RNG[0].x
pnt.y = RNG[0].y
pnt.z = RNG[0].z + float(obj.DepthOffset.Value)
# Adjust feed rate based on radius/circumference of cutter.
# Original feed rate based on travel at circumference.
if rN > 0:
# if pnt.z > self.layerEndPnt.z:
if pnt.z >= self.layerEndzMax:
clrZ = pnt.z + 5.0
output.append(Path.Command('G1', {'Z': clrZ, 'F': self.vertRapid}))
else:
output.append(Path.Command('G1', {'Z': self.clearHeight, 'F': self.vertRapid}))
output.append(Path.Command('G0', {axisOfRot: ang, 'F': self.axialFeed}))
output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'F': self.axialFeed}))
output.append(Path.Command('G1', {'Z': pnt.z, 'F': self.axialFeed}))
lenRNG = len(RNG)
lastIdx = lenRNG - 1
for i in range(0, lenRNG):
if i < lastIdx:
nxtAng = ang + advances[i + 1]
nxt.x = RNG[i + 1].x
nxt.y = RNG[i + 1].y
nxt.z = RNG[i + 1].z + float(obj.DepthOffset.Value)
if pnt.z > zMax:
zMax = pnt.z
output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, axisOfRot: ang, 'F': self.axialFeed}))
pnt.x = nxt.x
pnt.y = nxt.y
pnt.z = nxt.z
ang = nxtAng
# Save layer end point for use in transitioning to next layer
self.layerEndPnt.x = RNG[0].x
self.layerEndPnt.y = RNG[0].y
self.layerEndPnt.z = RNG[0].z
self.layerEndzMax = zMax
# Move cutter to final point
# output.append(Path.Command('G1', {'X': self.layerEndPnt.x, 'Y': self.layerEndPnt.y, 'Z': self.layerEndPnt.z, axisOfRot: endang, 'F': self.axialFeed}))
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 = []
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('Algorithm')
setup.append('AvoidLastX_Faces')
setup.append('AvoidLastX_InternalFeatures')
setup.append('BoundBox')
setup.append('BoundaryAdjustment')
setup.append('CircularCenterAt')
setup.append('CircularCenterCustom')
setup.append('CircularUseG2G3')
setup.append('InternalFeaturesCut')
setup.append('InternalFeaturesAdjustment')
setup.append('CutMode')
setup.append('CutPattern')
setup.append('CutPatternAngle')
setup.append('CutPatternReversed')
setup.append('CutterTilt')
setup.append('DepthOffset')
setup.append('DropCutterDir')
setup.append('GapSizes')
setup.append('GapThreshold')
setup.append('HandleMultipleFeatures')
setup.append('LayerMode')
setup.append('OptimizeStepOverTransitions')
setup.append('ProfileEdges')
setup.append('BoundaryEnforcement')
setup.append('RotationAxis')
setup.append('SampleInterval')
setup.append('ScanType')
setup.append('StartIndex')
setup.append('StartPoint')
setup.append('StepOver')
setup.append('StopIndex')
setup.append('UseStartPoint')
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
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