Path: More ambitious step-over and break optimizations

Extend the work from #3496 to allow the safe optimization of more
complex step transitions.

- Use the actual safePDC path for short step transitions and breaks,
  currently up to 2 cutter diameters. This value is chosen to cover
  basically all typical end step-overs, including those with heavy skew.
  Extending this much further (up to the break even point for a retract &
  rapid) will need some careful thinking for multi pass paths.
- Coordinate offset tolerances with per-operation tessellation
  tolerances, to avoid tessellation artifacts messing up paths by
  causing false retracts.  Such retracts can cause entire steps near
  vertical areas to be falsely skipped, which would cause a major
  deviation from the target model.  By considering per-job tolerances, we
  allow users to safely save computational resources by computing roughing
  operations with lower precision, or selectively increase precision for
  finish passes.
- Refine the default tessellation tolerance to GeometryTolerance / 4.
  This makes sure that the job GeometryTolerance is respected by
  operation defaults.
This commit is contained in:
Gabriel Wicke
2020-06-04 21:56:49 -07:00
parent 457dba4fee
commit 84113fbd17
2 changed files with 122 additions and 95 deletions

View File

@@ -249,8 +249,9 @@ class ObjectSurface(PathOp.ObjectOp):
'AvoidLastX_Faces': 0,
'PatternCenterCustom': FreeCAD.Vector(0.0, 0.0, 0.0),
'GapThreshold': 0.005,
'AngularDeflection': 0.25,
'LinearDeflection': 0.0001,
'AngularDeflection': 0.25, # AngularDeflection is unused
# Reasonable compromise between speed & precision
'LinearDeflection': 0.001,
# For debugging
'ShowTempObjects': False
}
@@ -259,12 +260,16 @@ class ObjectSurface(PathOp.ObjectOp):
if hasattr(job, 'GeometryTolerance'):
if job.GeometryTolerance.Value != 0.0:
warn = False
defaults['LinearDeflection'] = job.GeometryTolerance.Value
# Tessellation precision dictates the offsets we need to add to
# avoid false collisions with the model mesh, so make sure we
# default to tessellating with greater precision than the target
# GeometryTolerance.
defaults['LinearDeflection'] = job.GeometryTolerance.Value / 4
if warn:
msg = translate('PathSurface',
'The GeometryTolerance for this Job is 0.0.')
msg += translate('PathSurface',
'Initializing LinearDeflection to 0.0001 mm.')
'Initializing LinearDeflection to 0.001 mm.')
FreeCAD.Console.PrintWarning(msg + '\n')
return defaults
@@ -1027,11 +1032,11 @@ class ObjectSurface(PathOp.ObjectOp):
odd = False
else:
odd = True
minTrnsHght = self._getMinSafeTravelHeight(safePDC, lstStpEnd, first) # Check safe travel height against fullSTL
# cmds.append(Path.Command('N (Transition: last, first: {}, {}: minSTH: {})'.format(lstStpEnd, first, minTrnsHght), {}))
cmds.extend(self._stepTransitionCmds(obj, lstStpEnd, first, minTrnsHght, tolrnc))
# Override default `OptimizeLinearPaths` behavior to allow `ProfileEdges` optimization
cmds.extend(
self._stepTransitionCmds(obj, lstStpEnd, first, safePDC,
tolrnc))
# Override default `OptimizeLinearPaths` behavior to allow
# `ProfileEdges` optimization
if so == peIdx or peIdx == -1:
obj.OptimizeLinearPaths = self.preOLP
@@ -1041,9 +1046,10 @@ class ObjectSurface(PathOp.ObjectOp):
lenPrt = len(prt)
if prt == 'BRK':
nxtStart = PRTS[i + 1][0]
minSTH = self._getMinSafeTravelHeight(safePDC, last, nxtStart) # Check safe travel height against fullSTL
cmds.append(Path.Command('N (Break)', {}))
cmds.extend(self._breakCmds(obj, last, nxtStart, minSTH, tolrnc))
cmds.extend(
self._stepTransitionCmds(obj, last, nxtStart, safePDC,
tolrnc))
else:
cmds.append(Path.Command('N (part {}.)'.format(i + 1), {}))
start = prt[0]
@@ -1105,7 +1111,7 @@ class ObjectSurface(PathOp.ObjectOp):
prev = pnt
pnt = nxt
# Efor
PNTS.pop() # Remove temp end point
return output
@@ -1183,6 +1189,7 @@ class ObjectSurface(PathOp.ObjectOp):
transCmds = list()
if soHasPnts is True:
first = ADJPRTS[0][0] # first point of arc/line stepover group
last = None
# Manage step over transition and CircularZigZag direction
if so > 0:
@@ -1195,9 +1202,9 @@ class ObjectSurface(PathOp.ObjectOp):
# Control step over transition
if prvStpLast is None:
prvStpLast = lastPrvStpLast
minTrnsHght = self._getMinSafeTravelHeight(safePDC, prvStpLast, first, minDep=None) # Check safe travel height against fullSTL
transCmds.append(Path.Command('N (--Step {} transition)'.format(so), {}))
transCmds.extend(self._stepTransitionCmds(obj, prvStpLast, first, minTrnsHght, tolrnc))
transCmds.extend(
self._stepTransitionCmds(obj, prvStpLast, first,
safePDC, tolrnc))
# Override default `OptimizeLinearPaths` behavior to allow `ProfileEdges` optimization
if so == peIdx or peIdx == -1:
@@ -1209,9 +1216,10 @@ class ObjectSurface(PathOp.ObjectOp):
lenPrt = len(prt)
if prt == 'BRK' and prtsHasCmds is True:
nxtStart = ADJPRTS[i + 1][0]
minSTH = self._getMinSafeTravelHeight(safePDC, last, nxtStart, minDep=None) # Check safe travel height against fullSTL
prtsCmds.append(Path.Command('N (--Break)', {}))
prtsCmds.extend(self._breakCmds(obj, last, nxtStart, minSTH, tolrnc))
prtsCmds.extend(
self._stepTransitionCmds(
obj, last, nxtStart, safePDC, tolrnc))
else:
segCmds = False
prtsCmds.append(Path.Command('N (part {})'.format(i + 1), {}))
@@ -1305,7 +1313,7 @@ class ObjectSurface(PathOp.ObjectOp):
else:
PTS.append(FreeCAD.Vector(P.x, P.y, P.z))
# Efor
if optLinTrans is True:
# Remove leading and trailing Hold Points
popList = list()
@@ -1397,76 +1405,69 @@ class ObjectSurface(PathOp.ObjectOp):
return output
def _stepTransitionCmds(self, obj, lstPnt, first, minSTH, tolrnc):
def _stepTransitionCmds(self, obj, p1, p2, safePDC, tolrnc):
"""Generate transition commands / paths between two dropcutter steps or
passes, as well as other kinds of breaks. When
OptimizeStepOverTransitions is enabled, uses safePDC to safely optimize
short (~order of cutter diameter) transitions."""
cmds = list()
rtpd = False
horizGC = 'G0'
hSpeed = self.horizRapid
height = obj.SafeHeight.Value
maxXYDistanceSqrd = (self.cutter.getDiameter() + tolrnc)**2
# Allow cutter-down transitions with a distance up to 2x cutter
# diameter. We might be able to extend this further to the
# full-retract-and-rapid break even point in the future, but this will
# require a safeSTL that has all non-cut surfaces raised sufficiently
# to avoid inadvertent cutting.
maxXYDistanceSqrd = (self.cutter.getDiameter() * 2)**2
if obj.OptimizeStepOverTransitions is True:
if obj.OptimizeStepOverTransitions:
# Short distance within step over
xyDistanceSqrd = (abs(first.x - lstPnt.x)**2 +
abs(first.y - lstPnt.y)**2)
zChng = abs(first.z - lstPnt.z)
# Only optimize short distances <= cutter diameter. Staying at
# minSTH over long distances is not safe for multi layer paths,
# since minSTH is calculated from the model, and not based on
# stock cut so far.
xyDistanceSqrd = ((p1.x - p2.x)**2 + (p1.y - p2.y)**2)
# Try to keep cutting for short distances.
if xyDistanceSqrd <= maxXYDistanceSqrd:
horizGC = "G1"
hSpeed = self.horizFeed
if (minSTH <= max(first.z, lstPnt.z) + tolrnc
and zChng < tolrnc):
# Allow direct transition without any lift over short
# distances, and only when there is very little z change.
height = False
else:
# Avoid a full lift, but stay at least at minSTH along the
# entire transition.
# TODO: Consider using an actual scan path for the
# transition.
height = max(minSTH, first.z, lstPnt.z)
else:
# We conservatively lift to SafeHeight for lack of an accurate
# stock model, but then speed up the drop back down
# When using multi pass, only drop quickly to previous layer
# depth
stepDown = obj.StepDown.Value if hasattr(obj, "StepDown") else 0
rtpd = min(height,
max(minSTH, first.z, lstPnt.z) + stepDown + 2)
# Try to keep cutting, following the model shape
(transLine, minZ, maxZ) = self._getTransitionLine(
safePDC, p1, p2, obj)
# For now, only optimize moderate deviations in Z direction, and
# no dropping below the min of p1 and p2, primarily for multi
# layer path safety.
zFloor = min(p1.z, p2.z)
if abs(minZ - maxZ) < self.cutter.getDiameter():
for pt in transLine[1:-1]:
cmds.append(
Path.Command('G1', {
'X': pt.x,
'Y': pt.y,
# Enforce zFloor
'Z': max(pt.z, zFloor),
'F': self.horizFeed
}))
# Use p2 (start of next step) verbatim
cmds.append(
Path.Command('G1', {
'X': p2.x,
'Y': p2.y,
'Z': p2.z,
'F': self.horizFeed
}))
return cmds
# For longer distances or large z deltas, we conservatively lift
# to SafeHeight for lack of an accurate stock model, but then
# speed up the drop back down when using multi pass, dropping
# quickly to *previous* layer depth.
stepDown = obj.StepDown.Value if hasattr(obj,
"StepDown") else 0
rtpd = min(height, p2.z + stepDown + 2)
# Create raise, shift, and optional lower commands
if height is not False:
cmds.append(Path.Command('G0', {'Z': height, 'F': self.vertRapid}))
cmds.append(Path.Command(horizGC, {'X': first.x, 'Y': first.y, 'F': hSpeed}))
if rtpd is not False: # ReturnToPreviousDepth
cmds.append(Path.Command('G0', {'Z': rtpd, 'F': self.vertRapid}))
return cmds
def _breakCmds(self, obj, lstPnt, first, minSTH, tolrnc):
cmds = list()
rtpd = False
horizGC = 'G0'
hSpeed = self.horizRapid
height = obj.SafeHeight.Value
if obj.CutPattern in ['Line', 'Circular']:
if obj.OptimizeStepOverTransitions is True:
height = minSTH + 2.0
elif obj.CutPattern in ['ZigZag', 'CircularZigZag']:
if obj.OptimizeStepOverTransitions is True:
zChng = first.z - lstPnt.z
if abs(zChng) < tolrnc: # transitions to same Z height
if (minSTH - first.z) > tolrnc:
height = minSTH + 2.0
else:
height = first.z + 2.0 # first.z
cmds.append(Path.Command('G0', {'Z': height, 'F': self.vertRapid}))
cmds.append(Path.Command(horizGC, {'X': first.x, 'Y': first.y, 'F': hSpeed}))
cmds.append(
Path.Command('G0', {
'X': p2.x,
'Y': p2.y,
'F': self.horizRapid
}))
if rtpd is not False: # ReturnToPreviousDepth
cmds.append(Path.Command('G0', {'Z': rtpd, 'F': self.vertRapid}))
@@ -2075,7 +2076,7 @@ class ObjectSurface(PathOp.ObjectOp):
diam_1 += 4.0
if FR != 0.0:
FR += 2.0
PathLog.debug('ToolType: {}'.format(obj.ToolController.Tool.ToolType))
if obj.ToolController.Tool.ToolType == 'EndMill':
# Standard End Mill
@@ -2109,15 +2110,37 @@ class ObjectSurface(PathOp.ObjectOp):
PathLog.warning("Defaulting cutter to standard end mill.")
return ocl.CylCutter(diam_1, (CEH + lenOfst))
def _getMinSafeTravelHeight(self, pdc, p1, p2, minDep=None):
A = (p1.x, p1.y)
B = (p2.x, p2.y)
LINE = self._planarDropCutScan(pdc, A, B)
zMax = max([obj.z for obj in LINE])
if minDep is not None:
if zMax < minDep:
zMax = minDep
return zMax
def _optimizeLinearSegments(self, line):
"""Eliminate collinear interior segments"""
if len(line) > 2:
prv, pnt = line[0:2]
pts = [prv]
for nxt in line[2:]:
if not pnt.isOnLineSegment(prv, nxt):
pts.append(pnt)
prv = pnt
pnt = nxt
pts.append(line[-1])
return pts
else:
return line
def _getTransitionLine(self, pdc, p1, p2, obj):
"""Use an OCL PathDropCutter to generate a safe transition path between
two points in the x/y plane."""
p1xy, p2xy = ((p1.x, p1.y), (p2.x, p2.y))
pdcLine = self._planarDropCutScan(pdc, p1xy, p2xy)
if obj.OptimizeLinearPaths:
pdcLine = self._optimizeLinearSegments(pdcLine)
zs = [obj.z for obj in pdcLine]
# PDC z values are based on the model, and do not take into account
# any remaining stock / multi layer paths. Adjust raw PDC z values to
# align with p1 and p2 z values.
zDelta = p1.z - pdcLine[0].z
if zDelta > 0:
for p in pdcLine:
p.z += zDelta
return (pdcLine, min(zs), max(zs))
def showDebugObject(self, objShape, objName):
if self.showDebugObjects:

View File

@@ -114,9 +114,9 @@ class PathGeometryGenerator:
fCnt += 1
zeroCOM = zeroCOM.add(FreeCAD.Vector(comF.x, comF.y, 0.0).multiply(areaF))
if fCnt == 0:
msg = translate('PathSurfaceSupport',
msg = translate('PathSurfaceSupport',
'Cannot calculate the Center Of Mass.')
msg += ' ' + translate('PathSurfaceSupport',
msg += ' ' + translate('PathSurfaceSupport',
'Using Center of Boundbox instead.') + '\n'
FreeCAD.Console.PrintError(msg)
bbC = self.shape.BoundBox.Center
@@ -910,22 +910,26 @@ class ProcessSelectedFaces:
'''_calculateOffsetValue(self.obj, isHole, isVoid) ... internal function.
Calculate the offset for the Path.Area() function.'''
self.JOB = PathUtils.findParentJob(self.obj)
tolrnc = self.JOB.GeometryTolerance.Value
# We need to offset by at least our linear tessellation deflection
# (default GeometryTolerance / 4) to avoid false retracts at the
# boundaries.
tolrnc = max(self.JOB.GeometryTolerance.Value / 10.0,
self.obj.LinearDeflection.Value)
if isVoid is False:
if isHole is True:
offset = -1 * self.obj.InternalFeaturesAdjustment.Value
offset += self.radius + (tolrnc / 10.0)
offset += self.radius + tolrnc
else:
offset = -1 * self.obj.BoundaryAdjustment.Value
if self.obj.BoundaryEnforcement is True:
offset += self.radius + (tolrnc / 10.0)
offset += self.radius + tolrnc
else:
offset -= self.radius + (tolrnc / 10.0)
offset -= self.radius + tolrnc
offset = 0.0 - offset
else:
offset = -1 * self.obj.BoundaryAdjustment.Value
offset += self.radius + (tolrnc / 10.0)
offset += self.radius + tolrnc
return offset