Path: Fix rotational errors and organize/simplify some rotational code

Fix rotational alignment and drilling depth issues for rotational drilling ops.
Issues identified in forum at https://forum.freecadweb.org/viewtopic.php?style=3&f=15&t=52262.
Path:
This commit is contained in:
Russell Johnson
2020-11-21 23:07:06 -06:00
parent 43cd864f8f
commit d52a96f2b5
2 changed files with 244 additions and 77 deletions

View File

@@ -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

View File

@@ -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