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