diff --git a/src/Mod/Path/PathScripts/PathAreaOp.py b/src/Mod/Path/PathScripts/PathAreaOp.py index c8a468680980..d68e4676dd52 100644 --- a/src/Mod/Path/PathScripts/PathAreaOp.py +++ b/src/Mod/Path/PathScripts/PathAreaOp.py @@ -290,7 +290,6 @@ def _buildProfileOpenEdges(self, obj, edgeList, isHole, start, getsim): 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 @@ def _buildProfileOpenEdges(self, obj, edgeList, isHole, start, getsim): 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 @@ def opExecute(self, obj, getsim=False): # pylint: disable=arguments-differ 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 @@ def opExecute(self, obj, getsim=False): # pylint: disable=arguments-differ # 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 @@ def opExecute(self, obj, getsim=False): # pylint: disable=arguments-differ 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 @@ def warnDisabledAxis(self, obj, axis, sub=''): 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: - 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)): + 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 - 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 diff --git a/src/Mod/Path/PathScripts/PathPocketShape.py b/src/Mod/Path/PathScripts/PathPocketShape.py index 139b06d6846c..afdac350daab 100644 --- a/src/Mod/Path/PathScripts/PathPocketShape.py +++ b/src/Mod/Path/PathScripts/PathPocketShape.py @@ -321,265 +321,27 @@ def areaOpShapes(self, obj): 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', "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) - # 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 @@ def clasifySub(self, bs, sub): 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 @@ def clasifySub(self, bs, sub): 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 @@ def clasifySub(self, bs, sub): 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 @@ def clasifySub(self, bs, sub): 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)) - - 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 + 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.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)) - - extent = FreeCAD.Vector(0, 0, abs(start_dep - shpZMin) + clrnc) # adj_final_dep + clrnc) - extShp = face.removeSplitter().extrude(extent) + 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 + + # 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 @@ def makeTempExtrusion(base, sub, fCnt): 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 @@ def makeTempExtrusion(base, sub, fCnt): 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 @@ def roundValue(precision, val): 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', "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 + O.purgeTouched() + def SetupProperties(): setup = PathPocketBase.SetupProperties()