diff --git a/src/Mod/Path/PathScripts/PathCircularHoleBase.py b/src/Mod/Path/PathScripts/PathCircularHoleBase.py index 66a0441048..3fe35a55ca 100644 --- a/src/Mod/Path/PathScripts/PathCircularHoleBase.py +++ b/src/Mod/Path/PathScripts/PathCircularHoleBase.py @@ -244,77 +244,47 @@ class ObjectOp(PathOp.ObjectOp): baseSubsTuples.append((base, subList, 0.0, 'A', stock)) else: for p in range(0, len(obj.Base)): - (base, subsList) = obj.Base[p] - for sub in subsList: - if self.isHoleEnabled(obj, base, sub): - shape = getattr(base.Shape, sub) - rtn = False - (norm, surf) = self.getFaceNormAndSurf(shape) - (rtn, angle, axis, praInfo) = self.faceRotationAnalysis(obj, norm, surf) # pylint: disable=unused-variable - if rtn is True: - (clnBase, angle, clnStock, tag) = self.applyRotationalAnalysis(obj, base, angle, axis, subCount) - # Verify faces are correctly oriented - InverseAngle might be necessary - PathLog.debug("Verifying {} orientation: running faceRotationAnalysis() again.".format(sub)) - faceIA = getattr(clnBase.Shape, sub) - (norm, surf) = self.getFaceNormAndSurf(faceIA) - (rtn, praAngle, praAxis, praInfo) = self.faceRotationAnalysis(obj, norm, surf) # pylint: disable=unused-variable - if rtn is True: - msg = obj.Name + ":: " - msg += translate("Path", "{} might be misaligned after initial rotation.".format(sub)) + " " - if obj.AttemptInverseAngle is True and obj.InverseAngle is False: - (clnBase, clnStock, angle) = self.applyInverseAngle(obj, clnBase, clnStock, axis, angle) - msg += translate("Path", "Rotated to 'InverseAngle' to attempt access.") - else: - if len(subsList) == 1: - msg += translate("Path", "Consider toggling the 'InverseAngle' property and recomputing.") - else: - msg += translate("Path", "Consider transferring '{}' to independent operation.".format(sub)) - PathLog.warning(msg) - # title = translate("Path", 'Rotation Warning') - # self.guiMessage(title, msg, False) - else: - PathLog.debug("Face appears to be oriented correctly.") - - cmnt = "{}: {} @ {}; ".format(sub, axis, str(round(angle, 5))) - if cmnt not in obj.Comment: - obj.Comment += cmnt - - tup = clnBase, sub, tag, angle, axis, clnStock - allTuples.append(tup) - else: - if self.warnDisabledAxis(obj, axis, sub) is True: - pass # Skip drill feature due to access issue - else: - PathLog.debug(str(sub) + ": No rotation used") - axis = 'X' - angle = 0.0 - tag = base.Name + '_' + axis + str(angle).replace('.', '_') - stock = PathUtils.findParentJob(obj).Stock - tup = base, sub, tag, angle, axis, stock - allTuples.append(tup) - # Eif - # Eif - subCount += 1 - # Efor - # Efor - (Tags, Grps) = self.sortTuplesByIndex(allTuples, 2) # return (TagList, GroupList) - subList = [] - for o in range(0, len(Tags)): - PathLog.debug('hTag: {}'.format(Tags[o])) - subList = [] - for (base, sub, tag, angle, axis, stock) in Grps[o]: - subList.append(sub) - pair = base, subList, angle, axis, stock - baseSubsTuples.append(pair) - # Efor + (bst, at) = self.process_base_geometry_with_rotation(obj, p, subCount) + allTuples.extend(at) + baseSubsTuples.extend(bst) for base, subs, angle, axis, stock in baseSubsTuples: + # rotate shorter angle in opposite direction + if angle > 180: + angle -= 360 + elif angle < -180: + angle += 360 + + # Re-analyze rotated model for drillable holes + if obj.EnableRotation != 'Off': + rotated_features = self.findHoles(obj, base) + for sub in subs: + PathLog.debug('sub, angle, axis: {}, {}, {}'.format(sub, angle, axis)) if self.isHoleEnabled(obj, base, sub): pos = self.holePosition(obj, base, sub) if pos: - # Default is treat selection as 'Face' shape - holeBtm = base.Shape.getElement(sub).BoundBox.ZMin + # Identify face to which edge belongs + sub_shape = base.Shape.getElement(sub) + + # Default is to treat selection as 'Face' shape + holeBtm = sub_shape.BoundBox.ZMin + + if obj.EnableRotation != 'Off': + # Update Start and Final depths due to rotation, if auto defaults are active + parent_face = self._find_parent_face_of_edge(rotated_features, sub_shape) + if parent_face: + PathLog.debug('parent_face found') + holeBtm = parent_face.BoundBox.ZMin + if obj.OpStartDepth == obj.StartDepth: + obj.StartDepth.Value = parent_face.BoundBox.ZMax + PathLog.debug('new StartDepth: {}'.format(obj.StartDepth.Value)) + if obj.OpFinalDepth == obj.FinalDepth: + obj.FinalDepth.Value = holeBtm + PathLog.debug('new FinalDepth: {}'.format(holeBtm)) + else: + PathLog.debug('NO parent_face identified') + if base.Shape.getElement(sub).ShapeType == 'Edge': msg = translate("Path", "Verify Final Depth of holes based on edges. {} depth is: {} mm".format(sub, round(holeBtm, 4))) + " " msg += translate("Path", "Always select the bottom edge of the hole when using an edge.") @@ -837,3 +807,183 @@ class ObjectOp(PathOp.ObjectOp): return True else: 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 + + curve = face.OuterWire.Edges[0].Curve + if curve.TypeId == "Part::GeomCircle": + center = curve.Center + radius = curve.Radius * 1. + face = Part.Face(Part.Wire(Part.makeCircle(radius, center))) + + 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: + 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 + + PathLog.debug('isFaceUp - exit True') + return True + + 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 paths. + ''' + baseSubsTuples = [] + allTuples = [] + + (base, subsList) = obj.Base[p] + + 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) + + return (baseSubsTuples, allTuples) + + 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. + ''' + + rtn = False + face = base.Shape.getElement(sub) + + if sub[:4] != 'Face': + if face.ShapeType == 'Edge': + edgToFace = Part.Face(Part.Wire(Part.__sortEdges__([face]))) + face = edgToFace + else: + ignoreSub = base.Name + '.' + sub + PathLog.error(translate('Path', "Selected feature is not a Face. Ignoring: {}".format(ignoreSub))) + return False + + (norm, surf) = self.getFaceNormAndSurf(face) + (rtn, angle, axis, praInfo) = self.faceRotationAnalysis(obj, norm, surf) # pylint: disable=unused-variable + PathLog.debug("initial rotational analysis: {}".format(praInfo)) + + clnBase = base + faceIA = clnBase.Shape.getElement(sub) + if faceIA.ShapeType == 'Edge': + edgToFace = Part.Face(Part.Wire(Part.__sortEdges__([faceIA]))) + faceIA = edgToFace + + 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) + if faceIA.ShapeType == 'Edge': + edgToFace = Part.Face(Part.Wire(Part.__sortEdges__([faceIA]))) + faceIA = edgToFace + + (norm, surf) = self.getFaceNormAndSurf(faceIA) + (rtn, praAngle, praAxis, praInfo2) = self.faceRotationAnalysis(obj, norm, surf) # pylint: disable=unused-variable + PathLog.debug("follow-up rotational analysis: {}".format(praInfo2)) + + isFaceUp = self.isFaceUp(clnBase, faceIA) + PathLog.debug('... initial isFaceUp: {}'.format(isFaceUp)) + + if isFaceUp: + rtn = False + PathLog.debug('returning analysis: {}, {}'.format(praAngle, praAxis)) + return (clnBase, [sub], angle, axis, clnStock) + + 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 faceIA.ShapeType == 'Edge': + edgToFace = Part.Face(Part.Wire(Part.__sortEdges__([faceIA]))) + faceIA = edgToFace + 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) + + def _find_parent_face_of_edge(self, rotated_features, test_shape): + '''_find_parent_face_of_edge(rotated_features, test_shape)... + Compare test_shape with each within rotated_features to identify + and return the parent face of the test_shape, if it exists.''' + for (base, sub) in rotated_features: + sub_shape = base.Shape.getElement(sub) + if test_shape.isSame(sub_shape): + return sub_shape + elif test_shape.isEqual(sub_shape): + return sub_shape + else: + for e in sub_shape.Edges: + if test_shape.isSame(e): + return sub_shape + elif test_shape.isEqual(e): + return sub_shape + return False diff --git a/src/Mod/Path/PathScripts/PathDrilling.py b/src/Mod/Path/PathScripts/PathDrilling.py index a646479e8b..3be3a859cb 100644 --- a/src/Mod/Path/PathScripts/PathDrilling.py +++ b/src/Mod/Path/PathScripts/PathDrilling.py @@ -119,7 +119,6 @@ class ObjectDrilling(PathCircularHoleBase.ObjectOp): params = {} params['X'] = p['x'] params['Y'] = p['y'] - params.update(cmdParams) if obj.EnableRotation != 'Off': angle = p['angle'] axis = p['axis'] @@ -148,7 +147,14 @@ class ObjectDrilling(PathCircularHoleBase.ObjectOp): # Prepare for drilling cycle self.commandlist.append(Path.Command('G0', {axisOfRot: angle, 'F': self.axialRapid})) self.commandlist.append(Path.Command('G0', {'X': p['x'], 'Y': p['y'], 'F': self.horizRapid})) - self.commandlist.append(Path.Command('G1', {'Z': p['stkTop'], 'F': self.vertFeed})) + self.commandlist.append(Path.Command('G1', {'Z': obj.StartDepth.Value, 'F': self.vertFeed})) + + # Update retract height due to rotation + self.opSetDefaultRetractHeight(obj) + cmdParams['R'] = obj.RetractHeight.Value + + # Update changes to parameters + params.update(cmdParams) # Perform canned drilling cycle self.commandlist.append(Path.Command(cmd, params)) @@ -166,23 +172,34 @@ class ObjectDrilling(PathCircularHoleBase.ObjectOp): self.commandlist.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})) self.commandlist.append(Path.Command('G0', {lastAxis: 0.0, 'F': self.axialRapid})) - def opSetDefaultValues(self, obj, job): - '''opSetDefaultValues(obj, job) ... set default value for RetractHeight''' + def opSetDefaultRetractHeight(self, obj, job=None): + '''opSetDefaultRetractHeight(obj, job) ... set default Retract Height value''' - parentJob = PathUtils.findParentJob(obj) + has_job = True + if not job: + job = PathUtils.findParentJob(obj) + has_job = False - if hasattr(parentJob.SetupSheet, 'RetractHeight'): - obj.RetractHeight = parentJob.SetupSheet.RetractHeight + if hasattr(job.SetupSheet, 'RetractHeight'): + obj.RetractHeight = job.SetupSheet.RetractHeight elif self.applyExpression(obj, 'RetractHeight', 'OpStartDepth+1mm'): - obj.RetractHeight = 10 + if has_job: + obj.RetractHeight = 10 + else: + obj.RetractHeight.Value = obj.StartDepth.Value + 1.0 - if hasattr(parentJob.SetupSheet, 'PeckDepth'): - obj.PeckDepth = parentJob.SetupSheet.PeckDepth + def opSetDefaultValues(self, obj, job): + '''opSetDefaultValues(obj, job) ... Set default property values''' + + self.opSetDefaultRetractHeight(obj, job) + + if hasattr(job.SetupSheet, 'PeckDepth'): + obj.PeckDepth = job.SetupSheet.PeckDepth elif self.applyExpression(obj, 'PeckDepth', 'OpToolDiameter*0.75'): obj.PeckDepth = 1 - if hasattr(parentJob.SetupSheet, 'DwellTime'): - obj.DwellTime = parentJob.SetupSheet.DwellTime + if hasattr(job.SetupSheet, 'DwellTime'): + obj.DwellTime = job.SetupSheet.DwellTime else: obj.DwellTime = 1