Merge pull request #3916 from Russ4262/fix_PocketShape
Path: Pocket_Shape - Improved fixes for FinalDepth and rotational issues
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user