Merge pull request #3916 from Russ4262/fix_PocketShape

Path: Pocket_Shape - Improved fixes for FinalDepth and rotational issues
This commit is contained in:
sliptonic
2020-11-17 17:31:31 -06:00
committed by GitHub
2 changed files with 412 additions and 327 deletions

View File

@@ -290,7 +290,6 @@ class ObjectOp(PathOp.ObjectOp):
paths = []
heights = [i for i in self.depthparams]
PathLog.debug('depths: {}'.format(heights))
lstIdx = len(heights) - 1
for i in range(0, len(heights)):
for baseShape in edgeList:
hWire = Part.Wire(Part.__sortEdges__(baseShape.Edges))
@@ -323,14 +322,8 @@ class ObjectOp(PathOp.ObjectOp):
paths.extend(pp.Commands)
PathLog.debug('pp: {}, end vector: {}'.format(pp, end_vector))
self.endVector = end_vector # pylint: disable=attribute-defined-outside-init
self.endVector = end_vector
simobj = None
if getsim and False:
areaParams['ToolRadius'] = self.radius - self.radius * .005
area.setParams(**areaParams)
sec = area.makeSections(mode=0, project=False, heights=heights)[-1].getShape()
simobj = sec.extrude(FreeCAD.Vector(0, 0, baseobject.BoundBox.ZMax))
return paths, simobj
@@ -356,6 +349,8 @@ class ObjectOp(PathOp.ObjectOp):
self.useTempJobClones('Delete') # Clear temporary group and recreate for temp job clones
self.rotStartDepth = None # pylint: disable=attribute-defined-outside-init
start_depth = obj.StartDepth.Value
final_depth = obj.FinalDepth.Value
if obj.EnableRotation != 'Off':
# Calculate operation heights based upon rotation radii
opHeights = self.opDetermineRotationRadii(obj)
@@ -364,28 +359,28 @@ class ObjectOp(PathOp.ObjectOp):
# Set clearance and safe heights based upon rotation radii
if obj.EnableRotation == 'A(x)':
strDep = self.xRotRad
start_depth = self.xRotRad
elif obj.EnableRotation == 'B(y)':
strDep = self.yRotRad
start_depth = self.yRotRad
else:
strDep = max(self.xRotRad, self.yRotRad)
finDep = -1 * strDep
start_depth = max(self.xRotRad, self.yRotRad)
final_depth = -1 * start_depth
self.rotStartDepth = strDep
obj.ClearanceHeight.Value = strDep + self.clrOfset
obj.SafeHeight.Value = strDep + self.safOfst
self.rotStartDepth = start_depth
# The next two lines are improper code.
# The ClearanceHeight and SafeHeight need to be set in opSetDefaultValues() method.
# They should not be redefined here, so this entire `if...:` statement needs relocated.
obj.ClearanceHeight.Value = start_depth + self.clrOfset
obj.SafeHeight.Value = start_depth + self.safOfst
# Create visual axes when debugging.
if PathLog.getLevel(PathLog.thisModule()) == 4:
self.visualAxis()
else:
strDep = obj.StartDepth.Value
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 # pylint: disable=attribute-defined-outside-init
self.axialRapid = 360 / safeCircum * self.horizRapid # pylint: disable=attribute-defined-outside-init
# Set axial feed rates based upon horizontal feed rates
safeCircum = 2 * math.pi * obj.SafeHeight.Value
self.axialFeed = 360 / safeCircum * self.horizFeed # pylint: disable=attribute-defined-outside-init
self.axialRapid = 360 / safeCircum * self.horizRapid # pylint: disable=attribute-defined-outside-init
# Initiate depthparams and calculate operation heights for rotational operation
self.depthparams = self._customDepthParams(obj, obj.StartDepth.Value, obj.FinalDepth.Value)
@@ -403,8 +398,8 @@ class ObjectOp(PathOp.ObjectOp):
for shp in aOS:
if len(shp) == 2:
(fc, iH) = shp
# fc, iH, sub, angle, axis, strtDep, finDep
tup = fc, iH, 'otherOp', 0.0, 'S', obj.StartDepth.Value, obj.FinalDepth.Value
# fc, iH, sub, angle, axis, strtDep, finDep
tup = fc, iH, 'otherOp', 0.0, 'S', start_depth, final_depth
shapes.append(tup)
else:
shapes.append(shp)
@@ -985,19 +980,43 @@ class ObjectOp(PathOp.ObjectOp):
return False
def isFaceUp(self, base, face):
'''isFaceUp(base, face) ...
When passed a base object and face shape, returns True if face is up.
This method is used to identify correct rotation of a model.
'''
# verify face is normal to Z+-
(norm, surf) = self.getFaceNormAndSurf(face)
if round(abs(norm.z), 8) != 1.0 or round(abs(surf.z), 8) != 1.0:
PathLog.debug('isFaceUp - face not oriented normal to Z+-')
return False
up = face.extrude(FreeCAD.Vector(0.0, 0.0, 5.0))
dwn = face.extrude(FreeCAD.Vector(0.0, 0.0, -5.0))
upCmn = base.Shape.common(up)
dwnCmn = base.Shape.common(dwn)
# Identify orientation based on volumes of common() results
if len(upCmn.Edges) > 0 and round(upCmn.Volume, 6) == 0.0:
if len(upCmn.Edges) > 0:
PathLog.debug('isFaceUp - HAS up edges\n')
if len(dwnCmn.Edges) > 0:
PathLog.debug('isFaceUp - up and dwn edges\n')
dVol = round(dwnCmn.Volume, 6)
uVol = round(upCmn.Volume, 6)
if uVol > dVol:
return False
return True
else:
if round(upCmn.Volume, 6) == 0.0:
return True
return False
elif len(dwnCmn.Edges) > 0:
PathLog.debug('isFaceUp - HAS dwn edges only\n')
dVol = round(dwnCmn.Volume, 6)
if dVol == 0.0:
return False
return True
elif len(dwnCmn.Edges) > 0 and round(dwnCmn.Volume, 6) == 0.0:
return False
if (len(upCmn.Edges) > 0 and len(dwnCmn.Edges) > 0 and
round(dwnCmn.Volume, 6) > round(upCmn.Volume, 6)):
return True
return False
PathLog.debug('isFaceUp - exit True\n')
return True
def _customDepthParams(self, obj, strDep, finDep):
finish_step = obj.FinishDepth.Value if hasattr(obj, "FinishDepth") else 0.0

View File

@@ -321,265 +321,27 @@ class ObjectPocket(PathPocketBase.ObjectPocket):
PathLog.track()
PathLog.debug("----- areaOpShapes() in PathPocketShape.py")
self.isDebug = True if PathLog.getLevel(PathLog.thisModule()) == 4 else False
baseSubsTuples = []
subCount = 0
allTuples = []
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)
# Create planar face from edge
mFF = Part.Face(Part.Wire(Part.__sortEdges__([edg])))
if mFF.isNull():
PathLog.debug('Face(Part.Wire()) failed')
else:
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()
if minArea == 0.0:
minArea = tmpFace.Shape.Face1.Area
useFace = fName
planar = True
elif tmpFace.Shape.Face1.Area < minArea:
minArea = tmpFace.Shape.Face1.Area
FreeCAD.ActiveDocument.removeObject(useFace)
useFace = fName
else:
FreeCAD.ActiveDocument.removeObject(fName)
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')
if PathGeom.isVertical(face.Surface.Axis):
PathLog.debug(' -isVertical()')
# it's a flat horizontal face
self.horiz.append(face)
return True
elif PathGeom.isHorizontal(face.Surface.Axis):
PathLog.debug(' -isHorizontal()')
self.vert.append(face)
return True
else:
return False
elif type(face.Surface) == Part.Cylinder and PathGeom.isVertical(face.Surface.Axis):
PathLog.debug('type() == Part.Cylinder')
# vertical cylinder wall
if any(e.isClosed() for e in face.Edges):
PathLog.debug(' -e.isClosed()')
# complete cylinder
circle = Part.makeCircle(face.Surface.Radius, face.Surface.Center)
disk = Part.Face(Part.Wire(circle))
disk.translate(FreeCAD.Vector(0, 0, face.BoundBox.ZMin - disk.BoundBox.ZMin))
self.horiz.append(disk)
return True
else:
PathLog.debug(' -none isClosed()')
# partial cylinder wall
self.vert.append(face)
return True
elif type(face.Surface) == Part.SurfaceOfExtrusion:
# extrusion wall
PathLog.debug('type() == Part.SurfaceOfExtrusion')
# 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:
uFace = FreeCAD.ActiveDocument.getObject(useFace)
self.horiz.append(uFace.Shape.Faces[0])
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.warning(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(face.Surface): {}'.format(type(face.Surface)))
return False
subCount = 0
if obj.Base:
PathLog.debug('Processing... obj.Base')
PathLog.debug('Processing obj.Base')
self.removalshapes = [] # pylint: disable=attribute-defined-outside-init
if obj.EnableRotation == 'Off':
stock = PathUtils.findParentJob(obj).Stock
for (base, subList) in obj.Base:
baseSubsTuples.append((base, subList, 0.0, 'X', stock))
tup = (base, subList, 0.0, 'X', stock)
baseSubsTuples.append(tup)
else:
PathLog.debug('Rotation is active...')
PathLog.debug('... Rotation is active')
# method call here
for p in range(0, len(obj.Base)):
(base, subsList) = obj.Base[p]
isLoop = False
# First, check all subs collectively for loop of faces
if len(subsList) > 2:
(isLoop, norm, surf) = self.checkForFacesLoop(base, subsList)
if isLoop is True:
PathLog.debug("Common Surface.Axis or normalAt() value found for loop faces.")
rtn = False
subCount += 1
(rtn, angle, axis, praInfo) = self.faceRotationAnalysis(obj, norm, surf) # pylint: disable=unused-variable
PathLog.debug("angle: {}; axis: {}".format(angle, axis))
if rtn is True:
faceNums = ""
for f in subsList:
faceNums += '_' + f.replace('Face', '')
(clnBase, angle, clnStock, tag) = self.applyRotationalAnalysis(obj, base, angle, axis, faceNums) # pylint: disable=unused-variable
# Verify faces are correctly oriented - InverseAngle might be necessary
PathLog.debug("Checking if faces are oriented correctly after rotation...")
for sub in subsList:
face = clnBase.Shape.getElement(sub)
if type(face.Surface) == Part.Plane:
if not PathGeom.isHorizontal(face.Surface.Axis):
rtn = False
PathLog.warning(translate("PathPocketShape", "Face appears to NOT be horizontal AFTER rotation applied."))
break
if rtn is False:
PathLog.debug(translate("Path", "Face appears misaligned after initial rotation.") + ' 1')
if obj.InverseAngle:
(clnBase, clnStock, angle) = self.applyInverseAngle(obj, clnBase, clnStock, axis, angle)
else:
if obj.AttemptInverseAngle is True:
(clnBase, clnStock, angle) = self.applyInverseAngle(obj, clnBase, clnStock, axis, angle)
else:
msg = translate("Path", "Consider toggling the 'InverseAngle' property and recomputing.")
PathLog.warning(msg)
if angle < 0.0:
angle += 360.0
tup = clnBase, subsList, angle, axis, clnStock
else:
if self.warnDisabledAxis(obj, axis) is False:
PathLog.debug("No rotation used")
axis = 'X'
angle = 0.0
stock = PathUtils.findParentJob(obj).Stock
tup = base, subsList, angle, axis, stock
# Eif
allTuples.append(tup)
baseSubsTuples.append(tup)
# Eif
if isLoop is False:
PathLog.debug(translate('Path', "Processing subs individually ..."))
for sub in subsList:
subCount += 1
if 'Face' in sub:
rtn = False
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 created: {}'.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) # pylint: disable=unused-variable
PathLog.debug("initial {}".format(praInfo))
if rtn is True:
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)
(rtn, praAngle, praAxis, praInfo2) = self.faceRotationAnalysis(obj, norm, surf) # pylint: disable=unused-variable
PathLog.debug("follow-up {}".format(praInfo2))
if abs(praAngle) == 180.0:
rtn = False
if self.isFaceUp(clnBase, faceIA) is False:
PathLog.debug('isFaceUp is False')
angle -= 180.0
if rtn is True:
PathLog.debug(translate("Path", "Face appears misaligned after initial rotation.") + ' 2')
if obj.InverseAngle:
(clnBase, clnStock, angle) = self.applyInverseAngle(obj, clnBase, clnStock, axis, angle)
if self.isFaceUp(clnBase, faceIA) is False:
PathLog.debug('isFaceUp is False')
angle += 180.0
else:
if obj.AttemptInverseAngle is True:
(clnBase, clnStock, angle) = self.applyInverseAngle(obj, clnBase, clnStock, axis, angle)
else:
msg = translate("Path", "Consider toggling the 'InverseAngle' property and recomputing.")
PathLog.warning(msg)
if self.isFaceUp(clnBase, faceIA) is False:
PathLog.debug('isFaceUp is False')
angle += 180.0
else:
PathLog.debug("Face appears to be oriented correctly.")
if angle < 0.0:
angle += 360.0
tup = clnBase, [sub], angle, axis, clnStock
else:
if self.warnDisabledAxis(obj, axis) is False:
PathLog.debug(str(sub) + ": No rotation used")
axis = 'X'
angle = 0.0
stock = PathUtils.findParentJob(obj).Stock
tup = base, [sub], angle, axis, stock
# Eif
allTuples.append(tup)
baseSubsTuples.append(tup)
else:
ignoreSub = base.Name + '.' + sub
PathLog.error(translate('Path', "Selected feature is not a Face. Ignoring: {}".format(ignoreSub)))
(bst, at) = self.process_base_geometry_with_rotation(obj, p, subCount)
allTuples.extend(at)
baseSubsTuples.extend(bst)
for o in baseSubsTuples:
self.horiz = [] # pylint: disable=attribute-defined-outside-init
@@ -588,11 +350,11 @@ class ObjectPocket(PathPocketBase.ObjectPocket):
subsList = o[1]
angle = o[2]
axis = o[3]
stock = o[4]
# stock = o[4]
for sub in subsList:
if 'Face' in sub:
if clasifySub(self, subBase, sub) is False:
if not self.clasifySub(subBase, sub):
PathLog.error(translate('PathPocket', 'Pocket does not support shape %s.%s') % (subBase.Label, sub))
if obj.EnableRotation != 'Off':
PathLog.warning(translate('PathPocket', 'Face might not be within rotation accessibility limits.'))
@@ -614,15 +376,11 @@ class ObjectPocket(PathPocketBase.ObjectPocket):
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.warning(msg)
# title = translate('Path', 'Depth Warning')
# self.guiMessage(title, msg, False)
# add faces for extensions
self.exts = [] # pylint: disable=attribute-defined-outside-init
@@ -633,10 +391,6 @@ class ObjectPocket(PathPocketBase.ObjectPocket):
self.horiz.append(face)
self.exts.append(face)
# move all horizontal faces to FinalDepth
# for f in self.horiz:
# f.translate(FreeCAD.Vector(0, 0, obj.FinalDepth.Value - f.BoundBox.ZMin))
# check all faces and see if they are touching/overlapping and combine those into a compound
self.horizontal = [] # pylint: disable=attribute-defined-outside-init
for shape in PathGeom.combineConnectedShapes(self.horiz):
@@ -654,48 +408,56 @@ class ObjectPocket(PathPocketBase.ObjectPocket):
else:
self.horizontal.append(shape)
# move all horizontal faces to FinalDepth
# extrude all faces up to StartDepth and those are the removal shapes
start_dep = obj.StartDepth.Value
clrnc = 0.5
# self._addDebugObject('subBase', subBase.Shape)
for face in self.horizontal:
adj_final_dep = obj.FinalDepth.Value
isFaceUp = True
invZ = 0.0
useAngle = angle
shpZMin = face.BoundBox.ZMin
shpZMinVal = shpZMin
PathLog.debug('self.horizontal pre-shpZMin: {}'.format(shpZMin))
isFaceUp = self.isFaceUp(subBase, face)
if not isFaceUp:
useAngle += 180.0
invZ = (-2 * shpZMin) - clrnc
face.translate(FreeCAD.Vector(0.0, 0.0, invZ))
shpZMin = -1 * shpZMin
else:
face.translate(FreeCAD.Vector(0.0, 0.0, -1 * clrnc))
PathLog.debug('self.horizontal post-shpZMin: {}'.format(shpZMin))
faceZMin = face.BoundBox.ZMin
adj_final_dep = obj.FinalDepth.Value
trans = obj.FinalDepth.Value - face.BoundBox.ZMin
PathLog.debug('face.BoundBox.ZMin: {}'.format(face.BoundBox.ZMin))
if obj.LimitDepthToFace is True and obj.EnableRotation != 'Off':
if shpZMinVal > obj.FinalDepth.Value:
PathLog.debug('shpZMin > obj.FinalDepth.Value')
adj_final_dep = shpZMinVal # shpZMin
if start_dep <= adj_final_dep:
start_dep = adj_final_dep + 1.0
msg = translate('PathPocketShape', 'Start Depth is lower than face depth. Setting to ')
PathLog.warning(msg + ' {} mm.'.format(start_dep))
PathLog.debug('LimitDepthToFace adj_final_dep: {}'.format(adj_final_dep))
else:
translation = obj.FinalDepth.Value - shpZMin
if obj.EnableRotation != 'Off':
PathLog.debug('... running isFaceUp()')
isFaceUp = self.isFaceUp(subBase, face)
# Determine if face is really oriented toward Z+ (rotational purposes)
# ignore for cylindrical faces
if not isFaceUp:
# Check if the `isFaceUp` returned correctly
zDestination = face.BoundBox.ZMin + translation
if (round(start_dep - obj.FinalDepth.Value, 6) !=
round(start_dep - zDestination, 6)):
shpZMin = -1 * shpZMin
face.translate(FreeCAD.Vector(0, 0, translation))
PathLog.debug('... NOT isFaceUp')
useAngle += 180.0
invZ = (-2 * face.BoundBox.ZMin)
face.translate(FreeCAD.Vector(0.0, 0.0, invZ))
faceZMin = face.BoundBox.ZMin # reset faceZMin
PathLog.debug('... face.BoundBox.ZMin: {}'.format(face.BoundBox.ZMin))
else:
PathLog.debug('... isFaceUp')
if useAngle > 180.0:
useAngle -= 360.0
extent = FreeCAD.Vector(0, 0, abs(start_dep - shpZMin) + clrnc) # adj_final_dep + clrnc)
extShp = face.removeSplitter().extrude(extent)
# Apply LimitDepthToFace property for rotational operations
if obj.LimitDepthToFace:
if obj.FinalDepth.Value < face.BoundBox.ZMin:
PathLog.debug('obj.FinalDepth.Value < face.BoundBox.ZMin')
# Raise FinalDepth to face depth
adj_final_dep = faceZMin # face.BoundBox.ZMin # faceZMin
# Ensure StartDepth is above FinalDepth
if start_dep <= adj_final_dep:
start_dep = adj_final_dep + 1.0
msg = translate('PathPocketShape', 'Start Depth is lower than face depth. Setting to ')
PathLog.warning(msg + ' {} mm.'.format(start_dep))
PathLog.debug('LimitDepthToFace adj_final_dep: {}'.format(adj_final_dep))
# Eif
face.translate(FreeCAD.Vector(0.0, 0.0, adj_final_dep - faceZMin - clrnc))
zExtVal = start_dep - adj_final_dep + (2 * clrnc)
extShp = face.removeSplitter().extrude(FreeCAD.Vector(0, 0, zExtVal))
self.removalshapes.append((extShp, False, 'pathPocketShape', useAngle, axis, start_dep, adj_final_dep))
PathLog.debug("Extent values are strDep: {}, finDep: {}, extrd: {}".format(start_dep, adj_final_dep, extent))
PathLog.debug("Extent values are strDep: {}, finDep: {}, extrd: {}".format(start_dep, adj_final_dep, zExtVal))
# Efor face
# Efor
@@ -777,10 +539,10 @@ class ObjectPocket(PathPocketBase.ObjectPocket):
return (False, 0, 0)
else:
tmpWire = FreeCAD.ActiveDocument.addObject('Part::Feature', wireName).Shape = wr
tmpWire = FreeCAD.ActiveDocument.getObject(wireName)
tmpExt = FreeCAD.ActiveDocument.addObject('Part::Extrusion', extName)
tmpWireObj = FreeCAD.ActiveDocument.getObject(wireName)
tmpExtObj = FreeCAD.ActiveDocument.addObject('Part::Extrusion', extName)
tmpExt = FreeCAD.ActiveDocument.getObject(extName)
tmpExt.Base = tmpWire
tmpExt.Base = tmpWireObj
tmpExt.DirMode = "Normal"
tmpExt.DirLink = None
tmpExt.LengthFwd = 10.0
@@ -793,8 +555,8 @@ class ObjectPocket(PathPocketBase.ObjectPocket):
tmpExt.recompute()
tmpExt.purgeTouched()
tmpWire.purgeTouched()
return (True, tmpWire, tmpExt)
tmpWireObj.purgeTouched()
return (True, tmpWireObj, tmpExt)
def roundValue(precision, val):
# Convert VALxe-15 numbers to zero
@@ -910,6 +672,310 @@ class ObjectPocket(PathPocketBase.ObjectPocket):
return (go, norm, surf)
def planarFaceFromExtrusionEdges(self, face, trans):
'''planarFaceFromExtrusionEdges(face, trans)...
Use closed edges to create a temporary face for use in the pocketing operation.
'''
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)
# Create planar face from edge
mFF = Part.Face(Part.Wire(Part.__sortEdges__([edg])))
if mFF.isNull():
PathLog.debug('Face(Part.Wire()) failed')
else:
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)
tmpFaceObj = FreeCAD.ActiveDocument.addObject('Part::Feature', fName).Shape = mFF
tmpFace = FreeCAD.ActiveDocument.getObject(fName)
tmpFace.purgeTouched()
if minArea == 0.0:
minArea = tmpFace.Shape.Face1.Area
useFace = fName
planar = True
elif tmpFace.Shape.Face1.Area < minArea:
minArea = tmpFace.Shape.Face1.Area
FreeCAD.ActiveDocument.removeObject(useFace)
useFace = fName
else:
FreeCAD.ActiveDocument.removeObject(fName)
if useFace != 'useFaceName':
self.useTempJobClones(useFace)
return (planar, useFace)
def clasifySub(self, bs, sub):
'''clasifySub(bs, sub)...
Given a base and a sub-feature name, returns True
if the sub-feature is a horizontally oriented flat face.
'''
face = bs.Shape.getElement(sub)
if type(face.Surface) == Part.Plane:
PathLog.debug('type() == Part.Plane')
if PathGeom.isVertical(face.Surface.Axis):
PathLog.debug(' -isVertical()')
# it's a flat horizontal face
self.horiz.append(face)
return True
elif PathGeom.isHorizontal(face.Surface.Axis):
PathLog.debug(' -isHorizontal()')
self.vert.append(face)
return True
else:
return False
elif type(face.Surface) == Part.Cylinder and PathGeom.isVertical(face.Surface.Axis):
PathLog.debug('type() == Part.Cylinder')
# vertical cylinder wall
if any(e.isClosed() for e in face.Edges):
PathLog.debug(' -e.isClosed()')
# complete cylinder
circle = Part.makeCircle(face.Surface.Radius, face.Surface.Center)
disk = Part.Face(Part.Wire(circle))
disk.translate(FreeCAD.Vector(0, 0, face.BoundBox.ZMin - disk.BoundBox.ZMin))
self.horiz.append(disk)
return True
else:
PathLog.debug(' -none isClosed()')
# partial cylinder wall
self.vert.append(face)
return True
elif type(face.Surface) == Part.SurfaceOfExtrusion:
# extrusion wall
PathLog.debug('type() == Part.SurfaceOfExtrusion')
# Attempt to extract planar face from surface of extrusion
(planar, useFace) = self.planarFaceFromExtrusionEdges(face, trans=True)
# Save face object to self.horiz for processing or display error
if planar is True:
uFace = FreeCAD.ActiveDocument.getObject(useFace)
self.horiz.append(uFace.Shape.Faces[0])
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.warning(msg)
else:
PathLog.error(translate("Path", "Failed to create a planar face from edges in {}.".format(sub)))
else:
PathLog.debug(' -type(face.Surface): {}'.format(type(face.Surface)))
return False
# Process obj.Base with rotation enabled
def process_base_geometry_with_rotation(self, obj, p, subCount):
'''process_base_geometry_with_rotation(obj, p, subCount)...
This method is the control method for analyzing the selected features,
determining their rotational needs, and creating clones as needed
for rotational access for the pocketing operation.
Requires the object, obj.Base index (p), and subCount reference arguments.
Returns two lists of tuples for continued processing into pocket paths.
'''
baseSubsTuples = []
allTuples = []
isLoop = False
(base, subsList) = obj.Base[p]
# First, check all subs collectively for loop of faces
if len(subsList) > 2:
(isLoop, norm, surf) = self.checkForFacesLoop(base, subsList)
if isLoop:
PathLog.debug("Common Surface.Axis or normalAt() value found for loop faces.")
subCount += 1
tup = self.process_looped_sublist(obj, norm, surf)
if tup:
allTuples.append(tup)
baseSubsTuples.append(tup)
# Eif
if not isLoop:
PathLog.debug(translate('Path', "Processing subs individually ..."))
for sub in subsList:
subCount += 1
tup = self.process_nonloop_sublist(obj, base, sub)
if tup:
allTuples.append(tup)
baseSubsTuples.append(tup)
# Eif
return (baseSubsTuples, allTuples)
def process_looped_sublist(self, obj, norm, surf):
'''process_looped_sublist(obj, norm, surf)...
Process set of looped faces when rotation is enabled.
'''
PathLog.debug(translate("Path", "Selected faces form loop. Processing looped faces."))
rtn = False
(rtn, angle, axis, praInfo) = self.faceRotationAnalysis(obj, norm, surf) # pylint: disable=unused-variable
if rtn is True:
faceNums = ""
for f in subsList:
faceNums += '_' + f.replace('Face', '')
(clnBase, angle, clnStock, tag) = self.applyRotationalAnalysis(obj, base, angle, axis, faceNums) # pylint: disable=unused-variable
# Verify faces are correctly oriented - InverseAngle might be necessary
PathLog.debug("Checking if faces are oriented correctly after rotation.")
for sub in subsList:
face = clnBase.Shape.getElement(sub)
if type(face.Surface) == Part.Plane:
if not PathGeom.isHorizontal(face.Surface.Axis):
rtn = False
PathLog.warning(translate("PathPocketShape", "Face appears to NOT be horizontal AFTER rotation applied."))
break
if rtn is False:
PathLog.debug(translate("Path", "Face appears misaligned after initial rotation.") + ' 1')
if obj.InverseAngle:
(clnBase, clnStock, angle) = self.applyInverseAngle(obj, clnBase, clnStock, axis, angle)
else:
if obj.AttemptInverseAngle is True:
(clnBase, clnStock, angle) = self.applyInverseAngle(obj, clnBase, clnStock, axis, angle)
else:
msg = translate("Path", "Consider toggling the 'InverseAngle' property and recomputing.")
PathLog.warning(msg)
if angle < 0.0:
angle += 360.0
tup = clnBase, subsList, angle, axis, clnStock
else:
if self.warnDisabledAxis(obj, axis) is False:
PathLog.debug("No rotation used")
axis = 'X'
angle = 0.0
stock = PathUtils.findParentJob(obj).Stock
tup = base, subsList, angle, axis, stock
# Eif
return tup
def process_nonloop_sublist(self, obj, base, sub):
'''process_nonloop_sublist(obj, sub)...
Process sublist with non-looped set of features when rotation is enabled.
'''
if sub[:4] != 'Face':
ignoreSub = base.Name + '.' + sub
PathLog.error(translate('Path', "Selected feature is not a Face. Ignoring: {}".format(ignoreSub)))
return False
rtn = False
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) = self.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 created: {}'.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) # pylint: disable=unused-variable
PathLog.debug("initial {}".format(praInfo))
clnBase = base
faceIA = clnBase.Shape.getElement(sub)
if rtn is True:
faceNum = sub.replace('Face', '')
PathLog.debug("initial applyRotationalAnalysis")
(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)
(rtn, praAngle, praAxis, praInfo2) = self.faceRotationAnalysis(obj, norm, surf) # pylint: disable=unused-variable
PathLog.debug("follow-up {}".format(praInfo2))
isFaceUp = self.isFaceUp(clnBase, faceIA)
if isFaceUp:
rtn = False
if round(abs(praAngle), 8) == 180.0:
rtn = False
if not isFaceUp:
PathLog.debug('initial isFaceUp is False')
angle = 0.0
# Eif
if rtn:
# initial rotation failed, attempt inverse rotation if user requests it
PathLog.debug(translate("Path", "Face appears misaligned after initial rotation.") + ' 2')
if obj.AttemptInverseAngle:
PathLog.debug(translate("Path", "Applying inverse angle automatically."))
(clnBase, clnStock, angle) = self.applyInverseAngle(obj, clnBase, clnStock, axis, angle)
else:
if obj.InverseAngle:
PathLog.debug(translate("Path", "Applying inverse angle manually."))
(clnBase, clnStock, angle) = self.applyInverseAngle(obj, clnBase, clnStock, axis, angle)
else:
msg = translate("Path", "Consider toggling the 'InverseAngle' property and recomputing.")
PathLog.warning(msg)
faceIA = clnBase.Shape.getElement(sub)
if not self.isFaceUp(clnBase, faceIA):
angle += 180.0
# Normalize rotation angle
if angle < 0.0:
angle += 360.0
elif angle > 360.0:
angle -= 360.0
return (clnBase, [sub], angle, axis, clnStock)
if not self.warnDisabledAxis(obj, axis):
PathLog.debug(str(sub) + ": No rotation used")
axis = 'X'
angle = 0.0
stock = PathUtils.findParentJob(obj).Stock
return (base, [sub], angle, axis, stock)
# Method to add temporary debug object
def _addDebugObject(self, objName, objShape):
'''_addDebugObject(objName, objShape)...
Is passed a desired debug object's desired name and shape.
This method creates a FreeCAD object for debugging purposes.
The created object must be deleted manually from the object tree
by the user.
'''
if self.isDebug:
O = FreeCAD.ActiveDocument.addObject('Part::Feature', 'debug_' + objName)
O.Shape = objShape
O.purgeTouched()
def SetupProperties():
setup = PathPocketBase.SetupProperties()