From 760fa482ef8ed28b5022423f06ffcce428923c4b Mon Sep 17 00:00:00 2001
From: Russell Johnson <47639332+Russ4262@users.noreply.github.com>
Date: Sat, 3 Oct 2020 14:00:55 -0500
Subject: [PATCH 1/7] Path: Remove null comments and improve debugging
Added method to create a FreeCAD object when in debug mode.
---
src/Mod/Path/PathScripts/PathPocketShape.py | 18 ++++++++----------
1 file changed, 8 insertions(+), 10 deletions(-)
diff --git a/src/Mod/Path/PathScripts/PathPocketShape.py b/src/Mod/Path/PathScripts/PathPocketShape.py
index 139b06d684..4bb5068d2d 100644
--- a/src/Mod/Path/PathScripts/PathPocketShape.py
+++ b/src/Mod/Path/PathScripts/PathPocketShape.py
@@ -321,6 +321,7 @@ 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 = []
@@ -426,8 +427,6 @@ class ObjectPocket(PathPocketBase.ObjectPocket):
msg += translate('Path', "\n
Bottom of pocket might be non-planar and/or not normal to spindle axis.")
msg += translate('Path', "\n
\n
3D pocket bottom is NOT available in this operation.")
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)))
@@ -614,15 +613,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 +628,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):
@@ -910,6 +901,13 @@ class ObjectPocket(PathPocketBase.ObjectPocket):
return (go, norm, surf)
+ # Method to add temporary debug object
+ def _addDebugObject(self, objName, objShape):
+ if self.isDebug:
+ O = FreeCAD.ActiveDocument.addObject('Part::Feature', 'debug_' + objName)
+ O.Shape = objShape
+ O.purgeTouched()
+
def SetupProperties():
setup = PathPocketBase.SetupProperties()
From 947bfa345db16883f83630f7ca6a2b62d80d4c2d Mon Sep 17 00:00:00 2001
From: Russell Johnson <47639332+Russ4262@users.noreply.github.com>
Date: Thu, 1 Oct 2020 22:25:06 -0500
Subject: [PATCH 2/7] Path: Improve accuracy of `isFaceUp()` method
This method is used in rotational operations and some standard operations to identify if a face's normal-direction Z value is +1.0
---
src/Mod/Path/PathScripts/PathAreaOp.py | 38 +++++++++++++++++++++-----
1 file changed, 31 insertions(+), 7 deletions(-)
diff --git a/src/Mod/Path/PathScripts/PathAreaOp.py b/src/Mod/Path/PathScripts/PathAreaOp.py
index c8a4686809..7a1cc8e0ee 100644
--- a/src/Mod/Path/PathScripts/PathAreaOp.py
+++ b/src/Mod/Path/PathScripts/PathAreaOp.py
@@ -985,19 +985,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
From 45832af35b9f111b1656d846176bd65c27b6c9ec Mon Sep 17 00:00:00 2001
From: Russell Johnson <47639332+Russ4262@users.noreply.github.com>
Date: Thu, 1 Oct 2020 22:31:39 -0500
Subject: [PATCH 3/7] Path: Fix and cleanup removal shape creation code
The `isFaceUp()` method in the PathAreaOp module was returning false results in some instances.
Rotational code has been consolidated and better organized to fix certain errors reported in the forum.
Overall, this code section has been better organized as a part of the necessary fixes.
---
src/Mod/Path/PathScripts/PathPocketShape.py | 73 +++++++++++----------
1 file changed, 40 insertions(+), 33 deletions(-)
diff --git a/src/Mod/Path/PathScripts/PathPocketShape.py b/src/Mod/Path/PathScripts/PathPocketShape.py
index 4bb5068d2d..338cb763e1 100644
--- a/src/Mod/Path/PathScripts/PathPocketShape.py
+++ b/src/Mod/Path/PathScripts/PathPocketShape.py
@@ -645,48 +645,55 @@ 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
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 is True:
+ if face.BoundBox.ZMin > obj.FinalDepth.Value:
+ PathLog.debug('face.BoundBox.ZMin > obj.FinalDepth.Value')
+ # Raise FinalDepth to face depth
+ adj_final_dep = faceZMin # face.BoundBox.ZMin
+ # 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
From cae33892e1ff06789e21acf497b6fb3c177e97ea Mon Sep 17 00:00:00 2001
From: Russell Johnson <47639332+Russ4262@users.noreply.github.com>
Date: Sat, 3 Oct 2020 14:50:53 -0500
Subject: [PATCH 4/7] Path: Organize rotational code in `areaOpShapes()` method
Relocate the rotational analysis code into smaller independent methods to allow for easier maintenance and support.
Commented out recently added debug object creation statement.
---
src/Mod/Path/PathScripts/PathPocketShape.py | 563 +++++++++++---------
1 file changed, 312 insertions(+), 251 deletions(-)
diff --git a/src/Mod/Path/PathScripts/PathPocketShape.py b/src/Mod/Path/PathScripts/PathPocketShape.py
index 338cb763e1..006616db1e 100644
--- a/src/Mod/Path/PathScripts/PathPocketShape.py
+++ b/src/Mod/Path/PathScripts/PathPocketShape.py
@@ -323,262 +323,25 @@ class ObjectPocket(PathPocketBase.ObjectPocket):
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', "Verify depth of pocket for '{}'.".format(sub))
- msg += translate('Path', "\n
Pocket is based on extruded surface.")
- msg += translate('Path', "\n
Bottom of pocket might be non-planar and/or not normal to spindle axis.")
- msg += translate('Path', "\n
\n
3D pocket bottom is NOT available in this operation.")
- 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
+ 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
@@ -591,7 +354,7 @@ class ObjectPocket(PathPocketBase.ObjectPocket):
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.'))
@@ -649,6 +412,7 @@ class ObjectPocket(PathPocketBase.ObjectPocket):
# 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:
isFaceUp = True
invZ = 0.0
@@ -676,11 +440,11 @@ class ObjectPocket(PathPocketBase.ObjectPocket):
useAngle -= 360.0
# Apply LimitDepthToFace property for rotational operations
- if obj.LimitDepthToFace is True:
- if face.BoundBox.ZMin > obj.FinalDepth.Value:
- PathLog.debug('face.BoundBox.ZMin > obj.FinalDepth.Value')
+ 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
+ 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
@@ -908,8 +672,305 @@ 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)
+
+ 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):
+ '''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', "Verify depth of pocket for '{}'.".format(sub))
+ msg += translate('Path', "\n
Pocket is based on extruded surface.")
+ msg += translate('Path', "\n
Bottom of pocket might be non-planar and/or not normal to spindle axis.")
+ msg += translate('Path', "\n
\n
3D pocket bottom is NOT available in this operation.")
+ 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
From 5bb979e34a7e9091e85edfa6e442a17714703452 Mon Sep 17 00:00:00 2001
From: Russell Johnson <47639332+Russ4262@users.noreply.github.com>
Date: Wed, 14 Oct 2020 20:50:06 -0500
Subject: [PATCH 5/7] Path: LGTM cleanup
---
src/Mod/Path/PathScripts/PathAreaOp.py | 29 +++++++++++----------
src/Mod/Path/PathScripts/PathPocketShape.py | 14 +++++-----
2 files changed, 22 insertions(+), 21 deletions(-)
diff --git a/src/Mod/Path/PathScripts/PathAreaOp.py b/src/Mod/Path/PathScripts/PathAreaOp.py
index 7a1cc8e0ee..1144a44c8a 100644
--- a/src/Mod/Path/PathScripts/PathAreaOp.py
+++ b/src/Mod/Path/PathScripts/PathAreaOp.py
@@ -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))
@@ -326,7 +325,7 @@ class ObjectOp(PathOp.ObjectOp):
self.endVector = end_vector # pylint: disable=attribute-defined-outside-init
simobj = None
- if getsim and False:
+ if getsim:
areaParams['ToolRadius'] = self.radius - self.radius * .005
area.setParams(**areaParams)
sec = area.makeSections(mode=0, project=False, heights=heights)[-1].getShape()
@@ -356,6 +355,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,23 +365,23 @@ 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
@@ -403,8 +404,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)
diff --git a/src/Mod/Path/PathScripts/PathPocketShape.py b/src/Mod/Path/PathScripts/PathPocketShape.py
index 006616db1e..afdac350da 100644
--- a/src/Mod/Path/PathScripts/PathPocketShape.py
+++ b/src/Mod/Path/PathScripts/PathPocketShape.py
@@ -350,7 +350,7 @@ 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:
@@ -539,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
@@ -555,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
@@ -705,7 +705,7 @@ class ObjectPocket(PathPocketBase.ObjectPocket):
if FreeCAD.ActiveDocument.getObject(fName):
FreeCAD.ActiveDocument.removeObject(fName)
- tmpFace = FreeCAD.ActiveDocument.addObject('Part::Feature', fName).Shape = mFF
+ tmpFaceObj = FreeCAD.ActiveDocument.addObject('Part::Feature', fName).Shape = mFF
tmpFace = FreeCAD.ActiveDocument.getObject(fName)
tmpFace.purgeTouched()
From 6046e0600b249694d44829329a837cdce45031b5 Mon Sep 17 00:00:00 2001
From: Russell Johnson <47639332+Russ4262@users.noreply.github.com>
Date: Fri, 23 Oct 2020 09:52:14 -0500
Subject: [PATCH 6/7] Path: Fix div by zero error
Fix MillFace div by zero error reported in forum at https://forum.freecadweb.org/viewtopic.php?f=15&t=51415.
Rotational-related variables were exposed to non-rotational code block. They have been moved into correct rotational code block.
---
src/Mod/Path/PathScripts/PathAreaOp.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/Mod/Path/PathScripts/PathAreaOp.py b/src/Mod/Path/PathScripts/PathAreaOp.py
index 1144a44c8a..2626aa71f9 100644
--- a/src/Mod/Path/PathScripts/PathAreaOp.py
+++ b/src/Mod/Path/PathScripts/PathAreaOp.py
@@ -383,10 +383,10 @@ class ObjectOp(PathOp.ObjectOp):
if PathLog.getLevel(PathLog.thisModule()) == 4:
self.visualAxis()
- # 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)
From 60249f543c8be8fe9f1f996fb9857d7acd2e08db Mon Sep 17 00:00:00 2001
From: Russell Johnson <47639332+Russ4262@users.noreply.github.com>
Date: Wed, 28 Oct 2020 11:35:09 -0500
Subject: [PATCH 7/7] Path: Delete irrelevant code
---
src/Mod/Path/PathScripts/PathAreaOp.py | 8 +-------
1 file changed, 1 insertion(+), 7 deletions(-)
diff --git a/src/Mod/Path/PathScripts/PathAreaOp.py b/src/Mod/Path/PathScripts/PathAreaOp.py
index 2626aa71f9..d68e4676dd 100644
--- a/src/Mod/Path/PathScripts/PathAreaOp.py
+++ b/src/Mod/Path/PathScripts/PathAreaOp.py
@@ -322,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:
- 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