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:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user