2238 lines
96 KiB
Python
2238 lines
96 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-15 10:55 CST *
|
|
# * *
|
|
# ***************************************************************************
|
|
|
|
from __future__ import print_function
|
|
|
|
import FreeCAD
|
|
import MeshPart
|
|
import Path
|
|
import PathScripts.PathLog as PathLog
|
|
import PathScripts.PathUtils as PathUtils
|
|
import PathScripts.PathOp as PathOp
|
|
|
|
from PySide import QtCore
|
|
import time
|
|
import math
|
|
import Part
|
|
import Draft
|
|
|
|
if FreeCAD.GuiUp:
|
|
import FreeCADGui
|
|
|
|
__title__ = "Path Surface Operation"
|
|
__author__ = "sliptonic (Brad Collette)"
|
|
__url__ = "http://www.freecadweb.org"
|
|
__doc__ = "Class and implementation of Mill Facing operation."
|
|
|
|
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
|
|
# PathLog.trackModule(PathLog.thisModule())
|
|
|
|
|
|
# Qt translation handling
|
|
def translate(context, text, disambig=None):
|
|
return QtCore.QCoreApplication.translate(context, text, disambig)
|
|
|
|
|
|
# OCL must be installed
|
|
try:
|
|
import ocl
|
|
except ImportError:
|
|
FreeCAD.Console.PrintError(
|
|
translate("Path_Surface", "This operation requires OpenCamLib to be installed.") + "\n")
|
|
import sys
|
|
sys.exit(translate("Path_Surface", "This operation requires OpenCamLib to be installed."))
|
|
|
|
|
|
class ObjectSurface(PathOp.ObjectOp):
|
|
'''Proxy object for Surfacing operation.'''
|
|
|
|
def baseObject(self):
|
|
'''baseObject() ... returns super of receiver
|
|
Used to call base implementation in overwritten functions.'''
|
|
return super(self.__class__, self)
|
|
|
|
def opFeatures(self, obj):
|
|
'''opFeatures(obj) ... return all standard features and edges based geomtries'''
|
|
return PathOp.FeatureTool | PathOp.FeatureDepths | PathOp.FeatureHeights | PathOp.FeatureStepDown | PathOp.FeatureCoolant | PathOp.FeatureBaseFaces
|
|
|
|
def initOperation(self, obj):
|
|
'''initPocketOp(obj) ... create facing specific properties'''
|
|
obj.addProperty("App::PropertyEnumeration", "BoundBox", "Waterline", QtCore.QT_TRANSLATE_NOOP("App::Property", "Should the operation be limited by the stock object or by the bounding box of the base object"))
|
|
obj.addProperty("App::PropertyEnumeration", "LayerMode", "Waterline", QtCore.QT_TRANSLATE_NOOP("App::Property", "The completion mode for the operation: single or multi-pass"))
|
|
obj.addProperty("App::PropertyEnumeration", "ScanType", "Waterline", QtCore.QT_TRANSLATE_NOOP("App::Property", "Planar: Flat, 3D surface scan. Rotational: 4th-axis rotational scan."))
|
|
|
|
obj.addProperty("App::PropertyFloat", "CutterTilt", "Rotational", QtCore.QT_TRANSLATE_NOOP("App::Property", "Stop index(angle) for rotational scan"))
|
|
obj.addProperty("App::PropertyEnumeration", "RotationAxis", "Rotational", QtCore.QT_TRANSLATE_NOOP("App::Property", "The model will be rotated around this axis."))
|
|
obj.addProperty("App::PropertyFloat", "StartIndex", "Rotational", QtCore.QT_TRANSLATE_NOOP("App::Property", "Start index(angle) for rotational scan"))
|
|
obj.addProperty("App::PropertyFloat", "StopIndex", "Rotational", QtCore.QT_TRANSLATE_NOOP("App::Property", "Stop index(angle) for rotational scan"))
|
|
|
|
obj.addProperty("App::PropertyInteger", "AvoidLastX_Faces", "Surface", QtCore.QT_TRANSLATE_NOOP("App::Property", "Avoid cutting the last 'N' faces in the Base Geometry list of selected faces."))
|
|
obj.addProperty("App::PropertyBool", "AvoidLastX_InternalFeatures", "Surface", QtCore.QT_TRANSLATE_NOOP("App::Property", "Do not cut internal features on avoided faces."))
|
|
obj.addProperty("App::PropertyDistance", "BoundaryAdjustment", "Surface", QtCore.QT_TRANSLATE_NOOP("App::Property", "Positive values push the cutter toward, or beyond, the boundary. Negative values retract the cutter away from the boundary."))
|
|
obj.addProperty("App::PropertyBool", "BoundaryEnforcement", "Surface", QtCore.QT_TRANSLATE_NOOP("App::Property", "If true, the cutter will remain inside the boundaries of the model or selected face(s)."))
|
|
obj.addProperty("App::PropertyDistance", "DepthOffset", "Surface", QtCore.QT_TRANSLATE_NOOP("App::Property", "Z-axis offset from the surface of the object"))
|
|
obj.addProperty("App::PropertyEnumeration", "HandleMultipleFeatures", "Surface", QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose how to process multiple Base Geometry features."))
|
|
obj.addProperty("App::PropertyDistance", "InternalFeaturesAdjustment", "Surface", QtCore.QT_TRANSLATE_NOOP("App::Property", "Positive values push the cutter toward, or into, the feature. Negative values retract the cutter away from the feature."))
|
|
obj.addProperty("App::PropertyBool", "InternalFeaturesCut", "Surface", QtCore.QT_TRANSLATE_NOOP("App::Property", "Ignore internal feature areas within a larger selected face."))
|
|
obj.addProperty("App::PropertyEnumeration", "ProfileEdges", "Surface", QtCore.QT_TRANSLATE_NOOP("App::Property", "Profile the edges of the selection."))
|
|
obj.addProperty("App::PropertyDistance", "SampleInterval", "Surface", QtCore.QT_TRANSLATE_NOOP("App::Property", "The Sample Interval. Small values cause long wait times"))
|
|
obj.addProperty("App::PropertyPercent", "StepOver", "Surface", QtCore.QT_TRANSLATE_NOOP("App::Property", "Step over percentage of the drop cutter path"))
|
|
|
|
obj.addProperty("App::PropertyVectorDistance", "CircularCenterCustom", "Surface Cut Options", QtCore.QT_TRANSLATE_NOOP("PathOp", "The start point of this path"))
|
|
obj.addProperty("App::PropertyEnumeration", "CircularCenterAt", "Surface Cut Options", QtCore.QT_TRANSLATE_NOOP("PathOp", "Choose what point to start the ciruclar pattern: Center Of Mass, Center Of Boundbox, Xmin Ymin of boundbox, Custom."))
|
|
obj.addProperty("App::PropertyEnumeration", "CutMode", "Surface Cut Options", QtCore.QT_TRANSLATE_NOOP("App::Property", "The direction that the toolpath should go around the part: Climb(ClockWise) or Conventional(CounterClockWise)"))
|
|
obj.addProperty("App::PropertyEnumeration", "CutPattern", "Surface Cut Options", QtCore.QT_TRANSLATE_NOOP("App::Property", "Clearing pattern to use"))
|
|
obj.addProperty("App::PropertyFloat", "CutPatternAngle", "Surface Cut Options", QtCore.QT_TRANSLATE_NOOP("App::Property", "Yaw angle for certain clearing patterns"))
|
|
obj.addProperty("App::PropertyBool", "CutPatternReversed", "Surface Cut Options", QtCore.QT_TRANSLATE_NOOP("App::Property", "If true, the order of the step-overs will be reversed; the operation will begin cutting the outer most line/arc, and work toward the inner most line/arc."))
|
|
|
|
obj.addProperty("App::PropertyBool", "OptimizeLinearPaths", "Surface Optimization", QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable optimization of linear paths (co-linear points). Removes unnecessary co-linear points from G-Code output."))
|
|
obj.addProperty("App::PropertyBool", "OptimizeStepOverTransitions", "Surface Optimization", QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable separate optimization of transitions between, and breaks within, each step over path."))
|
|
obj.addProperty("App::PropertyBool", "CircularUseG2G3", "Surface Optimization", QtCore.QT_TRANSLATE_NOOP("App::Property", "Convert co-planar arcs to G2/G3 gcode commands for `Circular` and `CircularZigZag` cut patterns."))
|
|
obj.addProperty("App::PropertyDistance", "GapThreshold", "Surface Optimization", QtCore.QT_TRANSLATE_NOOP("App::Property", "Collinear and co-radial artifact gaps that are smaller than this threshold are closed in the path."))
|
|
obj.addProperty("App::PropertyString", "GapSizes", "Surface Optimization", QtCore.QT_TRANSLATE_NOOP("App::Property", "Feedback: three smallest gaps identified in the path geometry."))
|
|
|
|
obj.addProperty("App::PropertyBool", "IgnoreWaste", "Waste", QtCore.QT_TRANSLATE_NOOP("App::Property", "Ignore areas that proceed below specified depth."))
|
|
obj.addProperty("App::PropertyFloat", "IgnoreWasteDepth", "Waste", QtCore.QT_TRANSLATE_NOOP("App::Property", "Depth used to identify waste areas to ignore."))
|
|
obj.addProperty("App::PropertyBool", "ReleaseFromWaste", "Waste", QtCore.QT_TRANSLATE_NOOP("App::Property", "Cut through waste to depth at model edge, releasing the model."))
|
|
|
|
obj.addProperty("App::PropertyVectorDistance", "StartPoint", "Start Point", QtCore.QT_TRANSLATE_NOOP("PathOp", "The start point of this path"))
|
|
obj.addProperty("App::PropertyBool", "UseStartPoint", "Start Point", QtCore.QT_TRANSLATE_NOOP("PathOp", "Make True, if specifying a Start Point"))
|
|
|
|
# For debugging
|
|
obj.addProperty('App::PropertyString', 'AreaParams', 'Debugging')
|
|
obj.setEditorMode('AreaParams', 2) # hide
|
|
obj.addProperty("App::PropertyBool", "ShowTempObjects", "Debug", QtCore.QT_TRANSLATE_NOOP("App::Property", "If true, the temporary path construction objects will be shown."))
|
|
if PathLog.getLevel(PathLog.thisModule()) != 4:
|
|
obj.setEditorMode('ShowTempObjects', 2) # hide
|
|
|
|
obj.BoundBox = ['BaseBoundBox', 'Stock']
|
|
obj.CircularCenterAt = ['CenterOfMass', 'CenterOfBoundBox', 'XminYmin', 'Custom']
|
|
obj.CutMode = ['Conventional', 'Climb']
|
|
obj.CutPattern = ['Line', 'ZigZag', 'Circular', 'CircularZigZag'] # Additional goals ['Offset', 'Spiral', 'ZigZagOffset', 'Grid', 'Triangle']
|
|
obj.HandleMultipleFeatures = ['Collectively', 'Individually']
|
|
obj.LayerMode = ['Single-pass', 'Multi-pass']
|
|
obj.ProfileEdges = ['None', 'Only', 'First', 'Last']
|
|
obj.RotationAxis = ['X', 'Y']
|
|
obj.ScanType = ['Planar', 'Rotational']
|
|
|
|
if not hasattr(obj, 'DoNotSetDefaultValues'):
|
|
self.setEditorProperties(obj)
|
|
|
|
self.addedAllProperties = True
|
|
|
|
def setEditorProperties(self, obj):
|
|
# Used to hide inputs in properties list
|
|
|
|
'''
|
|
obj.setEditorMode('CutPattern', 0)
|
|
obj.setEditorMode('HandleMultipleFeatures', 0)
|
|
obj.setEditorMode('CircularCenterAt', 0)
|
|
obj.setEditorMode('CircularCenterCustom', 0)
|
|
obj.setEditorMode('CutPatternAngle', 0)
|
|
# obj.setEditorMode('BoundaryEnforcement', 0)
|
|
|
|
if obj.ScanType == 'Planar':
|
|
obj.setEditorMode('RotationAxis', 2) # 2=hidden
|
|
obj.setEditorMode('StartIndex', 2)
|
|
obj.setEditorMode('StopIndex', 2)
|
|
obj.setEditorMode('CutterTilt', 2)
|
|
if obj.CutPattern == 'Circular' or obj.CutPattern == 'CircularZigZag':
|
|
obj.setEditorMode('CutPatternAngle', 2)
|
|
else: # if obj.CutPattern == 'Line' or obj.CutPattern == 'ZigZag':
|
|
obj.setEditorMode('CircularCenterAt', 2)
|
|
obj.setEditorMode('CircularCenterCustom', 2)
|
|
elif obj.ScanType == 'Rotational':
|
|
obj.setEditorMode('RotationAxis', 0) # 0=show & editable
|
|
obj.setEditorMode('StartIndex', 0)
|
|
obj.setEditorMode('StopIndex', 0)
|
|
obj.setEditorMode('CutterTilt', 0)
|
|
'''
|
|
|
|
obj.setEditorMode('HandleMultipleFeatures', 2)
|
|
obj.setEditorMode('CutPattern', 2)
|
|
obj.setEditorMode('CutPatternAngle', 2)
|
|
# obj.setEditorMode('BoundaryEnforcement', 2)
|
|
|
|
# Disable IgnoreWaste feature
|
|
obj.setEditorMode('IgnoreWaste', 2)
|
|
obj.setEditorMode('IgnoreWasteDepth', 2)
|
|
obj.setEditorMode('ReleaseFromWaste', 2)
|
|
|
|
def onChanged(self, obj, prop):
|
|
if hasattr(self, 'addedAllProperties'):
|
|
if self.addedAllProperties is True:
|
|
if prop == 'ScanType':
|
|
self.setEditorProperties(obj)
|
|
if prop == 'CutPattern':
|
|
self.setEditorProperties(obj)
|
|
|
|
def opOnDocumentRestored(self, obj):
|
|
if PathLog.getLevel(PathLog.thisModule()) != 4:
|
|
obj.setEditorMode('ShowTempObjects', 2) # hide
|
|
else:
|
|
obj.setEditorMode('ShowTempObjects', 0) # show
|
|
self.addedAllProperties = True
|
|
self.setEditorProperties(obj)
|
|
|
|
def opSetDefaultValues(self, obj, job):
|
|
'''opSetDefaultValues(obj, job) ... initialize defaults'''
|
|
job = PathUtils.findParentJob(obj)
|
|
|
|
obj.OptimizeLinearPaths = True
|
|
obj.IgnoreWaste = False
|
|
obj.ReleaseFromWaste = False
|
|
obj.InternalFeaturesCut = True
|
|
obj.OptimizeStepOverTransitions = False
|
|
obj.CircularUseG2G3 = False
|
|
obj.BoundaryEnforcement = True
|
|
obj.UseStartPoint = False
|
|
obj.AvoidLastX_InternalFeatures = True
|
|
obj.CutPatternReversed = False
|
|
obj.StartPoint.x = 0.0
|
|
obj.StartPoint.y = 0.0
|
|
obj.StartPoint.z = obj.ClearanceHeight.Value
|
|
obj.ProfileEdges = 'None'
|
|
obj.LayerMode = 'Single-pass'
|
|
obj.ScanType = 'Planar'
|
|
obj.RotationAxis = 'X'
|
|
obj.CutMode = 'Conventional'
|
|
obj.CutPattern = 'Line'
|
|
obj.HandleMultipleFeatures = 'Collectively' # 'Individually'
|
|
obj.CircularCenterAt = 'CenterOfMass' # 'CenterOfBoundBox', 'XminYmin', 'Custom'
|
|
obj.AreaParams = ''
|
|
obj.GapSizes = 'No gaps identified.'
|
|
obj.StepOver = 100
|
|
obj.CutPatternAngle = 0.0
|
|
obj.CutterTilt = 0.0
|
|
obj.StartIndex = 0.0
|
|
obj.StopIndex = 360.0
|
|
obj.SampleInterval.Value = 1.0
|
|
obj.BoundaryAdjustment.Value = 0.0
|
|
obj.InternalFeaturesAdjustment.Value = 0.0
|
|
obj.AvoidLastX_Faces = 0
|
|
obj.CircularCenterCustom.x = 0.0
|
|
obj.CircularCenterCustom.y = 0.0
|
|
obj.CircularCenterCustom.z = 0.0
|
|
obj.GapThreshold.Value = 0.005
|
|
# For debugging
|
|
obj.ShowTempObjects = False
|
|
|
|
# need to overwrite the default depth calculations for facing
|
|
d = None
|
|
if job:
|
|
if job.Stock:
|
|
d = PathUtils.guessDepths(job.Stock.Shape, None)
|
|
PathLog.debug("job.Stock exists")
|
|
else:
|
|
PathLog.debug("job.Stock NOT exist")
|
|
else:
|
|
PathLog.debug("job NOT exist")
|
|
|
|
if d is not None:
|
|
obj.OpFinalDepth.Value = d.final_depth
|
|
obj.OpStartDepth.Value = d.start_depth
|
|
else:
|
|
obj.OpFinalDepth.Value = -10
|
|
obj.OpStartDepth.Value = 10
|
|
|
|
PathLog.debug('Default OpFinalDepth: {}'.format(obj.OpFinalDepth.Value))
|
|
PathLog.debug('Defualt OpStartDepth: {}'.format(obj.OpStartDepth.Value))
|
|
|
|
def opApplyPropertyLimits(self, obj):
|
|
'''opApplyPropertyLimits(obj) ... Apply necessary limits to user input property values before performing main operation.'''
|
|
# Limit start index
|
|
if obj.StartIndex < 0.0:
|
|
obj.StartIndex = 0.0
|
|
if obj.StartIndex > 360.0:
|
|
obj.StartIndex = 360.0
|
|
|
|
# Limit stop index
|
|
if obj.StopIndex > 360.0:
|
|
obj.StopIndex = 360.0
|
|
if obj.StopIndex < 0.0:
|
|
obj.StopIndex = 0.0
|
|
|
|
# Limit cutter tilt
|
|
if obj.CutterTilt < -90.0:
|
|
obj.CutterTilt = -90.0
|
|
if obj.CutterTilt > 90.0:
|
|
obj.CutterTilt = 90.0
|
|
|
|
# Limit sample interval
|
|
if obj.SampleInterval.Value < 0.001:
|
|
obj.SampleInterval.Value = 0.001
|
|
PathLog.error(translate('PathSurface', 'Sample interval limits are 0.001 to 25.4 millimeters.'))
|
|
if obj.SampleInterval.Value > 25.4:
|
|
obj.SampleInterval.Value = 25.4
|
|
PathLog.error(translate('PathSurface', 'Sample interval limits are 0.001 to 25.4 millimeters.'))
|
|
|
|
# Limit cut pattern angle
|
|
if obj.CutPatternAngle < -360.0:
|
|
obj.CutPatternAngle = 0.0
|
|
PathLog.error(translate('PathSurface', 'Cut pattern angle limits are +-360 degrees.'))
|
|
if obj.CutPatternAngle >= 360.0:
|
|
obj.CutPatternAngle = 0.0
|
|
PathLog.error(translate('PathSurface', 'Cut pattern angle limits are +- 360 degrees.'))
|
|
|
|
# Limit StepOver to natural number percentage
|
|
if obj.StepOver > 100:
|
|
obj.StepOver = 100
|
|
if obj.StepOver < 1:
|
|
obj.StepOver = 1
|
|
|
|
# Limit AvoidLastX_Faces to zero and positive values
|
|
if obj.AvoidLastX_Faces < 0:
|
|
obj.AvoidLastX_Faces = 0
|
|
PathLog.error(translate('PathSurface', 'AvoidLastX_Faces: Only zero or positive values permitted.'))
|
|
if obj.AvoidLastX_Faces > 100:
|
|
obj.AvoidLastX_Faces = 100
|
|
PathLog.error(translate('PathSurface', 'AvoidLastX_Faces: Avoid last X faces count limited to 100.'))
|
|
|
|
def opExecute(self, obj):
|
|
'''opExecute(obj) ... process surface operation'''
|
|
PathLog.track()
|
|
|
|
self.modelSTLs = list()
|
|
self.safeSTLs = list()
|
|
self.modelTypes = list()
|
|
self.boundBoxes = list()
|
|
self.profileShapes = list()
|
|
self.collectiveShapes = list()
|
|
self.individualShapes = list()
|
|
self.avoidShapes = list()
|
|
self.deflection = None
|
|
self.tempGroup = None
|
|
self.CutClimb = False
|
|
self.closedGap = False
|
|
self.gaps = [0.1, 0.2, 0.3]
|
|
CMDS = list()
|
|
modelVisibility = list()
|
|
FCAD = FreeCAD.ActiveDocument
|
|
|
|
# Set debugging behavior
|
|
self.showDebugObjects = False # Set to true if you want a visual DocObjects created for some path construction objects
|
|
self.showDebugObjects = obj.ShowTempObjects
|
|
deleteTempsFlag = True # Set to False for debugging
|
|
if PathLog.getLevel(PathLog.thisModule()) == 4:
|
|
deleteTempsFlag = False
|
|
else:
|
|
self.showDebugObjects = False
|
|
|
|
# mark beginning of operation and identify parent Job
|
|
PathLog.info('\nBegin 3D Surface operation...')
|
|
startTime = time.time()
|
|
|
|
# Disable(ignore) ReleaseFromWaste option(input)
|
|
obj.ReleaseFromWaste = False
|
|
|
|
# Identify parent Job
|
|
JOB = PathUtils.findParentJob(obj)
|
|
if JOB is None:
|
|
PathLog.error(translate('PathSurface', "No JOB"))
|
|
return
|
|
self.stockZMin = JOB.Stock.Shape.BoundBox.ZMin
|
|
|
|
# set cut mode; reverse as needed
|
|
if obj.CutMode == 'Climb':
|
|
self.CutClimb = True
|
|
if obj.CutPatternReversed is True:
|
|
if self.CutClimb is True:
|
|
self.CutClimb = False
|
|
else:
|
|
self.CutClimb = True
|
|
|
|
# Begin GCode for operation with basic information
|
|
# ... and move cutter to clearance height and startpoint
|
|
output = ''
|
|
if obj.Comment != '':
|
|
output += '(' + str(obj.Comment) + ')\n'
|
|
output += '(' + obj.Label + ')\n'
|
|
output += '(Tool type: ' + str(obj.ToolController.Tool.ToolType) + ')\n'
|
|
output += '(Compensated Tool Path. Diameter: ' + str(obj.ToolController.Tool.Diameter) + ')\n'
|
|
output += '(Sample interval: ' + str(obj.SampleInterval.Value) + ')\n'
|
|
output += '(Step over %: ' + str(obj.StepOver) + ')\n'
|
|
self.commandlist.append(Path.Command('N ({})'.format(output), {}))
|
|
self.commandlist.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid}))
|
|
if obj.UseStartPoint is True:
|
|
self.commandlist.append(Path.Command('G0', {'X': obj.StartPoint.x, 'Y': obj.StartPoint.y, 'F': self.horizRapid}))
|
|
|
|
# Instantiate additional class operation variables
|
|
self.resetOpVariables()
|
|
|
|
# Impose property limits
|
|
self.opApplyPropertyLimits(obj)
|
|
|
|
# Create temporary group for temporary objects, removing existing
|
|
# if self.showDebugObjects is True:
|
|
tempGroupName = 'tempPathSurfaceGroup'
|
|
if FCAD.getObject(tempGroupName):
|
|
for to in FCAD.getObject(tempGroupName).Group:
|
|
FCAD.removeObject(to.Name)
|
|
FCAD.removeObject(tempGroupName) # remove temp directory if already exists
|
|
if FCAD.getObject(tempGroupName + '001'):
|
|
for to in FCAD.getObject(tempGroupName + '001').Group:
|
|
FCAD.removeObject(to.Name)
|
|
FCAD.removeObject(tempGroupName + '001') # remove temp directory if already exists
|
|
tempGroup = FCAD.addObject('App::DocumentObjectGroup', tempGroupName)
|
|
tempGroupName = tempGroup.Name
|
|
self.tempGroup = tempGroup
|
|
tempGroup.purgeTouched()
|
|
# Add temp object to temp group folder with following code:
|
|
# ... self.tempGroup.addObject(OBJ)
|
|
|
|
# Setup cutter for OCL and cutout value for operation - based on tool controller properties
|
|
self.cutter = self.setOclCutter(obj)
|
|
self.safeCutter = self.setOclCutter(obj, safe=True)
|
|
if self.cutter is False or self.safeCutter is False:
|
|
PathLog.error(translate('PathSurface', "Canceling 3D Surface operation. Error creating OCL cutter."))
|
|
return
|
|
toolDiam = self.cutter.getDiameter()
|
|
self.cutOut = (toolDiam * (float(obj.StepOver) / 100.0))
|
|
self.radius = toolDiam / 2.0
|
|
self.gaps = [toolDiam, toolDiam, toolDiam]
|
|
|
|
# Get height offset values for later use
|
|
self.SafeHeightOffset = JOB.SetupSheet.SafeHeightOffset.Value
|
|
self.ClearHeightOffset = JOB.SetupSheet.ClearanceHeightOffset.Value
|
|
|
|
# Calculate default depthparams for operation
|
|
self.depthParams = PathUtils.depth_params(obj.ClearanceHeight.Value, obj.SafeHeight.Value, obj.StartDepth.Value, obj.StepDown.Value, 0.0, obj.FinalDepth.Value)
|
|
self.midDep = (obj.StartDepth.Value + obj.FinalDepth.Value) / 2.0
|
|
|
|
# make circle for workplane
|
|
self.wpc = Part.makeCircle(2.0)
|
|
|
|
# Set deflection values for mesh generation
|
|
self.angularDeflection = 0.05
|
|
try: # try/except is for Path Jobs created before GeometryTolerance
|
|
self.deflection = JOB.GeometryTolerance.Value
|
|
except AttributeError as ee:
|
|
PathLog.warning('Error setting Mesh deflection: {}. Using PathPreferences.defaultGeometryTolerance().'.format(ee))
|
|
import PathScripts.PathPreferences as PathPreferences
|
|
self.deflection = PathPreferences.defaultGeometryTolerance()
|
|
|
|
# Save model visibilities for restoration
|
|
if FreeCAD.GuiUp:
|
|
for m in range(0, len(JOB.Model.Group)):
|
|
mNm = JOB.Model.Group[m].Name
|
|
modelVisibility.append(FreeCADGui.ActiveDocument.getObject(mNm).Visibility)
|
|
|
|
# Setup STL, model type, and bound box containers for each model in Job
|
|
for m in range(0, len(JOB.Model.Group)):
|
|
M = JOB.Model.Group[m]
|
|
self.modelSTLs.append(False)
|
|
self.safeSTLs.append(False)
|
|
self.profileShapes.append(False)
|
|
# Set bound box
|
|
if obj.BoundBox == 'BaseBoundBox':
|
|
if M.TypeId.startswith('Mesh'):
|
|
self.modelTypes.append('M') # Mesh
|
|
self.boundBoxes.append(M.Mesh.BoundBox)
|
|
else:
|
|
self.modelTypes.append('S') # Solid
|
|
self.boundBoxes.append(M.Shape.BoundBox)
|
|
elif obj.BoundBox == 'Stock':
|
|
self.modelTypes.append('S') # Solid
|
|
self.boundBoxes.append(JOB.Stock.Shape.BoundBox)
|
|
|
|
# ###### MAIN COMMANDS FOR OPERATION ######
|
|
|
|
# If algorithm is `Waterline`, force certain property values
|
|
'''
|
|
# Save initial value for restoration later.
|
|
if obj.Algorithm == 'OCL Waterline':
|
|
preCP = obj.CutPattern
|
|
preCPA = obj.CutPatternAngle
|
|
preRB = obj.BoundaryEnforcement
|
|
obj.CutPattern = 'Line'
|
|
obj.CutPatternAngle = 0.0
|
|
obj.BoundaryEnforcement = False
|
|
'''
|
|
|
|
# Begin processing obj.Base data and creating GCode
|
|
# Process selected faces, if available
|
|
pPM = self._preProcessModel(JOB, obj)
|
|
if pPM is False:
|
|
PathLog.error('Unable to pre-process obj.Base.')
|
|
else:
|
|
(FACES, VOIDS) = pPM
|
|
|
|
# Create OCL.stl model objects
|
|
self._prepareModelSTLs(JOB, obj)
|
|
|
|
for m in range(0, len(JOB.Model.Group)):
|
|
Mdl = JOB.Model.Group[m]
|
|
if FACES[m] is False:
|
|
PathLog.error('No data for model base: {}'.format(JOB.Model.Group[m].Label))
|
|
else:
|
|
if m > 0:
|
|
# Raise to clearance between moddels
|
|
CMDS.append(Path.Command('N (Transition to base: {}.)'.format(Mdl.Label)))
|
|
CMDS.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid}))
|
|
PathLog.info('Working on Model.Group[{}]: {}'.format(m, Mdl.Label))
|
|
# make stock-model-voidShapes STL model for avoidance detection on transitions
|
|
self._makeSafeSTL(JOB, obj, m, FACES[m], VOIDS[m])
|
|
time.sleep(0.2)
|
|
# Process model/faces - OCL objects must be ready
|
|
CMDS.extend(self._processCutAreas(JOB, obj, m, FACES[m], VOIDS[m]))
|
|
|
|
# Save gcode produced
|
|
self.commandlist.extend(CMDS)
|
|
|
|
# If algorithm is `Waterline`, restore initial property values
|
|
'''
|
|
if obj.Algorithm == 'OCL Waterline':
|
|
obj.CutPattern = preCP
|
|
obj.CutPatternAngle = preCPA
|
|
obj.BoundaryEnforcement = preRB
|
|
'''
|
|
|
|
# ###### CLOSING COMMANDS FOR OPERATION ######
|
|
|
|
# Delete temporary objects
|
|
# Restore model visibilities for restoration
|
|
if FreeCAD.GuiUp:
|
|
FreeCADGui.ActiveDocument.getObject(tempGroupName).Visibility = False
|
|
for m in range(0, len(JOB.Model.Group)):
|
|
M = JOB.Model.Group[m]
|
|
M.Visibility = modelVisibility[m]
|
|
|
|
if deleteTempsFlag is True:
|
|
for to in tempGroup.Group:
|
|
if hasattr(to, 'Group'):
|
|
for go in to.Group:
|
|
FCAD.removeObject(go.Name)
|
|
FCAD.removeObject(to.Name)
|
|
FCAD.removeObject(tempGroupName)
|
|
else:
|
|
if len(tempGroup.Group) == 0:
|
|
FCAD.removeObject(tempGroupName)
|
|
else:
|
|
tempGroup.purgeTouched()
|
|
|
|
# Provide user feedback for gap sizes
|
|
gaps = list()
|
|
for g in self.gaps:
|
|
if g != toolDiam:
|
|
gaps.append(g)
|
|
if len(gaps) > 0:
|
|
obj.GapSizes = '{} mm'.format(gaps)
|
|
else:
|
|
if self.closedGap is True:
|
|
obj.GapSizes = 'Closed gaps < Gap Threshold.'
|
|
else:
|
|
obj.GapSizes = 'No gaps identified.'
|
|
|
|
# clean up class variables
|
|
self.resetOpVariables()
|
|
self.deleteOpVariables()
|
|
|
|
self.modelSTLs = None
|
|
self.safeSTLs = None
|
|
self.modelTypes = None
|
|
self.boundBoxes = None
|
|
self.gaps = None
|
|
self.closedGap = None
|
|
self.SafeHeightOffset = None
|
|
self.ClearHeightOffset = None
|
|
self.depthParams = None
|
|
self.midDep = None
|
|
self.wpc = None
|
|
self.angularDeflection = None
|
|
self.deflection = None
|
|
del self.modelSTLs
|
|
del self.safeSTLs
|
|
del self.modelTypes
|
|
del self.boundBoxes
|
|
del self.gaps
|
|
del self.closedGap
|
|
del self.SafeHeightOffset
|
|
del self.ClearHeightOffset
|
|
del self.depthParams
|
|
del self.midDep
|
|
del self.wpc
|
|
del self.angularDeflection
|
|
del self.deflection
|
|
|
|
execTime = time.time() - startTime
|
|
PathLog.info('Operation time: {} sec.'.format(execTime))
|
|
|
|
return True
|
|
|
|
# Methods for constructing the cut area
|
|
def _preProcessModel(self, JOB, obj):
|
|
PathLog.debug('_preProcessModel()')
|
|
|
|
FACES = list()
|
|
VOIDS = list()
|
|
fShapes = list()
|
|
vShapes = list()
|
|
preProcEr = translate('PathSurface', 'Error pre-processing Face')
|
|
warnFinDep = translate('PathSurface', 'Final Depth might need to be lower. Internal features detected in Face')
|
|
GRP = JOB.Model.Group
|
|
lenGRP = len(GRP)
|
|
|
|
# Crete place holders for each base model in Job
|
|
for m in range(0, lenGRP):
|
|
FACES.append(False)
|
|
VOIDS.append(False)
|
|
fShapes.append(False)
|
|
vShapes.append(False)
|
|
|
|
# The user has selected subobjects from the base. Pre-Process each.
|
|
if obj.Base and len(obj.Base) > 0:
|
|
PathLog.debug(' -obj.Base exists. Pre-processing for selected faces.')
|
|
|
|
(FACES, VOIDS) = self._identifyFacesAndVoids(JOB, obj, FACES, VOIDS)
|
|
|
|
# Cycle through each base model, processing faces for each
|
|
for m in range(0, lenGRP):
|
|
base = GRP[m]
|
|
(mFS, mVS, mPS) = self._preProcessFacesAndVoids(obj, base, m, FACES, VOIDS)
|
|
fShapes[m] = mFS
|
|
vShapes[m] = mVS
|
|
self.profileShapes[m] = mPS
|
|
else:
|
|
PathLog.debug(' -No obj.Base data.')
|
|
for m in range(0, lenGRP):
|
|
self.modelSTLs[m] = True
|
|
|
|
# Process each model base, as a whole, as needed
|
|
# PathLog.debug(' -Pre-processing all models in Job.')
|
|
for m in range(0, lenGRP):
|
|
if fShapes[m] is False:
|
|
PathLog.debug(' -Pre-processing {} as a whole.'.format(GRP[m].Label))
|
|
if obj.BoundBox == 'BaseBoundBox':
|
|
base = GRP[m]
|
|
elif obj.BoundBox == 'Stock':
|
|
base = JOB.Stock
|
|
|
|
pPEB = self._preProcessEntireBase(obj, base, m)
|
|
if pPEB is False:
|
|
PathLog.error(' -Failed to pre-process base as a whole.')
|
|
else:
|
|
(fcShp, prflShp) = pPEB
|
|
if fcShp is not False:
|
|
if fcShp is True:
|
|
PathLog.debug(' -fcShp is True.')
|
|
fShapes[m] = True
|
|
else:
|
|
fShapes[m] = [fcShp]
|
|
if prflShp is not False:
|
|
if fcShp is not False:
|
|
PathLog.debug('vShapes[{}]: {}'.format(m, vShapes[m]))
|
|
if vShapes[m] is not False:
|
|
PathLog.debug(' -Cutting void from base profile shape.')
|
|
adjPS = prflShp.cut(vShapes[m][0])
|
|
self.profileShapes[m] = [adjPS]
|
|
else:
|
|
PathLog.debug(' -vShapes[m] is False.')
|
|
self.profileShapes[m] = [prflShp]
|
|
else:
|
|
PathLog.debug(' -Saving base profile shape.')
|
|
self.profileShapes[m] = [prflShp]
|
|
PathLog.debug('self.profileShapes[{}]: {}'.format(m, self.profileShapes[m]))
|
|
# Efor
|
|
|
|
return (fShapes, vShapes)
|
|
|
|
def _identifyFacesAndVoids(self, JOB, obj, F, V):
|
|
TUPS = list()
|
|
GRP = JOB.Model.Group
|
|
lenGRP = len(GRP)
|
|
|
|
# Separate selected faces into (base, face) tuples and flag model(s) for STL creation
|
|
for (bs, SBS) in obj.Base:
|
|
for sb in SBS:
|
|
# Flag model for STL creation
|
|
mdlIdx = None
|
|
for m in range(0, lenGRP):
|
|
if bs is GRP[m]:
|
|
self.modelSTLs[m] = True
|
|
mdlIdx = m
|
|
break
|
|
TUPS.append((mdlIdx, bs, sb)) # (model idx, base, sub)
|
|
|
|
# Apply `AvoidXFaces` value
|
|
faceCnt = len(TUPS)
|
|
add = faceCnt - obj.AvoidLastX_Faces
|
|
for bst in range(0, faceCnt):
|
|
(m, base, sub) = TUPS[bst]
|
|
shape = getattr(base.Shape, sub)
|
|
if isinstance(shape, Part.Face):
|
|
faceIdx = int(sub[4:]) - 1
|
|
if bst < add:
|
|
if F[m] is False:
|
|
F[m] = list()
|
|
F[m].append((shape, faceIdx))
|
|
else:
|
|
if V[m] is False:
|
|
V[m] = list()
|
|
V[m].append((shape, faceIdx))
|
|
return (F, V)
|
|
|
|
def _preProcessFacesAndVoids(self, obj, base, m, FACES, VOIDS):
|
|
mFS = False
|
|
mVS = False
|
|
mPS = False
|
|
mIFS = list()
|
|
BB = base.Shape.BoundBox
|
|
|
|
if FACES[m] is not False:
|
|
isHole = False
|
|
if obj.HandleMultipleFeatures == 'Collectively':
|
|
cont = True
|
|
fsL = list() # face shape list
|
|
ifL = list() # avoid shape list
|
|
outFCS = list()
|
|
|
|
# Get collective envelope slice of selected faces
|
|
for (fcshp, fcIdx) in FACES[m]:
|
|
fNum = fcIdx + 1
|
|
fsL.append(fcshp)
|
|
gFW = self._getFaceWires(base, fcshp, fcIdx)
|
|
if gFW is False:
|
|
PathLog.debug('Failed to get wires from Face{}'.format(fNum))
|
|
elif gFW[0] is False:
|
|
PathLog.debug('Cannot process Face{}. Check that it has horizontal surface exposure.'.format(fNum))
|
|
else:
|
|
((otrFace, raised), intWires) = gFW
|
|
outFCS.append(otrFace)
|
|
if obj.InternalFeaturesCut is False:
|
|
if intWires is not False:
|
|
for (iFace, rsd) in intWires:
|
|
ifL.append(iFace)
|
|
|
|
PathLog.debug('Attempting to get cross-section of collective faces.')
|
|
if len(outFCS) == 0:
|
|
PathLog.error('Cannot process selected faces. Check horizontal surface exposure.'.format(fNum))
|
|
cont = False
|
|
else:
|
|
cfsL = Part.makeCompound(outFCS)
|
|
|
|
# Handle profile edges request
|
|
if cont is True and obj.ProfileEdges != 'None':
|
|
ofstVal = self._calculateOffsetValue(obj, isHole)
|
|
psOfst = self._extractFaceOffset(obj, cfsL, ofstVal)
|
|
if psOfst is not False:
|
|
mPS = [psOfst]
|
|
if obj.ProfileEdges == 'Only':
|
|
mFS = True
|
|
cont = False
|
|
else:
|
|
PathLog.error(' -Failed to create profile geometry for selected faces.')
|
|
cont = False
|
|
|
|
if cont is True:
|
|
if self.showDebugObjects is True:
|
|
T = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCollectiveShape')
|
|
T.Shape = cfsL
|
|
T.purgeTouched()
|
|
self.tempGroup.addObject(T)
|
|
|
|
ofstVal = self._calculateOffsetValue(obj, isHole)
|
|
faceOfstShp = self._extractFaceOffset(obj, cfsL, ofstVal)
|
|
if faceOfstShp is False:
|
|
PathLog.error(' -Failed to create offset face.')
|
|
cont = False
|
|
|
|
if cont is True:
|
|
lenIfL = len(ifL)
|
|
if obj.InternalFeaturesCut is False:
|
|
if lenIfL == 0:
|
|
PathLog.debug(' -No internal features saved.')
|
|
else:
|
|
if lenIfL == 1:
|
|
casL = ifL[0]
|
|
else:
|
|
casL = Part.makeCompound(ifL)
|
|
if self.showDebugObjects is True:
|
|
C = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCompoundIntFeat')
|
|
C.Shape = casL
|
|
C.purgeTouched()
|
|
self.tempGroup.addObject(C)
|
|
ofstVal = self._calculateOffsetValue(obj, isHole=True)
|
|
intOfstShp = self._extractFaceOffset(obj, casL, ofstVal)
|
|
mIFS.append(intOfstShp)
|
|
# faceOfstShp = faceOfstShp.cut(intOfstShp)
|
|
|
|
mFS = [faceOfstShp]
|
|
# Eif
|
|
|
|
elif obj.HandleMultipleFeatures == 'Individually':
|
|
for (fcshp, fcIdx) in FACES[m]:
|
|
cont = True
|
|
fsL = list() # face shape list
|
|
ifL = list() # avoid shape list
|
|
fNum = fcIdx + 1
|
|
outerFace = False
|
|
|
|
gFW = self._getFaceWires(base, fcshp, fcIdx)
|
|
if gFW is False:
|
|
PathLog.debug('Failed to get wires from Face{}'.format(fNum))
|
|
cont = False
|
|
elif gFW[0] is False:
|
|
PathLog.debug('Cannot process Face{}. Check that it has horizontal surface exposure.'.format(fNum))
|
|
cont = False
|
|
outerFace = False
|
|
else:
|
|
((otrFace, raised), intWires) = gFW
|
|
outerFace = otrFace
|
|
if obj.InternalFeaturesCut is False:
|
|
if intWires is not False:
|
|
for (iFace, rsd) in intWires:
|
|
ifL.append(iFace)
|
|
|
|
if outerFace is not False:
|
|
PathLog.debug('Attempting to create offset face of Face{}'.format(fNum))
|
|
|
|
if obj.ProfileEdges != 'None':
|
|
ofstVal = self._calculateOffsetValue(obj, isHole)
|
|
psOfst = self._extractFaceOffset(obj, outerFace, ofstVal)
|
|
if psOfst is not False:
|
|
if mPS is False:
|
|
mPS = list()
|
|
mPS.append(psOfst)
|
|
if obj.ProfileEdges == 'Only':
|
|
if mFS is False:
|
|
mFS = list()
|
|
mFS.append(True)
|
|
cont = False
|
|
else:
|
|
PathLog.error(' -Failed to create profile geometry for Face{}.'.format(fNum))
|
|
cont = False
|
|
|
|
if cont is True:
|
|
ofstVal = self._calculateOffsetValue(obj, isHole)
|
|
faceOfstShp = self._extractFaceOffset(obj, slc, ofstVal)
|
|
|
|
lenIfl = len(ifL)
|
|
if obj.InternalFeaturesCut is False and lenIfl > 0:
|
|
if lenIfl == 1:
|
|
casL = ifL[0]
|
|
else:
|
|
casL = Part.makeCompound(ifL)
|
|
|
|
ofstVal = self._calculateOffsetValue(obj, isHole=True)
|
|
intOfstShp = self._extractFaceOffset(obj, casL, ofstVal)
|
|
mIFS.append(intOfstShp)
|
|
# faceOfstShp = faceOfstShp.cut(intOfstShp)
|
|
|
|
if mFS is False:
|
|
mFS = list()
|
|
mFS.append(faceOfstShp)
|
|
# Eif
|
|
# Efor
|
|
# Eif
|
|
# Eif
|
|
|
|
if len(mIFS) > 0:
|
|
if mVS is False:
|
|
mVS = list()
|
|
for ifs in mIFS:
|
|
mVS.append(ifs)
|
|
|
|
if VOIDS[m] is not False:
|
|
PathLog.debug('Processing avoid faces.')
|
|
cont = True
|
|
isHole = False
|
|
outFCS = list()
|
|
intFEAT = list()
|
|
|
|
for (fcshp, fcIdx) in VOIDS[m]:
|
|
fNum = fcIdx + 1
|
|
gFW = self._getFaceWires(base, fcshp, fcIdx)
|
|
if gFW is False:
|
|
PathLog.debug('Failed to get wires from avoid Face{}'.format(fNum))
|
|
cont = False
|
|
else:
|
|
((otrFace, raised), intWires) = gFW
|
|
outFCS.append(otrFace)
|
|
if obj.AvoidLastX_InternalFeatures is False:
|
|
if intWires is not False:
|
|
for (iFace, rsd) in intWires:
|
|
intFEAT.append(iFace)
|
|
|
|
lenOtFcs = len(outFCS)
|
|
if lenOtFcs == 0:
|
|
cont = False
|
|
else:
|
|
if lenOtFcs == 1:
|
|
avoid = outFCS[0]
|
|
else:
|
|
avoid = Part.makeCompound(outFCS)
|
|
|
|
if self.showDebugObjects is True:
|
|
PathLog.debug('*** tmpAvoidArea')
|
|
P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpVoidEnvelope')
|
|
P.Shape = avoid
|
|
# P.recompute()
|
|
P.purgeTouched()
|
|
self.tempGroup.addObject(P)
|
|
|
|
if cont is True:
|
|
if self.showDebugObjects is True:
|
|
PathLog.debug('*** tmpVoidCompound')
|
|
P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpVoidCompound')
|
|
P.Shape = avoid
|
|
# P.recompute()
|
|
P.purgeTouched()
|
|
self.tempGroup.addObject(P)
|
|
ofstVal = self._calculateOffsetValue(obj, isHole, isVoid=True)
|
|
avdOfstShp = self._extractFaceOffset(obj, avoid, ofstVal)
|
|
if avdOfstShp is False:
|
|
PathLog.error('Failed to create collective offset avoid face.')
|
|
cont = False
|
|
|
|
if cont is True:
|
|
avdShp = avdOfstShp
|
|
|
|
if obj.AvoidLastX_InternalFeatures is False and len(intFEAT) > 0:
|
|
if len(intFEAT) > 1:
|
|
ifc = Part.makeCompound(intFEAT)
|
|
else:
|
|
ifc = intFEAT[0]
|
|
ofstVal = self._calculateOffsetValue(obj, isHole=True)
|
|
ifOfstShp = self._extractFaceOffset(obj, ifc, ofstVal)
|
|
if ifOfstShp is False:
|
|
PathLog.error('Failed to create collective offset avoid internal features.')
|
|
else:
|
|
avdShp = avdOfstShp.cut(ifOfstShp)
|
|
|
|
if mVS is False:
|
|
mVS = list()
|
|
mVS.append(avdShp)
|
|
|
|
|
|
return (mFS, mVS, mPS)
|
|
|
|
def _getFaceWires(self, base, fcshp, fcIdx):
|
|
outFace = False
|
|
INTFCS = list()
|
|
fNum = fcIdx + 1
|
|
# preProcEr = translate('PathSurface', 'Error pre-processing Face')
|
|
warnFinDep = translate('PathSurface', 'Final Depth might need to be lower. Internal features detected in Face')
|
|
|
|
PathLog.debug('_getFaceWires() from Face{}'.format(fNum))
|
|
WIRES = self._extractWiresFromFace(base, fcshp)
|
|
if WIRES is False:
|
|
PathLog.error('Failed to extract wires from Face{}'.format(fNum))
|
|
return False
|
|
|
|
# Process remaining internal features, adding to FCS list
|
|
lenW = len(WIRES)
|
|
for w in range(0, lenW):
|
|
(wire, rsd) = WIRES[w]
|
|
PathLog.debug('Processing Wire{} in Face{}. isRaised: {}'.format(w + 1, fNum, rsd))
|
|
if wire.isClosed() is False:
|
|
PathLog.debug(' -wire is not closed.')
|
|
else:
|
|
slc = self._flattenWireToFace(wire)
|
|
if slc is False:
|
|
PathLog.error('FAILED to identify horizontal exposure on Face{}.'.format(fNum))
|
|
else:
|
|
if w == 0:
|
|
outFace = (slc, rsd)
|
|
else:
|
|
# add to VOIDS so cutter avoids area.
|
|
PathLog.warning(warnFinDep + str(fNum) + '.')
|
|
INTFCS.append((slc, rsd))
|
|
if len(INTFCS) == 0:
|
|
return (outFace, False)
|
|
else:
|
|
return (outFace, INTFCS)
|
|
|
|
def _preProcessEntireBase(self, obj, base, m):
|
|
cont = True
|
|
isHole = False
|
|
prflShp = False
|
|
# Create envelope, extract cross-section and make offset co-planar shape
|
|
# baseEnv = PathUtils.getEnvelope(base.Shape, subshape=None, depthparams=self.depthParams)
|
|
|
|
try:
|
|
baseEnv = PathUtils.getEnvelope(partshape=base.Shape, subshape=None, depthparams=self.depthParams) # Produces .Shape
|
|
except Exception as ee:
|
|
PathLog.error(str(ee))
|
|
shell = base.Shape.Shells[0]
|
|
solid = Part.makeSolid(shell)
|
|
try:
|
|
baseEnv = PathUtils.getEnvelope(partshape=solid, subshape=None, depthparams=self.depthParams) # Produces .Shape
|
|
except Exception as eee:
|
|
PathLog.error(str(eee))
|
|
cont = False
|
|
time.sleep(0.2)
|
|
|
|
if cont is True:
|
|
csFaceShape = self._getShapeSlice(baseEnv)
|
|
if csFaceShape is False:
|
|
PathLog.debug('_getShapeSlice(baseEnv) failed')
|
|
csFaceShape = self._getCrossSection(baseEnv)
|
|
if csFaceShape is False:
|
|
PathLog.debug('_getCrossSection(baseEnv) failed')
|
|
csFaceShape = self._getSliceFromEnvelope(baseEnv)
|
|
if csFaceShape is False:
|
|
PathLog.error('Failed to slice baseEnv shape.')
|
|
cont = False
|
|
|
|
if cont is True and obj.ProfileEdges != 'None':
|
|
PathLog.debug(' -Attempting profile geometry for model base.')
|
|
ofstVal = self._calculateOffsetValue(obj, isHole)
|
|
psOfst = self._extractFaceOffset(obj, csFaceShape, ofstVal)
|
|
if psOfst is not False:
|
|
if obj.ProfileEdges == 'Only':
|
|
return (True, psOfst)
|
|
prflShp = psOfst
|
|
else:
|
|
PathLog.error(' -Failed to create profile geometry.')
|
|
cont = False
|
|
|
|
if cont is True:
|
|
ofstVal = self._calculateOffsetValue(obj, isHole)
|
|
faceOffsetShape = self._extractFaceOffset(obj, csFaceShape, ofstVal)
|
|
if faceOffsetShape is False:
|
|
PathLog.error('_extractFaceOffset() failed.')
|
|
else:
|
|
faceOffsetShape.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - faceOffsetShape.BoundBox.ZMin))
|
|
return (faceOffsetShape, prflShp)
|
|
return False
|
|
|
|
def _extractWiresFromFace(self, base, fc):
|
|
'''_extractWiresFromFace(base, fc) ...
|
|
Attempts to return all closed wires within a parent face, including the outer most wire of the parent.
|
|
The wires are ordered by area. Each wire is also categorized as a pocket(False) or raised protrusion(True).
|
|
'''
|
|
PathLog.debug('_extractWiresFromFace()')
|
|
|
|
WIRES = list()
|
|
lenWrs = len(fc.Wires)
|
|
PathLog.debug(' -Wire count: {}'.format(lenWrs))
|
|
|
|
def index0(tup):
|
|
return tup[0]
|
|
|
|
# Cycle through wires in face
|
|
for w in range(0, lenWrs):
|
|
PathLog.debug(' -Analyzing wire_{}'.format(w + 1))
|
|
wire = fc.Wires[w]
|
|
checkEdges = False
|
|
cont = True
|
|
|
|
# Check for closed edges (circles, ellipses, etc...)
|
|
for E in wire.Edges:
|
|
if E.isClosed() is True:
|
|
checkEdges = True
|
|
break
|
|
|
|
if checkEdges is True:
|
|
PathLog.debug(' -checkEdges is True')
|
|
for e in range(0, len(wire.Edges)):
|
|
edge = wire.Edges[e]
|
|
if edge.isClosed() is True and edge.Mass > 0.01:
|
|
PathLog.debug(' -Found closed edge')
|
|
raised = False
|
|
ip = self._isPocket(base, fc, edge)
|
|
if ip is False:
|
|
raised = True
|
|
ebb = edge.BoundBox
|
|
eArea = ebb.XLength * ebb.YLength
|
|
F = Part.Face(Part.Wire([edge]))
|
|
WIRES.append((eArea, F.Wires[0], raised))
|
|
cont = False
|
|
|
|
if cont is True:
|
|
PathLog.debug(' -cont is True')
|
|
# If only one wire and not checkEdges, return first wire
|
|
if lenWrs == 1:
|
|
return [(wire, False)]
|
|
|
|
raised = False
|
|
wbb = wire.BoundBox
|
|
wArea = wbb.XLength * wbb.YLength
|
|
if w > 0:
|
|
ip = self._isPocket(base, fc, wire)
|
|
if ip is False:
|
|
raised = True
|
|
WIRES.append((wArea, Part.Wire(wire.Edges), raised))
|
|
|
|
nf = len(WIRES)
|
|
if nf > 0:
|
|
PathLog.debug(' -number of wires found is {}'.format(nf))
|
|
if nf == 1:
|
|
(area, W, raised) = WIRES[0]
|
|
return [(W, raised)]
|
|
else:
|
|
sortedWIRES = sorted(WIRES, key=index0, reverse=True)
|
|
return [(W, raised) for (area, W, raised) in sortedWIRES] # outer, then inner by area size
|
|
|
|
return False
|
|
|
|
def _calculateOffsetValue(self, obj, isHole, isVoid=False):
|
|
'''_calculateOffsetValue(obj, isHole, isVoid) ... internal function.
|
|
Calculate the offset for the Path.Area() function.'''
|
|
JOB = PathUtils.findParentJob(obj)
|
|
tolrnc = JOB.GeometryTolerance.Value
|
|
|
|
if isVoid is False:
|
|
if isHole is True:
|
|
offset = -1 * obj.InternalFeaturesAdjustment.Value
|
|
offset += self.radius # (self.radius + (tolrnc / 10.0))
|
|
else:
|
|
offset = -1 * obj.BoundaryAdjustment.Value
|
|
if obj.BoundaryEnforcement is True:
|
|
offset += self.radius # (self.radius + (tolrnc / 10.0))
|
|
else:
|
|
offset -= self.radius # (self.radius + (tolrnc / 10.0))
|
|
offset = 0.0 - offset
|
|
else:
|
|
offset = -1 * obj.BoundaryAdjustment.Value
|
|
offset += self.radius # (self.radius + (tolrnc / 10.0))
|
|
|
|
return offset
|
|
|
|
def _extractFaceOffset(self, obj, fcShape, offset):
|
|
'''_extractFaceOffset(fcShape, offset) ... internal function.
|
|
Original _buildPathArea() version copied from PathAreaOp.py module. This version is modified.
|
|
Adjustments made based on notes by @sliptonic at this webpage: https://github.com/sliptonic/FreeCAD/wiki/PathArea-notes.'''
|
|
PathLog.debug('_extractFaceOffset()')
|
|
|
|
if fcShape.BoundBox.ZMin != 0.0:
|
|
fcShape.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - fcShape.BoundBox.ZMin))
|
|
|
|
areaParams = {}
|
|
areaParams['Offset'] = offset
|
|
areaParams['Fill'] = 1
|
|
areaParams['Coplanar'] = 0
|
|
areaParams['SectionCount'] = 1 # -1 = full(all per depthparams??) sections
|
|
areaParams['Reorient'] = True
|
|
areaParams['OpenMode'] = 0
|
|
areaParams['MaxArcPoints'] = 400 # 400
|
|
areaParams['Project'] = True
|
|
|
|
area = Path.Area() # Create instance of Area() class object
|
|
# area.setPlane(PathUtils.makeWorkplane(fcShape)) # Set working plane
|
|
area.setPlane(PathUtils.makeWorkplane(self.wpc)) # Set working plane to normal at Z=1
|
|
area.add(fcShape)
|
|
area.setParams(**areaParams) # set parameters
|
|
|
|
# Save parameters for debugging
|
|
# obj.AreaParams = str(area.getParams())
|
|
# PathLog.debug("Area with params: {}".format(area.getParams()))
|
|
|
|
offsetShape = area.getShape()
|
|
wCnt = len(offsetShape.Wires)
|
|
if wCnt == 0:
|
|
return False
|
|
elif wCnt == 1:
|
|
ofstFace = Part.Face(offsetShape.Wires[0])
|
|
else:
|
|
W = list()
|
|
for wr in offsetShape.Wires:
|
|
W.append(Part.Face(wr))
|
|
ofstFace = Part.makeCompound(W)
|
|
|
|
return ofstFace # offsetShape
|
|
|
|
def _isPocket(self, b, f, w):
|
|
'''_isPocket(b, f, w)...
|
|
Attempts to determing if the wire(w) in face(f) of base(b) is a pocket or raised protrusion.
|
|
Returns True if pocket, False if raised protrusion.'''
|
|
e = w.Edges[0]
|
|
for fi in range(0, len(b.Shape.Faces)):
|
|
face = b.Shape.Faces[fi]
|
|
for ei in range(0, len(face.Edges)):
|
|
edge = face.Edges[ei]
|
|
if e.isSame(edge) is True:
|
|
if f is face:
|
|
# Alternative: run loop to see if all edges are same
|
|
pass # same source face, look for another
|
|
else:
|
|
if face.CenterOfMass.z < f.CenterOfMass.z:
|
|
return True
|
|
return False
|
|
|
|
def _flattenWireToFace(self, wire):
|
|
PathLog.debug('_flattenWireToFace()')
|
|
if wire.isClosed() is False:
|
|
PathLog.debug(' -wire.isClosed() is False')
|
|
return False
|
|
|
|
# If wire is planar horizontal, convert to a face and return
|
|
if wire.BoundBox.ZLength == 0.0:
|
|
slc = Part.Face(wire)
|
|
return slc
|
|
|
|
# Attempt to create a new wire for manipulation, if not, use original
|
|
newWire = Part.Wire(wire.Edges)
|
|
if newWire.isClosed() is True:
|
|
nWire = newWire
|
|
else:
|
|
PathLog.debug(' -newWire.isClosed() is False')
|
|
nWire = wire
|
|
|
|
# Attempt extrusion, and then try a manual slice and then cross-section
|
|
ext = self._getExtrudedShape(nWire)
|
|
if ext is False:
|
|
PathLog.debug('_getExtrudedShape() failed')
|
|
else:
|
|
slc = self._getShapeSlice(ext)
|
|
if slc is not False:
|
|
return slc
|
|
cs = self._getCrossSection(ext, True)
|
|
if cs is not False:
|
|
return cs
|
|
|
|
# Attempt creating an envelope, and then try a manual slice and then cross-section
|
|
env = self._getShapeEnvelope(nWire)
|
|
if env is False:
|
|
PathLog.debug('_getShapeEnvelope() failed')
|
|
else:
|
|
slc = self._getShapeSlice(env)
|
|
if slc is not False:
|
|
return slc
|
|
cs = self._getCrossSection(env, True)
|
|
if cs is not False:
|
|
return cs
|
|
|
|
# Attempt creating a projection
|
|
slc = self._getProjectedFace(nWire)
|
|
if slc is False:
|
|
PathLog.debug('_getProjectedFace() failed')
|
|
else:
|
|
return slc
|
|
|
|
return False
|
|
|
|
def _getExtrudedShape(self, wire):
|
|
PathLog.debug('_getExtrudedShape()')
|
|
wBB = wire.BoundBox
|
|
extFwd = math.floor(2.0 * wBB.ZLength) + 10.0
|
|
|
|
try:
|
|
# slower, but renders collective faces correctly. Method 5 in TESTING
|
|
shell = wire.extrude(FreeCAD.Vector(0.0, 0.0, extFwd))
|
|
except Exception as ee:
|
|
PathLog.error(' -extrude wire failed: \n{}'.format(ee))
|
|
return False
|
|
|
|
SHP = Part.makeSolid(shell)
|
|
return SHP
|
|
|
|
def _getShapeSlice(self, shape):
|
|
PathLog.debug('_getShapeSlice()')
|
|
|
|
bb = shape.BoundBox
|
|
mid = (bb.ZMin + bb.ZMax) / 2.0
|
|
xmin = bb.XMin - 1.0
|
|
xmax = bb.XMax + 1.0
|
|
ymin = bb.YMin - 1.0
|
|
ymax = bb.YMax + 1.0
|
|
p1 = FreeCAD.Vector(xmin, ymin, mid)
|
|
p2 = FreeCAD.Vector(xmax, ymin, mid)
|
|
p3 = FreeCAD.Vector(xmax, ymax, mid)
|
|
p4 = FreeCAD.Vector(xmin, ymax, mid)
|
|
|
|
e1 = Part.makeLine(p1, p2)
|
|
e2 = Part.makeLine(p2, p3)
|
|
e3 = Part.makeLine(p3, p4)
|
|
e4 = Part.makeLine(p4, p1)
|
|
face = Part.Face(Part.Wire([e1, e2, e3, e4]))
|
|
fArea = face.BoundBox.XLength * face.BoundBox.YLength # face.Wires[0].Area
|
|
sArea = shape.BoundBox.XLength * shape.BoundBox.YLength
|
|
midArea = (fArea + sArea) / 2.0
|
|
|
|
slcShp = shape.common(face)
|
|
slcArea = slcShp.BoundBox.XLength * slcShp.BoundBox.YLength
|
|
|
|
if slcArea < midArea:
|
|
for W in slcShp.Wires:
|
|
if W.isClosed() is False:
|
|
PathLog.debug(' -wire.isClosed() is False')
|
|
return False
|
|
if len(slcShp.Wires) == 1:
|
|
wire = slcShp.Wires[0]
|
|
slc = Part.Face(wire)
|
|
slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin))
|
|
return slc
|
|
else:
|
|
fL = list()
|
|
for W in slcShp.Wires:
|
|
slc = Part.Face(W)
|
|
slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin))
|
|
fL.append(slc)
|
|
comp = Part.makeCompound(fL)
|
|
if self.showDebugObjects is True:
|
|
PathLog.debug('*** tmpSliceCompound')
|
|
P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpSliceCompound')
|
|
P.Shape = comp
|
|
# P.recompute()
|
|
P.purgeTouched()
|
|
self.tempGroup.addObject(P)
|
|
return comp
|
|
|
|
PathLog.debug(' -slcArea !< midArea')
|
|
PathLog.debug(' -slcShp.Edges count: {}. Might be a vertically oriented face.'.format(len(slcShp.Edges)))
|
|
return False
|
|
|
|
def _getProjectedFace(self, wire):
|
|
PathLog.debug('_getProjectedFace()')
|
|
F = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpProjectionWire')
|
|
F.Shape = wire
|
|
F.purgeTouched()
|
|
self.tempGroup.addObject(F)
|
|
try:
|
|
prj = Draft.makeShape2DView(F, FreeCAD.Vector(0, 0, 1))
|
|
prj.recompute()
|
|
prj.purgeTouched()
|
|
self.tempGroup.addObject(prj)
|
|
except Exception as ee:
|
|
PathLog.error(str(ee))
|
|
return False
|
|
else:
|
|
pWire = Part.Wire(prj.Shape.Edges)
|
|
if pWire.isClosed() is False:
|
|
# PathLog.debug(' -pWire.isClosed() is False')
|
|
return False
|
|
slc = Part.Face(pWire)
|
|
slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin))
|
|
return slc
|
|
return False
|
|
|
|
def _getCrossSection(self, shape, withExtrude=False):
|
|
PathLog.debug('_getCrossSection()')
|
|
wires = list()
|
|
bb = shape.BoundBox
|
|
mid = (bb.ZMin + bb.ZMax) / 2.0
|
|
|
|
for i in shape.slice(FreeCAD.Vector(0, 0, 1), mid):
|
|
wires.append(i)
|
|
|
|
if len(wires) > 0:
|
|
comp = Part.Compound(wires) # produces correct cross-section wire !
|
|
comp.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - comp.BoundBox.ZMin))
|
|
csWire = comp.Wires[0]
|
|
if csWire.isClosed() is False:
|
|
PathLog.debug(' -comp.Wires[0] is not closed')
|
|
return False
|
|
if withExtrude is True:
|
|
ext = self._getExtrudedShape(csWire)
|
|
CS = self._getShapeSlice(ext)
|
|
else:
|
|
CS = Part.Face(csWire)
|
|
CS.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - CS.BoundBox.ZMin))
|
|
return CS
|
|
else:
|
|
PathLog.debug(' -No wires from .slice() method')
|
|
|
|
return False
|
|
|
|
def _getShapeEnvelope(self, shape):
|
|
PathLog.debug('_getShapeEnvelope()')
|
|
|
|
wBB = shape.BoundBox
|
|
extFwd = wBB.ZLength + 10.0
|
|
minz = wBB.ZMin
|
|
maxz = wBB.ZMin + extFwd
|
|
stpDwn = (maxz - minz) / 4.0
|
|
dep_par = PathUtils.depth_params(maxz + 5.0, maxz + 3.0, maxz, stpDwn, 0.0, minz)
|
|
|
|
try:
|
|
env = PathUtils.getEnvelope(partshape=shape, depthparams=dep_par) # Produces .Shape
|
|
except Exception as ee:
|
|
PathLog.error('try: PathUtils.getEnvelope() failed.\n' + str(ee))
|
|
return False
|
|
else:
|
|
return env
|
|
|
|
return False
|
|
|
|
def _getSliceFromEnvelope(self, env):
|
|
PathLog.debug('_getSliceFromEnvelope()')
|
|
eBB = env.BoundBox
|
|
extFwd = eBB.ZLength + 10.0
|
|
maxz = eBB.ZMin + extFwd
|
|
|
|
maxMax = env.Edges[0].BoundBox.ZMin
|
|
emax = math.floor(maxz - 1.0)
|
|
E = list()
|
|
for e in range(0, len(env.Edges)):
|
|
emin = env.Edges[e].BoundBox.ZMin
|
|
if emin > emax:
|
|
E.append(env.Edges[e])
|
|
tf = Part.Face(Part.Wire(Part.__sortEdges__(E)))
|
|
tf.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - tf.BoundBox.ZMin))
|
|
|
|
return tf
|
|
|
|
def _prepareModelSTLs(self, JOB, obj):
|
|
PathLog.debug('_prepareModelSTLs()')
|
|
for m in range(0, len(JOB.Model.Group)):
|
|
M = JOB.Model.Group[m]
|
|
|
|
# PathLog.debug(f" -self.modelTypes[{m}] == 'M'")
|
|
if self.modelTypes[m] == 'M':
|
|
mesh = M.Mesh
|
|
else:
|
|
# base.Shape.tessellate(0.05) # 0.5 original value
|
|
# mesh = MeshPart.meshFromShape(base.Shape, Deflection=self.deflection)
|
|
mesh = MeshPart.meshFromShape(Shape=M.Shape, LinearDeflection=self.deflection, AngularDeflection=self.angularDeflection, Relative=False)
|
|
|
|
if self.modelSTLs[m] is True:
|
|
stl = ocl.STLSurf()
|
|
|
|
for f in mesh.Facets:
|
|
p = f.Points[0]
|
|
q = f.Points[1]
|
|
r = f.Points[2]
|
|
t = ocl.Triangle(ocl.Point(p[0], p[1], p[2] + obj.DepthOffset.Value),
|
|
ocl.Point(q[0], q[1], q[2] + obj.DepthOffset.Value),
|
|
ocl.Point(r[0], r[1], r[2] + obj.DepthOffset.Value))
|
|
stl.addTriangle(t)
|
|
self.modelSTLs[m] = stl
|
|
return
|
|
|
|
def _makeSafeSTL(self, JOB, obj, mdlIdx, faceShapes, voidShapes):
|
|
'''_makeSafeSTL(JOB, obj, mdlIdx, faceShapes, voidShapes)...
|
|
Creates and OCL.stl object with combined data with waste stock,
|
|
model, and avoided faces. Travel lines can be checked against this
|
|
STL object to determine minimum travel height to clear stock and model.'''
|
|
PathLog.debug('_makeSafeSTL()')
|
|
|
|
fuseShapes = list()
|
|
Mdl = JOB.Model.Group[mdlIdx]
|
|
FCAD = FreeCAD.ActiveDocument
|
|
mBB = Mdl.Shape.BoundBox
|
|
sBB = JOB.Stock.Shape.BoundBox
|
|
|
|
# add Model shape to safeSTL shape
|
|
fuseShapes.append(Mdl.Shape)
|
|
|
|
if obj.BoundBox == 'BaseBoundBox':
|
|
cont = False
|
|
extFwd = (sBB.ZLength)
|
|
zmin = mBB.ZMin
|
|
zmax = mBB.ZMin + extFwd
|
|
stpDwn = (zmax - zmin) / 4.0
|
|
dep_par = PathUtils.depth_params(zmax + 5.0, zmax + 3.0, zmax, stpDwn, 0.0, zmin)
|
|
|
|
try:
|
|
envBB = PathUtils.getEnvelope(partshape=Mdl.Shape, depthparams=dep_par) # Produces .Shape
|
|
cont = True
|
|
except Exception as ee:
|
|
PathLog.error(str(ee))
|
|
shell = Mdl.Shape.Shells[0]
|
|
solid = Part.makeSolid(shell)
|
|
try:
|
|
envBB = PathUtils.getEnvelope(partshape=solid, depthparams=dep_par) # Produces .Shape
|
|
cont = True
|
|
except Exception as eee:
|
|
PathLog.error(str(eee))
|
|
|
|
if cont is True:
|
|
stckWst = JOB.Stock.Shape.cut(envBB)
|
|
if obj.BoundaryAdjustment > 0.0:
|
|
cmpndFS = Part.makeCompound(faceShapes)
|
|
baBB = PathUtils.getEnvelope(partshape=cmpndFS, depthparams=self.depthParams) # Produces .Shape
|
|
adjStckWst = stckWst.cut(baBB)
|
|
else:
|
|
adjStckWst = stckWst
|
|
fuseShapes.append(adjStckWst)
|
|
else:
|
|
PathLog.warning('Path transitions might not avoid the model. Verify paths.')
|
|
time.sleep(0.3)
|
|
|
|
else:
|
|
# If boundbox is Job.Stock, add hidden pad under stock as base plate
|
|
toolDiam = self.cutter.getDiameter()
|
|
zMin = JOB.Stock.Shape.BoundBox.ZMin
|
|
xMin = JOB.Stock.Shape.BoundBox.XMin - toolDiam
|
|
yMin = JOB.Stock.Shape.BoundBox.YMin - toolDiam
|
|
bL = JOB.Stock.Shape.BoundBox.XLength + (2 * toolDiam)
|
|
bW = JOB.Stock.Shape.BoundBox.YLength + (2 * toolDiam)
|
|
bH = 1.0
|
|
crnr = FreeCAD.Vector(xMin, yMin, zMin - 1.0)
|
|
B = Part.makeBox(bL, bW, bH, crnr, FreeCAD.Vector(0, 0, 1))
|
|
fuseShapes.append(B)
|
|
|
|
if voidShapes is not False:
|
|
voidComp = Part.makeCompound(voidShapes)
|
|
voidEnv = PathUtils.getEnvelope(partshape=voidComp, depthparams=self.depthParams) # Produces .Shape
|
|
fuseShapes.append(voidEnv)
|
|
|
|
f0 = fuseShapes.pop(0)
|
|
if len(fuseShapes) > 0:
|
|
fused = f0.fuse(fuseShapes)
|
|
else:
|
|
fused = f0
|
|
|
|
if self.showDebugObjects is True:
|
|
T = FreeCAD.ActiveDocument.addObject('Part::Feature', 'safeSTLShape')
|
|
T.Shape = fused
|
|
T.purgeTouched()
|
|
self.tempGroup.addObject(T)
|
|
|
|
# Extract mesh from fusion
|
|
meshFuse = MeshPart.meshFromShape(Shape=fused, LinearDeflection=(self.deflection / 2.0), AngularDeflection=self.angularDeflection, Relative=False)
|
|
time.sleep(0.2)
|
|
stl = ocl.STLSurf()
|
|
for f in meshFuse.Facets:
|
|
p = f.Points[0]
|
|
q = f.Points[1]
|
|
r = f.Points[2]
|
|
t = ocl.Triangle(ocl.Point(p[0], p[1], p[2]),
|
|
ocl.Point(q[0], q[1], q[2]),
|
|
ocl.Point(r[0], r[1], r[2]))
|
|
stl.addTriangle(t)
|
|
|
|
self.safeSTLs[mdlIdx] = stl
|
|
|
|
def _processCutAreas(self, JOB, obj, mdlIdx, FCS, VDS):
|
|
'''_processCutAreas(JOB, obj, mdlIdx, FCS, VDS)...
|
|
This method applies any avoided faces or regions to the selected faces.
|
|
It then calls the correct scan method depending on the ScanType property.'''
|
|
PathLog.debug('_processCutAreas()')
|
|
|
|
final = list()
|
|
base = JOB.Model.Group[mdlIdx]
|
|
|
|
# Process faces Collectively or Individually
|
|
if obj.HandleMultipleFeatures == 'Collectively':
|
|
if FCS is True:
|
|
COMP = False
|
|
else:
|
|
ADD = Part.makeCompound(FCS)
|
|
if VDS is not False:
|
|
DEL = Part.makeCompound(VDS)
|
|
COMP = ADD.cut(DEL)
|
|
else:
|
|
COMP = ADD
|
|
|
|
final.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid}))
|
|
final.extend(self._waterlineOp(JOB, obj, mdlIdx, COMP)) # independent method set for Waterline
|
|
|
|
elif obj.HandleMultipleFeatures == 'Individually':
|
|
for fsi in range(0, len(FCS)):
|
|
fShp = FCS[fsi]
|
|
# self.deleteOpVariables(all=False)
|
|
self.resetOpVariables(all=False)
|
|
|
|
if fShp is True:
|
|
COMP = False
|
|
else:
|
|
ADD = Part.makeCompound([fShp])
|
|
if VDS is not False:
|
|
DEL = Part.makeCompound(VDS)
|
|
COMP = ADD.cut(DEL)
|
|
else:
|
|
COMP = ADD
|
|
|
|
final.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid}))
|
|
final.extend(self._waterlineOp(JOB, obj, mdlIdx, COMP)) # independent method set for Waterline
|
|
COMP = None
|
|
# Eif
|
|
|
|
return final
|
|
|
|
def _planarGetPDC(self, stl, finalDep, SampleInterval, useSafeCutter=False):
|
|
pdc = ocl.PathDropCutter() # create a pdc [PathDropCutter] object
|
|
pdc.setSTL(stl) # add stl model
|
|
if useSafeCutter is True:
|
|
pdc.setCutter(self.safeCutter) # add safeCutter
|
|
else:
|
|
pdc.setCutter(self.cutter) # add cutter
|
|
pdc.setZ(finalDep) # set minimumZ (final / target depth value)
|
|
pdc.setSampling(SampleInterval) # set sampling size
|
|
return pdc
|
|
|
|
# Main waterline functions
|
|
def _waterlineOp(self, JOB, obj, mdlIdx, subShp=None):
|
|
'''_waterlineOp(obj, base) ... Main waterline function to perform waterline extraction from model.'''
|
|
commands = []
|
|
|
|
t_begin = time.time()
|
|
# JOB = PathUtils.findParentJob(obj)
|
|
base = JOB.Model.Group[mdlIdx]
|
|
bb = self.boundBoxes[mdlIdx]
|
|
stl = self.modelSTLs[mdlIdx]
|
|
|
|
# Prepare global holdpoint and layerEndPnt containers
|
|
if self.holdPoint is None:
|
|
self.holdPoint = ocl.Point(float("inf"), float("inf"), float("inf"))
|
|
if self.layerEndPnt is None:
|
|
self.layerEndPnt = ocl.Point(float("inf"), float("inf"), float("inf"))
|
|
|
|
# Set extra offset to diameter of cutter to allow cutter to move around perimeter of model
|
|
toolDiam = self.cutter.getDiameter()
|
|
cdeoX = 0.6 * toolDiam
|
|
cdeoY = 0.6 * toolDiam
|
|
|
|
if subShp is None:
|
|
# Get correct boundbox
|
|
if obj.BoundBox == 'Stock':
|
|
BS = JOB.Stock
|
|
bb = BS.Shape.BoundBox
|
|
elif obj.BoundBox == 'BaseBoundBox':
|
|
BS = base
|
|
bb = base.Shape.BoundBox
|
|
|
|
env = PathUtils.getEnvelope(partshape=BS.Shape, depthparams=self.depthParams) # Produces .Shape
|
|
|
|
xmin = bb.XMin
|
|
xmax = bb.XMax
|
|
ymin = bb.YMin
|
|
ymax = bb.YMax
|
|
zmin = bb.ZMin
|
|
zmax = bb.ZMax
|
|
else:
|
|
xmin = subShp.BoundBox.XMin
|
|
xmax = subShp.BoundBox.XMax
|
|
ymin = subShp.BoundBox.YMin
|
|
ymax = subShp.BoundBox.YMax
|
|
zmin = subShp.BoundBox.ZMin
|
|
zmax = subShp.BoundBox.ZMax
|
|
|
|
smplInt = obj.SampleInterval.Value
|
|
minSampInt = 0.001 # value is mm
|
|
if smplInt < minSampInt:
|
|
smplInt = minSampInt
|
|
|
|
# Determine bounding box length for the OCL scan
|
|
bbLength = math.fabs(ymax - ymin)
|
|
numScanLines = int(math.ceil(bbLength / smplInt) + 1) # Number of lines
|
|
|
|
# Compute number and size of stepdowns, and final depth
|
|
if obj.LayerMode == 'Single-pass':
|
|
depthparams = [obj.FinalDepth.Value]
|
|
else:
|
|
depthparams = [dp for dp in self.depthParams]
|
|
lenDP = len(depthparams)
|
|
|
|
# Prepare PathDropCutter objects with STL data
|
|
safePDC = self._planarGetPDC(self.safeSTLs[mdlIdx],
|
|
depthparams[lenDP - 1], obj.SampleInterval.Value, useSafeCutter=False)
|
|
|
|
# Scan the piece to depth at smplInt
|
|
oclScan = []
|
|
oclScan = self._waterlineDropCutScan(stl, smplInt, xmin, xmax, ymin, depthparams[lenDP - 1], numScanLines)
|
|
# oclScan = SCANS
|
|
lenOS = len(oclScan)
|
|
ptPrLn = int(lenOS / numScanLines)
|
|
|
|
# Convert oclScan list of points to multi-dimensional list
|
|
scanLines = []
|
|
for L in range(0, numScanLines):
|
|
scanLines.append([])
|
|
for P in range(0, ptPrLn):
|
|
pi = L * ptPrLn + P
|
|
scanLines[L].append(oclScan[pi])
|
|
lenSL = len(scanLines)
|
|
pntsPerLine = len(scanLines[0])
|
|
PathLog.debug("--OCL scan: " + str(lenSL * pntsPerLine) + " points, with " + str(numScanLines) + " lines and " + str(pntsPerLine) + " pts/line")
|
|
|
|
# Extract Wl layers per depthparams
|
|
lyr = 0
|
|
cmds = []
|
|
layTime = time.time()
|
|
self.topoMap = []
|
|
for layDep in depthparams:
|
|
cmds = self._getWaterline(obj, scanLines, layDep, lyr, lenSL, pntsPerLine)
|
|
commands.extend(cmds)
|
|
lyr += 1
|
|
PathLog.debug("--All layer scans combined took " + str(time.time() - layTime) + " s")
|
|
return commands
|
|
|
|
def _waterlineDropCutScan(self, stl, smplInt, xmin, xmax, ymin, fd, numScanLines):
|
|
'''_waterlineDropCutScan(stl, smplInt, xmin, xmax, ymin, fd, numScanLines) ...
|
|
Perform OCL scan for waterline purpose.'''
|
|
pdc = ocl.PathDropCutter() # create a pdc
|
|
pdc.setSTL(stl)
|
|
pdc.setCutter(self.cutter)
|
|
pdc.setZ(fd) # set minimumZ (final / target depth value)
|
|
pdc.setSampling(smplInt)
|
|
|
|
# Create line object as path
|
|
path = ocl.Path() # create an empty path object
|
|
for nSL in range(0, numScanLines):
|
|
yVal = ymin + (nSL * smplInt)
|
|
p1 = ocl.Point(xmin, yVal, fd) # start-point of line
|
|
p2 = ocl.Point(xmax, yVal, fd) # end-point of line
|
|
path.append(ocl.Line(p1, p2))
|
|
# path.append(l) # add the line to the path
|
|
pdc.setPath(path)
|
|
pdc.run() # run drop-cutter on the path
|
|
|
|
# return the list the points
|
|
return pdc.getCLPoints()
|
|
|
|
def _getWaterline(self, obj, scanLines, layDep, lyr, lenSL, pntsPerLine):
|
|
'''_getWaterline(obj, scanLines, layDep, lyr, lenSL, pntsPerLine) ... Get waterline.'''
|
|
commands = []
|
|
cmds = []
|
|
loopList = []
|
|
self.topoMap = []
|
|
# Create topo map from scanLines (highs and lows)
|
|
self.topoMap = self._createTopoMap(scanLines, layDep, lenSL, pntsPerLine)
|
|
# Add buffer lines and columns to topo map
|
|
self._bufferTopoMap(lenSL, pntsPerLine)
|
|
# Identify layer waterline from OCL scan
|
|
self._highlightWaterline(4, 9)
|
|
# Extract waterline and convert to gcode
|
|
loopList = self._extractWaterlines(obj, scanLines, lyr, layDep)
|
|
# save commands
|
|
for loop in loopList:
|
|
cmds = self._loopToGcode(obj, layDep, loop)
|
|
commands.extend(cmds)
|
|
return commands
|
|
|
|
def _createTopoMap(self, scanLines, layDep, lenSL, pntsPerLine):
|
|
'''_createTopoMap(scanLines, layDep, lenSL, pntsPerLine) ... Create topo map version of OCL scan data.'''
|
|
topoMap = []
|
|
for L in range(0, lenSL):
|
|
topoMap.append([])
|
|
for P in range(0, pntsPerLine):
|
|
if scanLines[L][P].z > layDep:
|
|
topoMap[L].append(2)
|
|
else:
|
|
topoMap[L].append(0)
|
|
return topoMap
|
|
|
|
def _bufferTopoMap(self, lenSL, pntsPerLine):
|
|
'''_bufferTopoMap(lenSL, pntsPerLine) ... Add buffer boarder of zeros to all sides to topoMap data.'''
|
|
pre = [0, 0]
|
|
post = [0, 0]
|
|
for p in range(0, pntsPerLine):
|
|
pre.append(0)
|
|
post.append(0)
|
|
for l in range(0, lenSL):
|
|
self.topoMap[l].insert(0, 0)
|
|
self.topoMap[l].append(0)
|
|
self.topoMap.insert(0, pre)
|
|
self.topoMap.append(post)
|
|
return True
|
|
|
|
def _highlightWaterline(self, extraMaterial, insCorn):
|
|
'''_highlightWaterline(extraMaterial, insCorn) ... Highlight the waterline data, separating from extra material.'''
|
|
TM = self.topoMap
|
|
lastPnt = len(TM[1]) - 1
|
|
lastLn = len(TM) - 1
|
|
highFlag = 0
|
|
|
|
# ("--Convert parallel data to ridges")
|
|
for lin in range(1, lastLn):
|
|
for pt in range(1, lastPnt): # Ignore first and last points
|
|
if TM[lin][pt] == 0:
|
|
if TM[lin][pt + 1] == 2: # step up
|
|
TM[lin][pt] = 1
|
|
if TM[lin][pt - 1] == 2: # step down
|
|
TM[lin][pt] = 1
|
|
|
|
# ("--Convert perpendicular data to ridges and highlight ridges")
|
|
for pt in range(1, lastPnt): # Ignore first and last points
|
|
for lin in range(1, lastLn):
|
|
if TM[lin][pt] == 0:
|
|
highFlag = 0
|
|
if TM[lin + 1][pt] == 2: # step up
|
|
TM[lin][pt] = 1
|
|
if TM[lin - 1][pt] == 2: # step down
|
|
TM[lin][pt] = 1
|
|
elif TM[lin][pt] == 2:
|
|
highFlag += 1
|
|
if highFlag == 3:
|
|
if TM[lin - 1][pt - 1] < 2 or TM[lin - 1][pt + 1] < 2:
|
|
highFlag = 2
|
|
else:
|
|
TM[lin - 1][pt] = extraMaterial
|
|
highFlag = 2
|
|
|
|
# ("--Square corners")
|
|
for pt in range(1, lastPnt):
|
|
for lin in range(1, lastLn):
|
|
if TM[lin][pt] == 1: # point == 1
|
|
cont = True
|
|
if TM[lin + 1][pt] == 0: # forward == 0
|
|
if TM[lin + 1][pt - 1] == 1: # forward left == 1
|
|
if TM[lin][pt - 1] == 2: # left == 2
|
|
TM[lin + 1][pt] = 1 # square the corner
|
|
cont = False
|
|
|
|
if cont is True and TM[lin + 1][pt + 1] == 1: # forward right == 1
|
|
if TM[lin][pt + 1] == 2: # right == 2
|
|
TM[lin + 1][pt] = 1 # square the corner
|
|
cont = True
|
|
|
|
if TM[lin - 1][pt] == 0: # back == 0
|
|
if TM[lin - 1][pt - 1] == 1: # back left == 1
|
|
if TM[lin][pt - 1] == 2: # left == 2
|
|
TM[lin - 1][pt] = 1 # square the corner
|
|
cont = False
|
|
|
|
if cont is True and TM[lin - 1][pt + 1] == 1: # back right == 1
|
|
if TM[lin][pt + 1] == 2: # right == 2
|
|
TM[lin - 1][pt] = 1 # square the corner
|
|
|
|
# remove inside corners
|
|
for pt in range(1, lastPnt):
|
|
for lin in range(1, lastLn):
|
|
if TM[lin][pt] == 1: # point == 1
|
|
if TM[lin][pt + 1] == 1:
|
|
if TM[lin - 1][pt + 1] == 1 or TM[lin + 1][pt + 1] == 1:
|
|
TM[lin][pt + 1] = insCorn
|
|
elif TM[lin][pt - 1] == 1:
|
|
if TM[lin - 1][pt - 1] == 1 or TM[lin + 1][pt - 1] == 1:
|
|
TM[lin][pt - 1] = insCorn
|
|
|
|
return True
|
|
|
|
def _extractWaterlines(self, obj, oclScan, lyr, layDep):
|
|
'''_extractWaterlines(obj, oclScan, lyr, layDep) ... Extract water lines from OCL scan data.'''
|
|
srch = True
|
|
lastPnt = len(self.topoMap[0]) - 1
|
|
lastLn = len(self.topoMap) - 1
|
|
maxSrchs = 5
|
|
srchCnt = 1
|
|
loopList = []
|
|
loop = []
|
|
loopNum = 0
|
|
|
|
if self.CutClimb is True:
|
|
lC = [-1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0]
|
|
pC = [-1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1]
|
|
else:
|
|
lC = [1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0]
|
|
pC = [-1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1]
|
|
|
|
while srch is True:
|
|
srch = False
|
|
if srchCnt > maxSrchs:
|
|
PathLog.debug("Max search scans, " + str(maxSrchs) + " reached\nPossible incomplete waterline result!")
|
|
break
|
|
for L in range(1, lastLn):
|
|
for P in range(1, lastPnt):
|
|
if self.topoMap[L][P] == 1:
|
|
# start loop follow
|
|
srch = True
|
|
loopNum += 1
|
|
loop = self._trackLoop(oclScan, lC, pC, L, P, loopNum)
|
|
self.topoMap[L][P] = 0 # Mute the starting point
|
|
loopList.append(loop)
|
|
srchCnt += 1
|
|
PathLog.debug("Search count for layer " + str(lyr) + " is " + str(srchCnt) + ", with " + str(loopNum) + " loops.")
|
|
return loopList
|
|
|
|
def _trackLoop(self, oclScan, lC, pC, L, P, loopNum):
|
|
'''_trackLoop(oclScan, lC, pC, L, P, loopNum) ... Track the loop direction.'''
|
|
loop = [oclScan[L - 1][P - 1]] # Start loop point list
|
|
cur = [L, P, 1]
|
|
prv = [L, P - 1, 1]
|
|
nxt = [L, P + 1, 1]
|
|
follow = True
|
|
ptc = 0
|
|
ptLmt = 200000
|
|
while follow is True:
|
|
ptc += 1
|
|
if ptc > ptLmt:
|
|
PathLog.debug("Loop number " + str(loopNum) + " at [" + str(nxt[0]) + ", " + str(nxt[1]) + "] pnt count exceeds, " + str(ptLmt) + ". Stopped following loop.")
|
|
break
|
|
nxt = self._findNextWlPoint(lC, pC, cur[0], cur[1], prv[0], prv[1]) # get next point
|
|
loop.append(oclScan[nxt[0] - 1][nxt[1] - 1]) # add it to loop point list
|
|
self.topoMap[nxt[0]][nxt[1]] = nxt[2] # Mute the point, if not Y stem
|
|
if nxt[0] == L and nxt[1] == P: # check if loop complete
|
|
follow = False
|
|
elif nxt[0] == cur[0] and nxt[1] == cur[1]: # check if line cannot be detected
|
|
follow = False
|
|
prv = cur
|
|
cur = nxt
|
|
return loop
|
|
|
|
def _findNextWlPoint(self, lC, pC, cl, cp, pl, pp):
|
|
'''_findNextWlPoint(lC, pC, cl, cp, pl, pp) ...
|
|
Find the next waterline point in the point cloud layer provided.'''
|
|
dl = cl - pl
|
|
dp = cp - pp
|
|
num = 0
|
|
i = 3
|
|
s = 0
|
|
mtch = 0
|
|
found = False
|
|
while mtch < 8: # check all 8 points around current point
|
|
if lC[i] == dl:
|
|
if pC[i] == dp:
|
|
s = i - 3
|
|
found = True
|
|
# Check for y branch where current point is connection between branches
|
|
for y in range(1, mtch):
|
|
if lC[i + y] == dl:
|
|
if pC[i + y] == dp:
|
|
num = 1
|
|
break
|
|
break
|
|
i += 1
|
|
mtch += 1
|
|
if found is False:
|
|
# ("_findNext: No start point found.")
|
|
return [cl, cp, num]
|
|
|
|
for r in range(0, 8):
|
|
l = cl + lC[s + r]
|
|
p = cp + pC[s + r]
|
|
if self.topoMap[l][p] == 1:
|
|
return [l, p, num]
|
|
|
|
# ("_findNext: No next pnt found")
|
|
return [cl, cp, num]
|
|
|
|
def _loopToGcode(self, obj, layDep, loop):
|
|
'''_loopToGcode(obj, layDep, loop) ... Convert set of loop points to Gcode.'''
|
|
# generate the path commands
|
|
output = []
|
|
optimize = obj.OptimizeLinearPaths
|
|
|
|
prev = ocl.Point(float("inf"), float("inf"), float("inf"))
|
|
nxt = ocl.Point(float("inf"), float("inf"), float("inf"))
|
|
pnt = ocl.Point(float("inf"), float("inf"), float("inf"))
|
|
|
|
# Create first point
|
|
pnt.x = loop[0].x
|
|
pnt.y = loop[0].y
|
|
pnt.z = layDep
|
|
|
|
# Position cutter to begin loop
|
|
output.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid}))
|
|
output.append(Path.Command('G0', {'X': pnt.x, 'Y': pnt.y, 'F': self.horizRapid}))
|
|
output.append(Path.Command('G1', {'Z': pnt.z, 'F': self.vertFeed}))
|
|
|
|
lenCLP = len(loop)
|
|
lastIdx = lenCLP - 1
|
|
# Cycle through each point on loop
|
|
for i in range(0, lenCLP):
|
|
if i < lastIdx:
|
|
nxt.x = loop[i + 1].x
|
|
nxt.y = loop[i + 1].y
|
|
nxt.z = layDep
|
|
else:
|
|
optimize = False
|
|
|
|
if not optimize or not self.isPointOnLine(FreeCAD.Vector(prev.x, prev.y, prev.z), FreeCAD.Vector(nxt.x, nxt.y, nxt.z), FreeCAD.Vector(pnt.x, pnt.y, pnt.z)):
|
|
output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'F': self.horizFeed}))
|
|
|
|
# Rotate point data
|
|
prev.x = pnt.x
|
|
prev.y = pnt.y
|
|
prev.z = pnt.z
|
|
pnt.x = nxt.x
|
|
pnt.y = nxt.y
|
|
pnt.z = nxt.z
|
|
|
|
# Save layer end point for use in transitioning to next layer
|
|
self.layerEndPnt.x = pnt.x
|
|
self.layerEndPnt.y = pnt.y
|
|
self.layerEndPnt.z = pnt.z
|
|
|
|
return output
|
|
|
|
# Support functions for both dropcutter and waterline operations
|
|
def isPointOnLine(self, strtPnt, endPnt, pointP):
|
|
'''isPointOnLine(strtPnt, endPnt, pointP) ... Determine if a given point is on the line defined by start and end points.'''
|
|
tolerance = 1e-6
|
|
vectorAB = endPnt - strtPnt
|
|
vectorAC = pointP - strtPnt
|
|
crossproduct = vectorAB.cross(vectorAC)
|
|
dotproduct = vectorAB.dot(vectorAC)
|
|
|
|
if crossproduct.Length > tolerance:
|
|
return False
|
|
|
|
if dotproduct < 0:
|
|
return False
|
|
|
|
if dotproduct > vectorAB.Length * vectorAB.Length:
|
|
return False
|
|
|
|
return True
|
|
|
|
def holdStopCmds(self, obj, zMax, pd, p2, txt):
|
|
'''holdStopCmds(obj, zMax, pd, p2, txt) ... Gcode commands to be executed at beginning of hold.'''
|
|
cmds = []
|
|
msg = 'N (' + txt + ')'
|
|
cmds.append(Path.Command(msg, {})) # Raise cutter rapid to zMax in line of travel
|
|
cmds.append(Path.Command('G0', {'Z': zMax, 'F': self.vertRapid})) # Raise cutter rapid to zMax in line of travel
|
|
cmds.append(Path.Command('G0', {'X': p2.x, 'Y': p2.y, 'F': self.horizRapid})) # horizontal rapid to current XY coordinate
|
|
if zMax != pd:
|
|
cmds.append(Path.Command('G0', {'Z': pd, 'F': self.vertRapid})) # drop cutter down rapidly to prevDepth depth
|
|
cmds.append(Path.Command('G0', {'Z': p2.z, 'F': self.vertFeed})) # drop cutter down to current Z depth, returning to normal cut path and speed
|
|
return cmds
|
|
|
|
def holdStopEndCmds(self, obj, p2, txt):
|
|
'''holdStopEndCmds(obj, p2, txt) ... Gcode commands to be executed at end of hold stop.'''
|
|
cmds = []
|
|
msg = 'N (' + txt + ')'
|
|
cmds.append(Path.Command(msg, {})) # Raise cutter rapid to zMax in line of travel
|
|
cmds.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})) # Raise cutter rapid to zMax in line of travel
|
|
# cmds.append(Path.Command('G0', {'X': p2.x, 'Y': p2.y, 'F': self.horizRapid})) # horizontal rapid to current XY coordinate
|
|
return cmds
|
|
|
|
def subsectionCLP(self, CLP, xmin, ymin, xmax, ymax):
|
|
'''subsectionCLP(CLP, xmin, ymin, xmax, ymax) ...
|
|
This function returns a subsection of the CLP scan, limited to the min/max values supplied.'''
|
|
section = list()
|
|
lenCLP = len(CLP)
|
|
for i in range(0, lenCLP):
|
|
if CLP[i].x < xmax:
|
|
if CLP[i].y < ymax:
|
|
if CLP[i].x > xmin:
|
|
if CLP[i].y > ymin:
|
|
section.append(CLP[i])
|
|
return section
|
|
|
|
def getMaxHeightBetweenPoints(self, finalDepth, p1, p2, cutter, CLP):
|
|
''' getMaxHeightBetweenPoints(finalDepth, p1, p2, cutter, CLP) ...
|
|
This function connects two HOLD points with line.
|
|
Each point within the subsection point list is tested to determinie if it is under cutter.
|
|
Points determined to be under the cutter on line are tested for z height.
|
|
The highest z point is the requirement for clearance between p1 and p2, and returned as zMax with 2 mm extra.
|
|
'''
|
|
dx = (p2.x - p1.x)
|
|
if dx == 0.0:
|
|
dx = 0.00001 # Need to employ a global tolerance here
|
|
m = (p2.y - p1.y) / dx
|
|
b = p1.y - (m * p1.x)
|
|
|
|
avoidTool = round(cutter * 0.75, 1) # 1/2 diam. of cutter is theoretically safe, but 3/4 diam is used for extra clearance
|
|
zMax = finalDepth
|
|
lenCLP = len(CLP)
|
|
for i in range(0, lenCLP):
|
|
mSqrd = m**2
|
|
if mSqrd < 0.0000001: # Need to employ a global tolerance here
|
|
mSqrd = 0.0000001
|
|
perpDist = math.sqrt((CLP[i].y - (m * CLP[i].x) - b)**2 / (1 + 1 / (mSqrd)))
|
|
if perpDist < avoidTool: # if point within cutter reach on line of travel, test z height and update as needed
|
|
if CLP[i].z > zMax:
|
|
zMax = CLP[i].z
|
|
return zMax + 2.0
|
|
|
|
def resetOpVariables(self, all=True):
|
|
'''resetOpVariables() ... Reset class variables used for instance of operation.'''
|
|
self.holdPoint = None
|
|
self.layerEndPnt = None
|
|
self.onHold = False
|
|
self.SafeHeightOffset = 2.0
|
|
self.ClearHeightOffset = 4.0
|
|
self.layerEndzMax = 0.0
|
|
self.resetTolerance = 0.0
|
|
self.holdPntCnt = 0
|
|
self.bbRadius = 0.0
|
|
self.axialFeed = 0.0
|
|
self.axialRapid = 0.0
|
|
self.FinalDepth = 0.0
|
|
self.clearHeight = 0.0
|
|
self.safeHeight = 0.0
|
|
self.faceZMax = -999999999999.0
|
|
if all is True:
|
|
self.cutter = None
|
|
self.stl = None
|
|
self.fullSTL = None
|
|
self.cutOut = 0.0
|
|
self.radius = 0.0
|
|
self.useTiltCutter = False
|
|
return True
|
|
|
|
def deleteOpVariables(self, all=True):
|
|
'''deleteOpVariables() ... Reset class variables used for instance of operation.'''
|
|
del self.holdPoint
|
|
del self.layerEndPnt
|
|
del self.onHold
|
|
del self.SafeHeightOffset
|
|
del self.ClearHeightOffset
|
|
del self.layerEndzMax
|
|
del self.resetTolerance
|
|
del self.holdPntCnt
|
|
del self.bbRadius
|
|
del self.axialFeed
|
|
del self.axialRapid
|
|
del self.FinalDepth
|
|
del self.clearHeight
|
|
del self.safeHeight
|
|
del self.faceZMax
|
|
if all is True:
|
|
del self.cutter
|
|
del self.stl
|
|
del self.fullSTL
|
|
del self.cutOut
|
|
del self.radius
|
|
del self.useTiltCutter
|
|
return True
|
|
|
|
def setOclCutter(self, obj, safe=False):
|
|
''' setOclCutter(obj) ... Translation function to convert FreeCAD tool definition to OCL formatted tool. '''
|
|
# Set cutter details
|
|
# https://www.freecadweb.org/api/dd/dfe/classPath_1_1Tool.html#details
|
|
diam_1 = float(obj.ToolController.Tool.Diameter)
|
|
lenOfst = obj.ToolController.Tool.LengthOffset if hasattr(obj.ToolController.Tool, 'LengthOffset') else 0
|
|
FR = obj.ToolController.Tool.FlatRadius if hasattr(obj.ToolController.Tool, 'FlatRadius') else 0
|
|
CEH = obj.ToolController.Tool.CuttingEdgeHeight if hasattr(obj.ToolController.Tool, 'CuttingEdgeHeight') else 0
|
|
CEA = obj.ToolController.Tool.CuttingEdgeAngle if hasattr(obj.ToolController.Tool, 'CuttingEdgeAngle') else 0
|
|
|
|
# Make safeCutter with 2 mm buffer around physical cutter
|
|
if safe is True:
|
|
diam_1 += 4.0
|
|
if FR != 0.0:
|
|
FR += 2.0
|
|
|
|
PathLog.debug('ToolType: {}'.format(obj.ToolController.Tool.ToolType))
|
|
if obj.ToolController.Tool.ToolType == 'EndMill':
|
|
# Standard End Mill
|
|
return ocl.CylCutter(diam_1, (CEH + lenOfst))
|
|
|
|
elif obj.ToolController.Tool.ToolType == 'BallEndMill' and FR == 0.0:
|
|
# Standard Ball End Mill
|
|
# OCL -> BallCutter::BallCutter(diameter, length)
|
|
self.useTiltCutter = True
|
|
return ocl.BallCutter(diam_1, (diam_1 / 2 + lenOfst))
|
|
|
|
elif obj.ToolController.Tool.ToolType == 'BallEndMill' and FR > 0.0:
|
|
# Bull Nose or Corner Radius cutter
|
|
# Reference: https://www.fine-tools.com/halbstabfraeser.html
|
|
# OCL -> BallCutter::BallCutter(diameter, length)
|
|
return ocl.BullCutter(diam_1, FR, (CEH + lenOfst))
|
|
|
|
elif obj.ToolController.Tool.ToolType == 'Engraver' and FR > 0.0:
|
|
# Bull Nose or Corner Radius cutter
|
|
# Reference: https://www.fine-tools.com/halbstabfraeser.html
|
|
# OCL -> ConeCutter::ConeCutter(diameter, angle, lengthOffset)
|
|
return ocl.ConeCutter(diam_1, (CEA / 2), lenOfst)
|
|
|
|
elif obj.ToolController.Tool.ToolType == 'ChamferMill':
|
|
# Bull Nose or Corner Radius cutter
|
|
# Reference: https://www.fine-tools.com/halbstabfraeser.html
|
|
# OCL -> ConeCutter::ConeCutter(diameter, angle, lengthOffset)
|
|
return ocl.ConeCutter(diam_1, (CEA / 2), lenOfst)
|
|
else:
|
|
# Default to standard end mill
|
|
PathLog.warning("Defaulting cutter to standard end mill.")
|
|
return ocl.CylCutter(diam_1, (CEH + lenOfst))
|
|
|
|
# http://www.carbidecutter.net/products/carbide-burr-cone-shape-sm.html
|
|
'''
|
|
# Available FreeCAD cutter types - some still need translation to available OCL cutter classes.
|
|
Drill, CenterDrill, CounterSink, CounterBore, FlyCutter, Reamer, Tap,
|
|
EndMill, SlotCutter, BallEndMill, ChamferMill, CornerRound, Engraver
|
|
'''
|
|
# Adittional problem is with new ToolBit user-defined cutter shapes.
|
|
# Some sort of translation/conversion will have to be defined to make compatible with OCL.
|
|
PathLog.error('Unable to set OCL cutter.')
|
|
return False
|
|
|
|
def determineVectDirect(self, pnt, nxt, travVect):
|
|
if nxt.x == pnt.x:
|
|
travVect.x = 0
|
|
elif nxt.x < pnt.x:
|
|
travVect.x = -1
|
|
else:
|
|
travVect.x = 1
|
|
|
|
if nxt.y == pnt.y:
|
|
travVect.y = 0
|
|
elif nxt.y < pnt.y:
|
|
travVect.y = -1
|
|
else:
|
|
travVect.y = 1
|
|
return travVect
|
|
|
|
def determineLineOfTravel(self, travVect):
|
|
if travVect.x == 0 and travVect.y != 0:
|
|
lineOfTravel = "Y"
|
|
elif travVect.y == 0 and travVect.x != 0:
|
|
lineOfTravel = "X"
|
|
else:
|
|
lineOfTravel = "O" # used for turns
|
|
return lineOfTravel
|
|
|
|
def _getMinSafeTravelHeight(self, pdc, p1, p2, minDep=None):
|
|
A = (p1.x, p1.y)
|
|
B = (p2.x, p2.y)
|
|
LINE = self._planarDropCutScan(pdc, A, B)
|
|
zMax = LINE[0].z
|
|
for p in LINE:
|
|
if p.z > zMax:
|
|
zMax = p.z
|
|
if minDep is not None:
|
|
if zMax < minDep:
|
|
zMax = minDep
|
|
return zMax
|
|
|
|
|
|
def SetupProperties():
|
|
''' SetupProperties() ... Return list of properties required for operation.'''
|
|
setup = []
|
|
setup.append('AvoidLastX_Faces')
|
|
setup.append('AvoidLastX_InternalFeatures')
|
|
setup.append('BoundBox')
|
|
setup.append('BoundaryAdjustment')
|
|
setup.append('CircularCenterAt')
|
|
setup.append('CircularCenterCustom')
|
|
setup.append('CircularUseG2G3')
|
|
setup.append('InternalFeaturesCut')
|
|
setup.append('InternalFeaturesAdjustment')
|
|
setup.append('CutMode')
|
|
setup.append('CutPattern')
|
|
setup.append('CutPatternAngle')
|
|
setup.append('CutPatternReversed')
|
|
setup.append('CutterTilt')
|
|
setup.append('DepthOffset')
|
|
setup.append('GapSizes')
|
|
setup.append('GapThreshold')
|
|
setup.append('HandleMultipleFeatures')
|
|
setup.append('LayerMode')
|
|
setup.append('OptimizeStepOverTransitions')
|
|
setup.append('ProfileEdges')
|
|
setup.append('BoundaryEnforcement')
|
|
setup.append('RotationAxis')
|
|
setup.append('SampleInterval')
|
|
setup.append('ScanType')
|
|
setup.append('StartIndex')
|
|
setup.append('StartPoint')
|
|
setup.append('StepOver')
|
|
setup.append('StopIndex')
|
|
setup.append('UseStartPoint')
|
|
# For debugging
|
|
setup.append('AreaParams')
|
|
setup.append('ShowTempObjects')
|
|
# Targeted for possible removal
|
|
setup.append('IgnoreWaste')
|
|
setup.append('IgnoreWasteDepth')
|
|
setup.append('ReleaseFromWaste')
|
|
return setup
|
|
|
|
|
|
def Create(name, obj=None):
|
|
'''Create(name) ... Creates and returns a Surface operation.'''
|
|
if obj is None:
|
|
obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name)
|
|
obj.Proxy = ObjectSurface(obj, name)
|
|
return obj
|