Misc. fixes

Clean up code, improve comments, improve user feedback.
Improve temp clone management.
Correct depth issue with self.finDep
Add comment descriptions for new methods
Clean up comments and contribution information
Initiate recognition of type()==SurfaceOfExtrusion
Adopt standard PathGeom.isRoughly() and .Tolerance rather than idependent precision settings
This commit is contained in:
Russell Johnson
2019-06-12 23:52:12 -05:00
parent 73e00617d1
commit 183228cc25
3 changed files with 268 additions and 172 deletions

View File

@@ -21,22 +21,31 @@
# * USA *
# * *
# ***************************************************************************
# * *
# * Additional modifications and contributions beginning 2019 *
# * Focus: 4th-axis integration *
# * by Russell Johnson <russ4262@gmail.com> *
# * *
# ***************************************************************************
# SCRIPT NOTES:
# - FUTURE: Relocate rotational calculations to Job setup tool, creating a Machine section
# with axis & rotation toggles and associated min/max values
import FreeCAD
import FreeCADGui
import Path
import PathScripts.PathLog as PathLog
import PathScripts.PathOp as PathOp
import PathScripts.PathUtils as PathUtils
import PathScripts.PathGeom as PathGeom
import Draft
import math
# from PathScripts.PathUtils import waiting_effects
from PySide import QtCore
import math
if FreeCAD.GuiUp:
import FreeCADGui
__title__ = "Base class for PathArea based operations."
__author__ = "sliptonic (Brad Collette)"
@@ -44,8 +53,8 @@ __url__ = "http://www.freecadweb.org"
__doc__ = "Base class and properties for Path.Area based operations."
__contributors__ = "mlampert [FreeCAD], russ4262 (Russell Johnson)"
__createdDate__ = "2017"
__scriptVersion__ = "2f testing"
__lastModified__ = "2019-06-12 14:12 CST"
__scriptVersion__ = "2g testing"
__lastModified__ = "2019-06-12 23:29 CST"
if False:
PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
@@ -189,7 +198,8 @@ class ObjectOp(PathOp.ObjectOp):
if PathOp.FeatureDepths & self.opFeatures(obj):
try:
shape = self.areaOpShapeForDepths(obj, job)
except:
except Exception as ee:
PathLog.error(ee)
shape = None
# Set initial start and final depths
@@ -316,10 +326,8 @@ class ObjectOp(PathOp.ObjectOp):
areaOpShapes(obj) ... the shape for path area to process
areaOpUseProjection(obj) ... return true if operation can use projection
instead.'''
PathLog.debug("\n\n----- opExecute() in PathAreaOp.py")
PathLog.track()
self.endVector = None
PathLog.debug("\n\n----- opExecute() in PathAreaOp.py")
PathLog.info("\n----- opExecute() in PathAreaOp.py")
# PathLog.debug("OpDepths are Start: {}, and Final: {}".format(obj.OpStartDepth.Value, obj.OpFinalDepth.Value))
# PathLog.debug("Depths are Start: {}, and Final: {}".format(obj.StartDepth.Value, obj.FinalDepth.Value))
# PathLog.debug("initOpDepths are Start: {}, and Final: {}".format(self.initOpStartDepth, self.initOpFinalDepth))
@@ -331,6 +339,7 @@ class ObjectOp(PathOp.ObjectOp):
self.cloneNames = []
self.guiMsgs = [] # list of message tuples (title, msg) to be displayed in GUI
self.stockBB = PathUtils.findParentJob(obj).Stock.Shape.BoundBox
self.useTempJobClones('Delete') # Clear temporary group and recreate for temp job clones
# Import OpFinalDepth from pre-existing operation for recompute() scenarios
if self.defValsSet is True:
@@ -347,7 +356,6 @@ class ObjectOp(PathOp.ObjectOp):
self.defValsSet = False
if obj.EnableRotation != 'Off':
self.useRotJobClones('Start')
# Calculate operation heights based upon rotation radii
opHeights = self.opDetermineRotationRadii(obj)
(self.xRotRad, self.yRotRad, self.zRotRad) = opHeights[0]
@@ -360,16 +368,11 @@ class ObjectOp(PathOp.ObjectOp):
self.strDep = self.yRotRad
else:
self.strDep = max(self.xRotRad, self.yRotRad)
self.finDep = -1 * self.strDep
self.finDep = -1 * self.strDep
obj.ClearanceHeight.Value = self.strDep + self.safOfset
obj.SafeHeight.Value = self.strDep + self.safOfst
# Set axial feed rates based upon horizontal feed rates
safeCircum = 2 * math.pi * obj.SafeHeight.Value
self.axialFeed = 360 / safeCircum * self.horizFeed
self.axialRapid = 360 / safeCircum * self.horizRapid
if self.initWithRotation == False:
if obj.FinalDepth.Value == obj.OpFinalDepth.Value:
obj.FinalDepth.Value = self.finDep
@@ -383,6 +386,11 @@ class ObjectOp(PathOp.ObjectOp):
self.strDep = obj.StartDepth.Value
self.finDep = obj.FinalDepth.Value
# Set axial feed rates based upon horizontal feed rates
safeCircum = 2 * math.pi * obj.SafeHeight.Value
self.axialFeed = 360 / safeCircum * self.horizFeed
self.axialRapid = 360 / safeCircum * self.horizRapid
# Initiate depthparams and calculate operation heights for rotational operation
finish_step = obj.FinishDepth.Value if hasattr(obj, "FinishDepth") else 0.0
self.depthparams = PathUtils.depth_params(
@@ -408,7 +416,7 @@ class ObjectOp(PathOp.ObjectOp):
if len(shp) == 2:
(fc, iH) = shp
# fc, iH, sub, angle, axis
tup = fc, iH, 'otherOp', 0.0, 'X', obj.StartDepth.Value, obj.FinalDepth.Value
tup = fc, iH, 'otherOp', 0.0, 'S', obj.StartDepth.Value, obj.FinalDepth.Value
shapes.append(tup)
else:
shapes.append(shp)
@@ -427,12 +435,13 @@ class ObjectOp(PathOp.ObjectOp):
# PathLog.debug("Pre_path depths are Start: {}, and Final: {}".format(obj.StartDepth.Value, obj.FinalDepth.Value))
sims = []
numShapes = len(shapes)
if numShapes == 1:
nextAxis = shapes[0][4]
elif numShapes > 1:
nextAxis = shapes[1][4]
else:
nextAxis = 'X'
nextAxis = 'L'
for ns in range(0, numShapes):
(shape, isHole, sub, angle, axis, strDep, finDep) = shapes[ns]
@@ -460,26 +469,33 @@ class ObjectOp(PathOp.ObjectOp):
ppCmds = pp.Commands
if obj.EnableRotation != 'Off' and self.rotateFlag is True:
# Rotate model to index for cut
axisOfRot = 'A'
if axis == 'Y':
if axis == 'X':
axisOfRot = 'A'
elif axis == 'Y':
axisOfRot = 'B'
# Reverse angle temporarily to match model. Error in FreeCAD render of B axis rotations
if obj.B_AxisErrorOverride is True:
angle = -1 * angle
elif axis == 'Z':
axisOfRot = 'C'
else:
axisOfRot = 'A'
# Rotate Model to correct angle
ppCmds.insert(0, Path.Command('G1', {axisOfRot: angle, 'F': self.axialFeed}))
ppCmds.insert(0, Path.Command('N100', {}))
# Raise cutter to safe depth and return index to starting position
ppCmds.insert(0, Path.Command('N200', {}))
ppCmds.append(Path.Command('G1', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid}))
# ppCmds.append(Path.Command('G0', {axisOfRot: 0.0, 'F': self.axialRapid}))
ppCmds.append(Path.Command('N200', {}))
ppCmds.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid}))
if axis != nextAxis:
ppCmds.append(Path.Command('G0', {axisOfRot: 0.0, 'F': self.axialRapid}))
# Eif
# Save gcode commands to object command list
self.commandlist.extend(ppCmds)
sims.append(sim)
# Eif
if self.areaOpRetractTool(obj):
self.endVector = None
@@ -489,10 +505,9 @@ class ObjectOp(PathOp.ObjectOp):
self.commandlist.append(Path.Command('G0', {'A': 0.0, 'F': self.axialRapid}))
self.commandlist.append(Path.Command('G0', {'B': 0.0, 'F': self.axialRapid}))
self.useTempJobClones('Delete') # Delete temp job clone group and contents
self.guiMessage('title', None, show=True) # Process GUI messages to user
PathLog.debug("obj.Name: " + str(obj.Name))
if obj.EnableRotation != 'Off':
self.useRotJobClones('Delete')
self.guiMessage('title', None, show=True)
return sims
def areaOpRetractTool(self, obj):
@@ -522,7 +537,7 @@ class ObjectOp(PathOp.ObjectOp):
return False
def opDetermineRotationRadii(self, obj):
'''opDetermineRotationRadii(self, obj)
'''opDetermineRotationRadii(obj)
Determine rotational radii for 4th-axis rotations, for clearance/safe heights '''
parentJob = PathUtils.findParentJob(obj)
@@ -561,36 +576,40 @@ class ObjectOp(PathOp.ObjectOp):
return [(xRotRad, yRotRad, zRotRad), (clrOfst, safOfst)]
def faceRotationAnalysis(self, obj, norm, surf):
'''faceRotationAnalysis(self, obj, norm, surf)
'''faceRotationAnalysis(obj, norm, surf)
Determine X and Y independent rotation necessary to make normalAt = Z=1 (0,0,1) '''
PathLog.track()
praInfo = "faceRotationAnalysis() in PathAreaOp.py"
rtn = False
rtn = True
axis = 'X'
orientation = 'X'
angle = 500.0
precision = 6
def roundRoughValues(val):
zTol = 1.0E-9
rndTol = 1.0 - zTol
for i in range(0, 13):
if PathGeom.Tolerance * (i * 10) == 1.0:
precision = i
break
def roundRoughValues(precision, val):
# Convert VALxe-15 numbers to zero
if math.fabs(val) <= zTol:
if PathGeom.isRoughly(0.0, val) is True:
return 0.0
# Convert VAL.99999999 to next integer
elif math.fabs(val % 1) > rndTol:
elif math.fabs(val % 1) > 1.0 - PathGeom.Tolerance:
return round(val)
else:
return val
return round(val, precision)
nX = roundRoughValues(norm.x)
nY = roundRoughValues(norm.y)
nZ = roundRoughValues(norm.z)
nX = roundRoughValues(precision, norm.x)
nY = roundRoughValues(precision, norm.y)
nZ = roundRoughValues(precision, norm.z)
praInfo += "\n -normalAt(0,0): " + str(nX) + ", " + str(nY) + ", " + str(nZ)
saX = roundRoughValues(surf.x)
saY = roundRoughValues(surf.y)
saZ = roundRoughValues(surf.z)
saX = roundRoughValues(precision, surf.x)
saY = roundRoughValues(precision, surf.y)
saZ = roundRoughValues(precision, surf.z)
praInfo += "\n -Surface.Axis: " + str(saX) + ", " + str(saY) + ", " + str(saZ)
# Determine rotation needed and current orientation
@@ -653,15 +672,20 @@ class ObjectOp(PathOp.ObjectOp):
# Enforce enabled rotation in settings
if orientation == 'Y':
axis = 'X'
if obj.EnableRotation == 'B(y)': # Axis disabled
angle = 500.0
if obj.EnableRotation == 'B(y)': # Required axis disabled
rtn = False
else:
axis = 'Y'
if obj.EnableRotation == 'A(x)': # Axis disabled
angle = 500.0
if obj.EnableRotation == 'A(x)': # Required axis disabled
rtn = False
if angle != 500.0 and angle != 0.0:
praInfo += "\n - ... rotation triggered"
if angle == 500.0:
rtn = False
if angle == 0.0:
rtn = False
if rtn is True:
self.rotateFlag = True
rtn = True
if obj.ReverseDirection is True:
@@ -669,7 +693,11 @@ class ObjectOp(PathOp.ObjectOp):
angle = angle + 180.0
else:
angle = angle - 180.0
praInfo += "\n -Suggested rotation: angle: " + str(angle) + ", axis: " + str(axis)
angle = round(angle, precision)
praInfo += "\n -Rotation analysis: angle: " + str(angle) + ", axis: " + str(axis)
if rtn is True:
praInfo += "\n - ... rotation triggered"
else:
praInfo += "\n - ... NO rotation triggered"
@@ -678,6 +706,8 @@ class ObjectOp(PathOp.ObjectOp):
return (rtn, angle, axis, praInfo)
def guiMessage(self, title, msg, show=False):
'''guiMessage(title, msg, show=False)
Handle op related GUI messages to user'''
if msg is not None:
self.guiMsgs.append((title, msg))
if show is True:
@@ -712,6 +742,10 @@ class ObjectOp(PathOp.ObjectOp):
return False
def visualAxis(self):
'''visualAxis()
Create visual X & Y axis for use in orientation of rotational operations
Triggered only for PathLog.debug'''
if not FreeCAD.ActiveDocument.getObject('xAxCyl'):
xAx = 'xAxCyl'
yAx = 'yAxCyl'
@@ -760,20 +794,20 @@ class ObjectOp(PathOp.ObjectOp):
cylGui.Visibility = False
vaGrp.addObject(cyl)
def useRotJobClones(self, cloneName):
def useTempJobClones(self, cloneName):
'''useTempJobClones(cloneName)
Manage use of temporary model clones for rotational operation calculations.
Clones are stored in 'rotJobClones' group.'''
if FreeCAD.ActiveDocument.getObject('rotJobClones'):
if cloneName == 'Delete':
if cloneName == 'Start':
if PathLog.getLevel(PathLog.thisModule()) < 4:
for cln in FreeCAD.ActiveDocument.getObject('rotJobClones').Group:
FreeCAD.ActiveDocument.removeObject(cln.Name)
elif cloneName == 'Delete':
if PathLog.getLevel(PathLog.thisModule()) < 4:
for cln in FreeCAD.ActiveDocument.getObject('rotJobClones').Group:
FreeCAD.ActiveDocument.removeObject(cln.Name)
FreeCAD.ActiveDocument.removeObject('rotJobClones')
return
if cloneName == 'Start':
for cln in FreeCAD.ActiveDocument.getObject('rotJobClones').Group:
FreeCAD.ActiveDocument.removeObject(cln.Name)
FreeCAD.ActiveDocument.removeObject('rotJobClones')
return
else:
FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup","rotJobClones")
FreeCADGui.ActiveDocument.getObject('rotJobClones').Visibility = False
@@ -783,6 +817,9 @@ class ObjectOp(PathOp.ObjectOp):
FreeCADGui.ActiveDocument.getObject(cloneName).Visibility = False
def cloneBaseAndStock(self, obj, base, angle, axis, subCount):
'''cloneBaseAndStock(obj, base, angle, axis, subCount)
Method called to create a temporary clone of the base and parent Job stock.
Clones are destroyed after usage for calculations related to rotational operations.'''
# Create a temporary clone and stock of model for rotational use.
rndAng = round(angle, 8)
if rndAng < 0.0: # neg sign is converted to underscore in clone name creation.
@@ -794,19 +831,23 @@ class ObjectOp(PathOp.ObjectOp):
if clnNm not in self.cloneNames:
self.cloneNames.append(clnNm)
self.cloneNames.append(stckClnNm)
if FreeCAD.ActiveDocument.getObject(clnNm):
FreeCAD.ActiveDocument.removeObject(clnNm)
if FreeCAD.ActiveDocument.getObject(stckClnNm):
FreeCAD.ActiveDocument.removeObject(stckClnNm)
FreeCAD.ActiveDocument.addObject('Part::Feature', clnNm).Shape = base.Shape
FreeCAD.ActiveDocument.addObject('Part::Feature', stckClnNm).Shape = PathUtils.findParentJob(obj).Stock.Shape
FreeCADGui.ActiveDocument.getObject(stckClnNm).Transparency=90
FreeCADGui.ActiveDocument.getObject(clnNm).ShapeColor = (1.000,0.667,0.000)
self.useRotJobClones(clnNm)
self.useRotJobClones(stckClnNm)
self.useTempJobClones(clnNm)
self.useTempJobClones(stckClnNm)
clnBase = FreeCAD.ActiveDocument.getObject(clnNm)
clnStock = FreeCAD.ActiveDocument.getObject(stckClnNm)
tag = base.Name + '_' + tag
return (clnBase, clnStock, tag)
def getFaceNormAndSurf(self, face):
'''getFaceNormAndSurf(self, face)
'''getFaceNormAndSurf(face)
Return face.normalAt(0,0) or face.normal(0,0) and face.Surface.Axis vectors
'''
norm = FreeCAD.Vector(0.0, 0.0, 0.0)
@@ -829,7 +870,7 @@ class ObjectOp(PathOp.ObjectOp):
return (norm, surf)
def applyRotationalAnalysis(self, obj, base, angle, axis, subCount):
'''applyRotationalAnalysis(self, obj, base, angle, axis, subCount)
'''applyRotationalAnalysis(obj, base, angle, axis, subCount)
Create temp clone and stock and apply rotation to both.
Return new rotated clones
'''
@@ -852,7 +893,9 @@ class ObjectOp(PathOp.ObjectOp):
clnStock.purgeTouched()
return (clnBase, angle, clnStock, tag)
def applyInverseAngle(self, obj, clnBase, clnStock, axis, angle):
def applyInverseAngle(self, obj, clnBase, clnStock, axis, angle):
'''applyInverseAngle(obj, clnBase, clnStock, axis, angle)
Apply rotations to incoming base and stock objects.'''
if axis == 'X':
vect = FreeCAD.Vector(1, 0, 0)
elif axis == 'Y':
@@ -864,15 +907,15 @@ class ObjectOp(PathOp.ObjectOp):
clnStock.purgeTouched()
# Update property and angle values
obj.InverseAngle = True
obj.AttemptInverseAngle = False
angle = -1 * angle
PathLog.info(translate("Path", "Rotated to inverse angle."))
return (clnBase, clnStock, angle)
def calculateStartFinalDepths(self, obj, shape, stock):
'''calculateStartFinalDepths(self, obj, shape, stock)
Calculate correct start and final depths for the face
'''
'''calculateStartFinalDepths(obj, shape, stock)
Calculate correct start and final depths for the shape(face) object provided.'''
finDep = max(obj.FinalDepth.Value, shape.BoundBox.ZMin)
stockTop = stock.Shape.BoundBox.ZMax
if obj.EnableRotation == 'Off':
@@ -883,14 +926,12 @@ class ObjectOp(PathOp.ObjectOp):
strDep = min(obj.StartDepth.Value, stockTop)
if strDep <= finDep:
strDep = stockTop # self.strDep
msg = "Start depth <= face depth.\nIncreased to stock top."
# msg = translate('Path', msg + "\nFace depth is {} mm.".format(face.BoundBox.ZMax)
msg = translate('Path', msg)
msg = translate('Path', "Start depth <= face depth.\nIncreased to stock top.")
PathLog.error(msg)
return (strDep, finDep)
def sortTuplesByIndex(self, TupleList, tagIdx):
'''sortTuplesByIndex(self, TupleList, tagIdx)
'''sortTuplesByIndex(TupleList, tagIdx)
sort list of tuples based on tag index provided
return (TagList, GroupList)
'''
@@ -914,5 +955,14 @@ class ObjectOp(PathOp.ObjectOp):
GroupList.pop(0)
return (TagList, GroupList)
def warnDisabledAxis(self, obj, axis):
'''warnDisabledAxis(self, obj, axis)
Provide user feedback if required axis is disabled'''
if axis == 'X' and obj.EnableRotation == 'B(y)':
PathLog.warning(translate('Path', "Part feature is inaccessible. Selected feature(s) require 'A(x)' for access."))
return True
elif axis == 'Y' and obj.EnableRotation == 'A(x)':
PathLog.warning(translate('Path', "Part feature is inaccessible. Selected feature(s) require 'B(y)' for access."))
return True
else:
return False

View File

@@ -21,16 +21,17 @@
# * USA *
# * *
# ***************************************************************************
# * *
# * Additional modifications and contributions beginning 2019 *
# * Focus: 4th-axis integration *
# * by Russell Johnson <russ4262@gmail.com> *
# * *
# ***************************************************************************
# SCRIPT NOTES:
# - Need test models for testing vertical faces scenarios.
# - Need to group VERTICAL faces per axis_angle tag just like horizontal faces.
# Then, need to run each grouping through
# PathGeom.combineConnectedShapes(vertical) algorithm grouping
# - Need to add face boundbox analysis code to vertical axis_angle
# section to identify highest zMax for all faces included in group
# - FUTURE: Re-iterate PathAreaOp.py need to relocate rotational settings
# to Job setup, under Machine settings tab
# - FUTURE: PathAreaOp.py need to relocate rotational settings
# to Job setup, under Machine settings tab
import FreeCAD
import Part
@@ -50,8 +51,8 @@ __url__ = "http://www.freecadweb.org"
__doc__ = "Class and implementation of shape based Pocket operation."
__contributors__ = "mlampert [FreeCAD], russ4262 (Russell Johnson)"
__created__ = "2017"
__scriptVersion__ = "2f testing"
__lastModified__ = "2019-06-12 14:12 CST"
__scriptVersion__ = "2g testing"
__lastModified__ = "2019-06-12 23:29 CST"
if False:
PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
@@ -173,8 +174,9 @@ class Extension(object):
tangent = e0.tangentAt(midparam)
try:
normal = tangent.cross(FreeCAD.Vector(0, 0, 1)).normalize()
except:
except Exception as e:
PathLog.error('getDirection(): tangent.cross(FreeCAD.Vector(0, 0, 1)).normalize()')
PathLog.error(e)
return None
else:
poffPlus = e0.valueAt(midparam) + 0.01 * normal
@@ -241,22 +243,27 @@ class ObjectPocket(PathPocketBase.ObjectPocket):
'''areaOpShapes(obj) ... return shapes representing the solids to be removed.'''
PathLog.track()
PathLog.debug("----- areaOpShapes() in PathPocketShape.py")
import Draft
baseSubsTuples = []
subCount = 0
allTuples = []
self.tempNameList = []
self.delTempNameList = 0
finalDepths = []
def clasifySub(self, bs, sub):
face = bs.Shape.getElement(sub)
def planarFaceFromExtrusionEdges(clsd):
useFace = 'useFaceName'
minArea = 0.0
fCnt = 0
def planarFaceFromExtrusionEdges(face, trans):
useFace = 'useFaceName'
minArea = 0.0
fCnt = 0
clsd = []
planar = False
# Identify closed edges
for edg in face.Edges:
if edg.isClosed():
PathLog.debug(' -e.isClosed()')
clsd.append(edg)
planar = True
# Attempt to create planar faces and select that with smallest area for use as pocket base
if planar is True:
planar = False
for edg in clsd:
fCnt += 1
fName = sub + '_face_' + str(fCnt)
@@ -265,7 +272,10 @@ class ObjectPocket(PathPocketBase.ObjectPocket):
if mFF.isNull():
PathLog.debug('Face(Part.Wire()) failed')
else:
mFF.translate(FreeCAD.Vector(0, 0, face.BoundBox.ZMin - mFF.BoundBox.ZMin))
if trans is True:
mFF.translate(FreeCAD.Vector(0, 0, face.BoundBox.ZMin - mFF.BoundBox.ZMin))
if FreeCAD.ActiveDocument.getObject(fName):
FreeCAD.ActiveDocument.removeObject(fName)
tmpFace = FreeCAD.ActiveDocument.addObject('Part::Feature', fName).Shape = mFF
tmpFace = FreeCAD.ActiveDocument.getObject(fName)
tmpFace.purgeTouched()
@@ -279,7 +289,12 @@ class ObjectPocket(PathPocketBase.ObjectPocket):
useFace = fName
else:
FreeCAD.ActiveDocument.removeObject(fName)
return (planar, useFace)
if useFace != 'useFaceName':
self.useTempJobClones(useFace)
return (planar, useFace)
def clasifySub(self, bs, sub):
face = bs.Shape.getElement(sub)
if type(face.Surface) == Part.Plane:
PathLog.debug('type() == Part.Plane')
@@ -292,7 +307,8 @@ class ObjectPocket(PathPocketBase.ObjectPocket):
PathLog.debug(' -isHorizontal()')
self.vert.append(face)
return True
return False
else:
return False
elif type(face.Surface) == Part.Cylinder and PathGeom.isVertical(face.Surface.Axis):
PathLog.debug('type() == Part.Cylinder')
# vertical cylinder wall
@@ -312,36 +328,22 @@ class ObjectPocket(PathPocketBase.ObjectPocket):
elif type(face.Surface) == Part.SurfaceOfExtrusion:
# extrusion wall
PathLog.debug('type() == Part.SurfaceOfExtrusion')
clsd = []
planar = False
# Identify closed edges
for edg in face.Edges:
if edg.isClosed():
PathLog.debug(' -e.isClosed()')
clsd.append(edg)
planar = True
# Attempt to create planar faces and select that with smallest area for use as pocket base
if planar is True:
planar = False
(planar, useFace) = planarFaceFromExtrusionEdges(clsd)
# Attempt to extract planar face from surface of extrusion
(planar, useFace) = planarFaceFromExtrusionEdges(face, trans=True)
# Save face object to self.horiz for processing or display error
if planar is True:
self.tempNameList.append(useFace)
self.delTempNameList += 1
uFace = FreeCAD.ActiveDocument.getObject(useFace)
self.horiz.append(uFace.Shape.Faces[0])
msg = "<b>Verify depth of pocket for '{}'.</b>".format(sub)
msg += "\n<br>Pocket is based on extruded surface."
msg += "\n<br>Bottom of pocket might be non-planar and/or not normal to spindle axis."
msg += "\n<br>\n<br><i>3D pocket bottom is NOT available in this operation</i>."
msg = translate('Path', msg)
msg = translate('Path', "<b>Verify depth of pocket for '{}'.</b>".format(sub))
msg += translate('Path', "\n<br>Pocket is based on extruded surface.")
msg += translate('Path', "\n<br>Bottom of pocket might be non-planar and/or not normal to spindle axis.")
msg += translate('Path', "\n<br>\n<br><i>3D pocket bottom is NOT available in this operation</i>.")
PathLog.info(msg)
title = translate('Path', 'Depth Warning')
self.guiMessage(title, msg, False)
else:
PathLog.error(translate("Path", "Failed to create a planar face from edges in {}.".format(sub)))
else:
PathLog.debug('type() == OTHER')
PathLog.debug(' -type(face.Surface): {}'.format(type(face.Surface)))
return False
@@ -356,19 +358,23 @@ class ObjectPocket(PathPocketBase.ObjectPocket):
else:
for p in range(0, len(obj.Base)):
(base, subsList) = obj.Base[p]
go = False
isLoop = False
# First, check all subs collectively for loop of faces
if len(subsList) > 2:
(go, norm, surf) = self.checkForFacesLoop(base, subsList)
if go is True:
PathLog.debug("Common Surface.Axis or normalAt() value found for loop faces.")
(isLoop, norm, surf) = self.checkForFacesLoop(base, subsList)
if isLoop is True:
PathLog.info("Common Surface.Axis or normalAt() value found for loop faces.")
rtn = False
subCount += 1
(rtn, angle, axis, praInfo) = self.faceRotationAnalysis(obj, norm, surf)
PathLog.info("angle: {}; axis: {}".format(angle, axis))
if rtn is True:
(clnBase, angle, clnStock, tag) = self.applyRotationalAnalysis(obj, base, angle, axis, subCount)
faceNums = ""
for f in subsList:
faceNums += '_' + f.replace('Face', '')
(clnBase, angle, clnStock, tag) = self.applyRotationalAnalysis(obj, base, angle, axis, faceNums)
# Verify faces are correctly oriented - InverseAngle might be necessary
PathLog.debug("Checking if faces are oriented correctly after rotation...")
@@ -386,7 +392,8 @@ class ObjectPocket(PathPocketBase.ObjectPocket):
tup = clnBase, subsList, angle, axis, clnStock
else:
PathLog.debug("No rotation used")
if self.warnDisabledAxis(obj, axis) is False:
PathLog.debug("No rotation used")
axis = 'X'
angle = 0.0
stock = PathUtils.findParentJob(obj).Stock
@@ -396,7 +403,7 @@ class ObjectPocket(PathPocketBase.ObjectPocket):
baseSubsTuples.append(tup)
# Eif
if go is False:
if isLoop is False:
PathLog.debug(translate('Path', "Processing subs individually ..."))
for sub in subsList:
subCount += 1
@@ -405,11 +412,28 @@ class ObjectPocket(PathPocketBase.ObjectPocket):
PathLog.debug(translate('Path', "Base Geometry sub: {}".format(sub)))
face = base.Shape.getElement(sub)
# --------------------------------------------------------
if type(face.Surface) == Part.SurfaceOfExtrusion:
# extrusion wall
PathLog.debug('analyzing type() == Part.SurfaceOfExtrusion')
# Attempt to extract planar face from surface of extrusion
(planar, useFace) = planarFaceFromExtrusionEdges(face, trans=False)
# Save face object to self.horiz for processing or display error
if planar is True:
base = FreeCAD.ActiveDocument.getObject(useFace)
sub = 'Face1'
PathLog.debug(' -successful face crated: {}'.format(useFace))
else:
PathLog.error(translate("Path", "Failed to create a planar face from edges in {}.".format(sub)))
# --------------------------------------------------------
(norm, surf) = self.getFaceNormAndSurf(face)
(rtn, angle, axis, praInfo)= self.faceRotationAnalysis(obj, norm, surf)
if rtn is True:
(clnBase, angle, clnStock, tag) = self.applyRotationalAnalysis(obj, base, angle, axis, subCount)
faceNum = sub.replace('Face', '')
(clnBase, angle, clnStock, tag) = self.applyRotationalAnalysis(obj, base, angle, axis, faceNum)
# Verify faces are correctly oriented - InverseAngle might be necessary
faceIA = clnBase.Shape.getElement(sub)
(norm, surf) = self.getFaceNormAndSurf(faceIA)
@@ -425,7 +449,8 @@ class ObjectPocket(PathPocketBase.ObjectPocket):
tup = clnBase, [sub], angle, axis, clnStock
else:
PathLog.debug(str(sub) + ": No rotation used")
if self.warnDisabledAxis(obj, axis) is False:
PathLog.debug(str(sub) + ": No rotation used")
axis = 'X'
angle = 0.0
stock = PathUtils.findParentJob(obj).Stock
@@ -460,40 +485,44 @@ class ObjectPocket(PathPocketBase.ObjectPocket):
self.horiz = []
self.vert = []
subBase = o[0]
subsList = o[1]
angle = o[2]
axis = o[3]
stock = o[4]
for sub in o[1]:
for sub in subsList:
if 'Face' in sub:
if clasifySub(self, subBase, sub) is False:
PathLog.error(translate('PathPocket', 'Pocket does not support shape %s.%s') % (subBase.Label, sub))
if obj.EnableRotation != 'Off':
PathLog.info(translate('PathPocket', 'Face might not be within rotation accessibility limits.'))
# Determine final depth as highest value of bottom boundbox of vertical face,
# in case of uneven faces on bottom
vFinDep = self.vert[0].BoundBox.ZMin
for vFace in self.vert:
if vFace.BoundBox.ZMin > vFinDep:
vFinDep = vFace.BoundBox.ZMin
# Determine if vertical faces for a loop: Extract planar loop wire as new horizontal face.
self.vertical = PathGeom.combineConnectedShapes(self.vert)
self.vWires = [TechDraw.findShapeOutline(shape, 1, FreeCAD.Vector(0, 0, 1)) for shape in self.vertical]
for wire in self.vWires:
w = PathGeom.removeDuplicateEdges(wire)
face = Part.Face(w)
# face.tessellate(0.1)
if PathGeom.isRoughly(face.Area, 0):
msg = translate('PathPocket', 'Vertical faces do not form a loop - ignoring')
PathLog.error(msg)
# title = translate("Path", "Face Selection Warning")
# self.guiMessage(title, msg, True)
else:
face.translate(FreeCAD.Vector(0, 0, vFinDep - face.BoundBox.ZMin))
self.horiz.append(face)
msg = translate('Path', 'Verify final depth of pocket shaped by vertical faces.')
PathLog.error(msg)
title = translate('Path', 'Depth Warning')
self.guiMessage(title, msg, False)
if len(self.vert) > 0:
vFinDep = self.vert[0].BoundBox.ZMin
for vFace in self.vert:
if vFace.BoundBox.ZMin > vFinDep:
vFinDep = vFace.BoundBox.ZMin
# Determine if vertical faces for a loop: Extract planar loop wire as new horizontal face.
self.vertical = PathGeom.combineConnectedShapes(self.vert)
self.vWires = [TechDraw.findShapeOutline(shape, 1, FreeCAD.Vector(0, 0, 1)) for shape in self.vertical]
for wire in self.vWires:
w = PathGeom.removeDuplicateEdges(wire)
face = Part.Face(w)
# face.tessellate(0.1)
if PathGeom.isRoughly(face.Area, 0):
msg = translate('PathPocket', 'Vertical faces do not form a loop - ignoring')
PathLog.error(msg)
# title = translate("Path", "Face Selection Warning")
# self.guiMessage(title, msg, True)
else:
face.translate(FreeCAD.Vector(0, 0, vFinDep - face.BoundBox.ZMin))
self.horiz.append(face)
msg = translate('Path', 'Verify final depth of pocket shaped by vertical faces.')
PathLog.error(msg)
title = translate('Path', 'Depth Warning')
self.guiMessage(title, msg, False)
# add faces for extensions
self.exts = []
@@ -524,15 +553,19 @@ class ObjectPocket(PathPocketBase.ObjectPocket):
for face in self.horizontal:
# extrude all faces up to StartDepth and those are the removal shapes
(strDep, finDep) = self.calculateStartFinalDepths(obj, face, stock)
finalDepths.append(finDep)
extent = FreeCAD.Vector(0, 0, strDep - finDep)
self.removalshapes.append((face.removeSplitter().extrude(extent), False, 'pathPocketShape', angle, axis, strDep, finDep))
PathLog.debug("Extent depths are str: {}, and fin: {}".format(strDep, finDep))
# Efor face
# Adjust obj.FinalDepth.Value as needed.
if subCount == 1:
obj.FinalDepth.Value = finDep
else: # process the job base object as a whole
if len(finalDepths) > 0:
finalDep = min(finalDepths)
if subCount == 1:
obj.FinalDepth.Value = finDep
else:
# process the job base object as a whole
PathLog.debug(translate("Path", 'Processing model as a whole ...'))
finDep = obj.FinalDepth.Value
strDep = obj.StartDepth.Value
@@ -554,9 +587,10 @@ class ObjectPocket(PathPocketBase.ObjectPocket):
if self.removalshapes:
obj.removalshape = self.removalshapes[0][0]
if self.delTempNameList > 0:
for tmpNm in self.tempNameList:
FreeCAD.ActiveDocument.removeObject(tmpNm)
#if PathLog.getLevel(PathLog.thisModule()) != 4:
#if self.delTempNameList > 0:
# for tmpNm in self.tempNameList:
# FreeCAD.ActiveDocument.removeObject(tmpNm)
return self.removalshapes
@@ -590,7 +624,7 @@ class ObjectPocket(PathPocketBase.ObjectPocket):
obj.ExtensionFeature = [(ext.obj, ext.getSubLink()) for ext in extensions]
def checkForFacesLoop(self, base, subsList):
'''checkForFacesLoop(self, base, subsList)...
'''checkForFacesLoop(base, subsList)...
Accepts a list of face names for the given base.
Checks to determine if they are looped together.
'''
@@ -603,6 +637,7 @@ class ObjectPocket(PathPocketBase.ObjectPocket):
saSum = FreeCAD.Vector(0.0, 0.0, 0.0)
norm = FreeCAD.Vector(0.0, 0.0, 0.0)
surf = FreeCAD.Vector(0.0, 0.0, 0.0)
precision = 6
def makeTempExtrusion(base, sub, fCnt):
extName = 'tmpExtrude' + str(fCnt)
@@ -632,17 +667,21 @@ class ObjectPocket(PathPocketBase.ObjectPocket):
tmpWire.purgeTouched()
return (True, tmpWire, tmpExt)
def roundValue(val):
zTol = 1.0E-8
rndTol = 1.0 - zTol
def roundValue(precision, val):
# Convert VALxe-15 numbers to zero
if math.fabs(val) <= zTol:
if PathGeom.isRoughly(0.0, val) is True:
return 0.0
# Convert VAL.99999999 to next integer
elif math.fabs(val % 1) > rndTol:
elif math.fabs(val % 1) > 1.0 - PathGeom.Tolerance:
return round(val)
else:
return round(val, 8)
return round(val, precision)
# Determine precision from Tolerance
for i in range(0, 13):
if PathGeom.Tolerance * (i * 10) == 1.0:
precision = i
break
# Sub Surface.Axis values of faces
# Vector of (0, 0, 0) will suggests a loop
@@ -692,21 +731,21 @@ class ObjectPocket(PathPocketBase.ObjectPocket):
if len(matchList) == 0:
for fc in lastExtrusion.Shape.Faces:
(norm, raw) = self.getFaceNormAndSurf(fc)
rnded = FreeCAD.Vector(roundValue(raw.x), roundValue(raw.y), roundValue(raw.z))
rnded = FreeCAD.Vector(roundValue(precision, raw.x), roundValue(precision, raw.y), roundValue(precision, raw.z))
if rnded.x == 0.0 or rnded.y == 0.0 or rnded.z == 0.0:
for fc2 in tmpExt.Shape.Faces:
(norm2, raw2) = self.getFaceNormAndSurf(fc2)
rnded2 = FreeCAD.Vector(roundValue(raw2.x), roundValue(raw2.y), roundValue(raw2.z))
rnded2 = FreeCAD.Vector(roundValue(precision, raw2.x), roundValue(precision, raw2.y), roundValue(precision, raw2.z))
if rnded == rnded2:
matchList.append(fc2)
go = True
else:
for m in matchList:
(norm, raw) = self.getFaceNormAndSurf(m)
rnded = FreeCAD.Vector(roundValue(raw.x), roundValue(raw.y), roundValue(raw.z))
rnded = FreeCAD.Vector(roundValue(precision, raw.x), roundValue(precision, raw.y), roundValue(precision, raw.z))
for fc2 in tmpExt.Shape.Faces:
(norm2, raw2) = self.getFaceNormAndSurf(fc2)
rnded2 = FreeCAD.Vector(roundValue(raw2.x), roundValue(raw2.y), roundValue(raw2.z))
rnded2 = FreeCAD.Vector(roundValue(precision, raw2.x), roundValue(precision, raw2.y), roundValue(precision, raw2.z))
if rnded.x == 0.0 or rnded.y == 0.0 or rnded.z == 0.0:
if rnded == rnded2:
go = True
@@ -722,7 +761,7 @@ class ObjectPocket(PathPocketBase.ObjectPocket):
saTotal = FreeCAD.Vector(0.0, 0.0, 0.0)
for fc in matchList:
(norm, raw) = self.getFaceNormAndSurf(fc)
rnded = FreeCAD.Vector(roundValue(raw.x), roundValue(raw.y), roundValue(raw.z))
rnded = FreeCAD.Vector(roundValue(precision, raw.x), roundValue(precision, raw.y), roundValue(precision, raw.z))
if (rnded.y > 0.0 or rnded.z > 0.0) and vertLoopFace is None:
vertLoopFace = fc
saTotal = saTotal.add(rnded)

View File

@@ -21,6 +21,12 @@
# * USA *
# * *
# ***************************************************************************
# * *
# * Additional modifications and contributions beginning 2019 *
# * Focus: 4th-axis integration *
# * by Russell Johnson <russ4262@gmail.com> *
# * *
# ***************************************************************************
import ArchPanel
import FreeCAD
@@ -40,9 +46,10 @@ __title__ = "Path Profile Faces Operation"
__author__ = "sliptonic (Brad Collette)"
__url__ = "http://www.freecadweb.org"
__doc__ = "Path Profile operation based on faces."
__contributors__ = "russ4262 (Russell Johnson)"
__created__ = "2014"
__scriptVersion__ = "2f testing"
__lastModified__ = "2019-06-12 14:12 CST"
__scriptVersion__ = "2g testing"
__lastModified__ = "2019-06-12 23:29 CST"
if False:
PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
@@ -136,7 +143,8 @@ class ObjectProfile(PathProfileBase.ObjectProfile):
tup = clnBase, sub, tag, angle, axis, clnStock
else:
PathLog.debug(str(sub) + ": no rotation used")
if self.warnDisabledAxis(obj, axis) is False:
PathLog.debug(str(sub) + ": No rotation used")
axis = 'X'
angle = 0.0
tag = base.Name + '_' + axis + str(angle).replace('.', '_')
@@ -148,9 +156,8 @@ class ObjectProfile(PathProfileBase.ObjectProfile):
# Efor
# Efor
if subCount > 1:
msg = "Multiple faces in Base Geometry."
msg += " Depth settings will be applied to all faces."
msg = translate("Path", msg)
msg = translate('Path', "Multiple faces in Base Geometry.") + " "
msg += translate('Path', "Depth settings will be applied to all faces.")
PathLog.warning(msg)
#title = translate("Path", "Depth Warning")
#self.guiMessage(title, msg)