CAM: LeadInOut - Fix overtravel

This commit is contained in:
tarman3
2025-10-24 19:02:03 +03:00
committed by Chris Hennes
parent 31929cb4e5
commit da8e6d71ac

View File

@@ -162,6 +162,7 @@ class ObjectDressup:
"Path Lead-out",
QT_TRANSLATE_NOOP("App::Property", "Move end point"),
)
obj.Proxy = self
def dumps(self):
@@ -201,7 +202,7 @@ class ObjectDressup:
return None
if isinstance(obj.Base, list) and obj.Base and obj.Base[0].isDerivedFrom("Path::Feature"):
return self.getBaseWithTC(obj.Base[0])
elif not isinstance(obj.Base, list) and obj.Base.isDerivedFrom("Path::Feature"):
if not isinstance(obj.Base, list) and obj.Base.isDerivedFrom("Path::Feature"):
return self.getBaseWithTC(obj.Base)
return None
@@ -251,8 +252,18 @@ class ObjectDressup:
)
return
self.invertAlt = False
self.job = PathUtils.findParentJob(obj)
self.horizFeed = self.toolController.HorizFeed.Value
self.vertFeed = self.toolController.VertFeed.Value
self.clearanceHeight = self.baseOp.ClearanceHeight.Value
self.safeHeight = self.baseOp.SafeHeight.Value
self.startDepth = self.baseOp.StartDepth.Value
self.side = self.baseOp.Side if hasattr(self.baseOp, "Side") else "Inside"
if hasattr(self.baseOp, "Direction") and self.baseOp.Direction in ("CW", "CCW"):
self.direction = self.baseOp.Direction
else:
self.direction = "CCW"
self.entranceFeed = self.toolController.LeadInFeed.Value
self.exitFeed = self.toolController.LeadOutFeed.Value
@@ -424,23 +435,30 @@ class ObjectDressup:
# Get direction for lead-in/lead-out in XY plane
def getLeadDir(self, obj, invert=False):
output = math.pi / 2
side = self.baseOp.Side if hasattr(self.baseOp, "Side") else "Inside"
direction = self.baseOp.Direction if hasattr(self.baseOp, "Direction") else "CCW"
side = self.side
direction = self.direction
if (side == "Inside" and direction == "CW") or (side == "Outside" and direction == "CCW"):
output = -output
if invert:
output = -output
if self.invertAlt:
output = -output
return output
# Get direction of original path
def getPathDir(self, obj):
# only CW or CCW is matter
direction = self.baseOp.Direction if hasattr(self.baseOp, "Direction") else "CCW"
def getArcPathDir(self, obj, cmdName):
# only CW/CCW and G2/G3 matters
direction = self.direction
output = math.pi / 2
if direction == "CW":
output = -output
if cmdName == "G2" and direction == "CCW":
output = -output
elif cmdName == "G3" and direction == "CW":
output = -output
return output
# Create safety movements to start point
@@ -453,9 +471,7 @@ class ObjectDressup:
if first or (distance > obj.RetractThreshold):
# move to clearance height
commands.append(
PathLanguage.MoveStraight(None, "G00", {"Z": self.baseOp.ClearanceHeight.Value})
)
commands.append(PathLanguage.MoveStraight(None, "G00", {"Z": self.clearanceHeight}))
# move to mill position at clearance height
commands.append(PathLanguage.MoveStraight(None, "G00", {"X": pos.x, "Y": pos.y}))
@@ -466,9 +482,7 @@ class ObjectDressup:
commands.append(PathLanguage.MoveStraight(None, "G00", {"Z": pos.z}))
else:
# move to mill position in two steps
commands.append(
PathLanguage.MoveStraight(None, "G00", {"Z": self.baseOp.SafeHeight.Value})
)
commands.append(PathLanguage.MoveStraight(None, "G00", {"Z": self.safeHeight}))
commands.append(
PathLanguage.MoveStraight(None, "G01", {"Z": pos.z, "F": self.vertFeed})
)
@@ -491,7 +505,7 @@ class ObjectDressup:
# Create commands with movements to clearance height
def getTravelEnd(self, obj):
commands = []
z = self.baseOp.ClearanceHeight.Value
z = self.clearanceHeight
commands.append(PathLanguage.MoveStraight(None, "G00", {"Z": z}))
return commands
@@ -535,10 +549,9 @@ class ObjectDressup:
return command
# Get optimal step angle for iteration ArcZ
def getStepAngleArcZ(self, obj, radius):
job = PathUtils.findParentJob(obj)
minArcLength = job.GeometryTolerance.Value * 2
maxArcLength = 1
def getStepAngleArcZ(self, obj, radius, segm=1):
minArcLength = self.job.GeometryTolerance.Value * 2
maxArcLength = segm
stepAngle = math.pi / 60
stepArcLength = stepAngle * radius
if stepArcLength > maxArcLength:
@@ -681,7 +694,7 @@ class ObjectDressup:
elif styleIn == "LineZ":
# tangent vector in XY plane
# normal vector is vertical
normalLengthMax = self.baseOp.SafeHeight.Value - begin.z
normalLengthMax = self.safeHeight - begin.z
normalLength = math.sin(angleIn) * length
# do not exceed Normal vector max length
normalLength = min(normalLength, normalLengthMax)
@@ -692,12 +705,12 @@ class ObjectDressup:
lead.append(self.createStraightMove(obj, lineBegin, begin, self.entranceFeed))
# prepend "ArcZ" style lead-in - vertical Arc
# Should be applied only on straight Path segment
# Should be applied only on straight Path segment or open profile
elif styleIn == "ArcZ":
# tangent vector in XY plane
# normal vector is vertical
arcRadius = length
normalLengthMax = self.baseOp.SafeHeight.Value - begin.z
normalLengthMax = self.safeHeight - begin.z
normalLength = arcRadius * (1 - math.cos(angleIn))
if normalLength > normalLengthMax:
# do not exceed Normal vector max length
@@ -720,7 +733,7 @@ class ObjectDressup:
if inInstrPrev and inInstrPrev.z() > begin.z:
begin.z = inInstrPrev.z()
else:
begin.z = self.baseOp.StartDepth.Value
begin.z = self.startDepth
lead[0].setPositionBegin(begin)
elif styleIn == "Helix":
@@ -732,7 +745,7 @@ class ObjectDressup:
halfStepZ = (posPrevZ - beginZ) / 2
begin.z += halfStepZ
else:
begin.z = self.baseOp.StartDepth.Value
begin.z = self.startDepth
if obj.StyleOut == "Helix" and outInstrPrev:
"""change Z for previous helix lead-out
@@ -821,7 +834,7 @@ class ObjectDressup:
elif obj.StyleOut == "LineZ":
# tangent vector in XY plane
# normal vector is vertical
normalLengthMax = self.baseOp.StartDepth.Value - end.z
normalLengthMax = self.startDepth - end.z
normalLength = math.sin(angleOut) * length
# do not exceed Normal vector max length
normalLength = min(normalLength, normalLengthMax)
@@ -837,7 +850,7 @@ class ObjectDressup:
# tangent vector in XY plane
# normal vector is vertical
arcRadius = length
normalLengthMax = self.baseOp.SafeHeight.Value - end.z
normalLengthMax = self.safeHeight - end.z
normalLength = arcRadius * (1 - math.cos(angleOut))
if normalLength > normalLengthMax:
# do not exceed Normal vector max length
@@ -855,7 +868,7 @@ class ObjectDressup:
if outInstrPrev and outInstrPrev.positionBegin().z > end.z:
lead[-1].param["Z"] = outInstrPrev.positionBegin().z
else:
lead[-1].param["Z"] = self.baseOp.StartDepth.Value
lead[-1].param["Z"] = self.startDepth
# append travel moves to clearance height after finishing all profiles
if last and obj.StyleOut != "No Retract":
@@ -881,43 +894,55 @@ class ObjectDressup:
return None
# Get last index of mill command in whole Path
def findLastCuttingMoveIndex(self, source):
for i in range(len(source) - 1, -1, -1):
if self.isCuttingMove(source[i]):
def findLastCuttingMoveIndex(self):
for i in range(len(self.source) - 1, -1, -1):
if self.isCuttingMove(self.source[i]):
return i
return None
# Get finish index of mill command for one profile
def findLastCutMultiProfileIndex(self, source, startIndex):
if startIndex >= len(source):
return len(source) - 1
for i in range(startIndex, len(source), +1):
if not self.isCuttingMove(source[i]):
def findLastCutMultiProfileIndex(self):
startIndex = self.firstMillIndex
if startIndex >= len(self.source):
return len(self.source) - 1
for i in range(startIndex, len(self.source), +1):
if not self.isCuttingMove(self.source[i]):
return i - 1
return i
# Increase travel length from 'begin'
def getOvertravelIn(self, obj, source, length, start, end):
startPoint = source[start].positionBegin()
endPoint = source[end].positionEnd()
def isProfileClosed(self):
start = self.firstMillIndex
end = self.lastMillIndex
startPoint = self.source[start].positionBegin()
endPoint = self.source[end].positionEnd()
if Path.Geom.pointsCoincide(startPoint, endPoint):
self.closedProfile = True
else:
self.closedProfile = False
# Increase travel length from 'begin', take commands from profile 'end'
def getOvertravelIn(self, obj, length):
start = self.firstMillIndex
end = self.lastMillIndex
if self.closedProfile:
# closed profile
# get extra commands from end of the closed profile
measuredLength = 0
for i, instr in enumerate(reversed(source[start : end + 1])):
for i, instr in enumerate(reversed(self.source[start : end + 1])):
instrLength = instr.pathLength()
if Path.Geom.isRoughly(measuredLength + instrLength, length, 1):
if Path.Geom.isRoughly(measuredLength + instrLength, length):
# get needed length without needing to cut last command
commands = source[end - i : end + 1]
commands = self.source[end - i : end + 1]
return commands
elif measuredLength + instrLength > length:
# measured length exceeds needed length and needs cut command
commands = source[end - i + 1 : end + 1]
commands = self.source[end - i + 1 : end + 1]
newLength = length - measuredLength
newInstr = self.cutInstrBegin(obj, instr, newLength)
commands.insert(0, newInstr)
@@ -928,33 +953,32 @@ class ObjectDressup:
else:
# open profile
# extend first move
instr = source[start]
instr = self.source[start]
newLength = length + instr.pathLength()
newInstr = self.cutInstrBegin(obj, instr, newLength)
return [newInstr]
return None
# Increase travel length from end
def getOvertravelOut(self, obj, source, length, start, end):
startPoint = source[start].positionBegin()
endPoint = source[end].positionEnd()
if Path.Geom.pointsCoincide(startPoint, endPoint):
# Increase travel length from end, take commands from profile start
def getOvertravelOut(self, obj, length):
start = self.firstMillIndex
end = self.lastMillIndex
if self.closedProfile:
# closed profile
# get extra commands from begin of the closed profile
measuredLength = 0
for i, instr in enumerate(source[start:end]):
for i, instr in enumerate(self.source[start : end + 1]):
instrLength = instr.pathLength()
if Path.Geom.isRoughly(measuredLength + instrLength, length, 1):
if Path.Geom.isRoughly(measuredLength + instrLength, length):
# get needed length without needing to cut last command
commands = source[start : start + i + 1]
commands = self.source[start : start + i + 1]
return commands
elif measuredLength + instrLength > length:
# measured length exceeds needed length and needs cut command
commands = source[start : start + i]
commands = self.source[start : start + i]
newLength = length - measuredLength
newInstr = self.cutInstrEnd(obj, instr, newLength)
commands.append(newInstr)
@@ -965,7 +989,7 @@ class ObjectDressup:
else:
# open profile
# extend last move
instr = source[end]
instr = self.source[end]
newLength = length + instr.pathLength()
newInstr = self.cutInstrEnd(obj, instr, newLength)
return [newInstr]
@@ -973,25 +997,29 @@ class ObjectDressup:
return None
# Cut travel end by distance (negative overtravel out)
def cutTravelEnd(self, obj, source, cutLength):
def cutTravelEnd(self, obj, commands, cutLength):
measuredLength = 0
for i, instr in enumerate(reversed(commands)):
if instr.positionBegin() is None:
# workaround if cut whole profile by negative offset
cmds = commands[:-i]
newInstr = self.cutInstrEnd(obj, commands[-i], 0.1)
cmds.append(newInstr)
return cmds
for i, instr in enumerate(reversed(source)):
instrLength = instr.pathLength()
measuredLength += instrLength
if Path.Geom.isRoughly(measuredLength, cutLength):
# get needed length and not need to cut any command
commands = source[: -i - 1]
return commands
return commands[: -i - 1]
elif measuredLength > cutLength:
# measured length exceed needed cut length and need cut command
commands = source[: -i - 1]
cmds = commands[: -i - 1]
newLength = measuredLength - cutLength
newInstr = self.cutInstrEnd(obj, instr, newLength)
commands.append(newInstr)
return commands
cmds.append(newInstr)
return cmds
return None
@@ -1010,6 +1038,7 @@ class ObjectDressup:
# Cut arc move from begin
elif instr.isArc():
cmdName = instr.cmd
angleTangent = instr.anglesOfTangents()[0]
arcBegin = instr.positionBegin()
arcOffset = App.Vector(instr.i(), instr.j(), instr.k())
@@ -1018,9 +1047,10 @@ class ObjectDressup:
tangentLength = math.sin(arcAngle) * arcRadius
normalLength = arcRadius * (1 - math.cos(arcAngle))
tangent = self.angleToVector(angleTangent) * tangentLength
normal = self.angleToVector(angleTangent + self.getPathDir(obj)) * normalLength
normal = (
self.angleToVector(angleTangent + self.getArcPathDir(obj, cmdName)) * normalLength
)
arcEnd = arcBegin + tangent + normal
cmdName = "G2" if instr.isCW() else "G3"
command = self.createArcMoveN(obj, arcBegin, arcEnd, arcOffset, cmdName, self.horizFeed)
return command
@@ -1040,6 +1070,7 @@ class ObjectDressup:
# Cut arc move from begin
elif instr.isArc():
cmdName = instr.cmd
angleTangent = instr.anglesOfTangents()[1]
arcEnd = instr.positionEnd()
arcCenter = instr.xyCenter()
@@ -1048,10 +1079,11 @@ class ObjectDressup:
tangentLength = math.sin(arcAngle) * arcRadius
normalLength = arcRadius * (1 - math.cos(arcAngle))
tangent = -self.angleToVector(angleTangent) * tangentLength
normal = self.angleToVector(angleTangent + self.getPathDir(obj)) * normalLength
normal = (
self.angleToVector(angleTangent + self.getArcPathDir(obj, cmdName)) * normalLength
)
arcBegin = arcEnd + tangent + normal
arcOffset = arcCenter - arcBegin
cmdName = "G2" if instr.isCW() else "G3"
command = self.createArcMoveN(obj, arcBegin, arcEnd, arcOffset, cmdName, self.horizFeed)
return command
@@ -1059,20 +1091,22 @@ class ObjectDressup:
def generateLeadInOutCurve(self, obj):
source = PathLanguage.Maneuver.FromPath(PathUtils.getPathWithPlacement(obj.Base)).instr
self.source = source
maneuver = PathLanguage.Maneuver()
# Knowing whether a given instruction is the first cutting move is easy,
# we just use a flag and set it to false afterwards. To find the last
# cutting move we need to search the list in reverse order.
commands = []
first = True # prepare first move at clearance height
firstMillIndex = None # Index start mill instruction for one profile
lastCuttingMoveIndex = self.findLastCuttingMoveIndex(source)
inInstrPrev = None
outInstrPrev = None
self.firstMillIndex = None # Index start mill instruction for one profile
self.lastMillIndex = None # Index end mill instruction for one profile
self.lastCuttingMoveIndex = self.findLastCuttingMoveIndex()
inInstrPrev = None # for RetractThreshold
outInstrPrev = None # for RetractThreshold
measuredLength = 0 # for negative OffsetIn
skipCounter = 0 # for negative OffsetIn
commands = []
moveDir = None
# Process all instructions
@@ -1105,8 +1139,17 @@ class ObjectDressup:
if obj.LeadIn or obj.LeadOut:
# can not skip leadin if leadout
# Process negative Offset Lead-In (cut travel from begin)
self.firstMillIndex = i if self.firstMillIndex is None else self.firstMillIndex
self.lastMillIndex = (
self.findLastCutMultiProfileIndex()
if self.lastMillIndex is None
else self.lastMillIndex
)
self.isProfileClosed()
overtravelIn = None
if obj.OffsetIn.Value < 0 and obj.StyleIn != "No Retract":
# Process negative Offset Lead-In (cut travel from begin)
if measuredLength <= abs(obj.OffsetIn.Value):
# skip mill instruction
skipCounter += 1 # count skipped instructions
@@ -1117,18 +1160,9 @@ class ObjectDressup:
newLength = measuredLength - abs(obj.OffsetIn.Value)
instr = self.cutInstrBegin(obj, instr, newLength)
# Process positive offset Lead-In (overtravel)
firstMillIndex = i
lastMillIndex = self.findLastCutMultiProfileIndex(source, i + 1)
overtravelIn = None
if obj.OffsetIn.Value > 0 and obj.StyleIn != "No Retract":
overtravelIn = self.getOvertravelIn(
obj,
source,
obj.OffsetIn.Value,
firstMillIndex,
lastMillIndex,
)
elif obj.OffsetIn.Value > 0 and obj.StyleIn != "No Retract":
# Process positive offset Lead-In (overtravel)
overtravelIn = self.getOvertravelIn(obj, obj.OffsetIn.Value)
if overtravelIn:
commands.extend(
self.getLeadStart(
@@ -1140,7 +1174,6 @@ class ObjectDressup:
commands.extend(
self.getLeadStart(obj, instr, first, inInstrPrev, outInstrPrev)
)
firstMillIndex = i if not firstMillIndex else firstMillIndex
inInstrPrev = commands[-1]
first = False
@@ -1148,10 +1181,9 @@ class ObjectDressup:
commands.append(instr)
# Process Lead-Out
last = bool(i == lastCuttingMoveIndex)
last = bool(i == self.lastCuttingMoveIndex)
if (last or not self.isCuttingMove(source[i + 1])) and obj.LeadOut:
measuredLength = 0 # reset measured length for last profile
lastMillIndex = i # index last mill instruction for last profile
# Process negative Offset Lead-Out (cut travel from end)
if obj.OffsetOut.Value < 0 and obj.StyleOut != "No Retract":
@@ -1159,14 +1191,7 @@ class ObjectDressup:
# Process positive Offset Lead-Out (overtravel)
if obj.OffsetOut.Value > 0 and obj.StyleOut != "No Retract":
overtravelOut = self.getOvertravelOut(
obj,
source,
obj.OffsetOut.Value,
firstMillIndex,
lastMillIndex,
)
firstMillIndex = None
overtravelOut = self.getOvertravelOut(obj, obj.OffsetOut.Value)
if overtravelOut:
commands.extend(overtravelOut)
@@ -1180,6 +1205,10 @@ class ObjectDressup:
else:
outInstrPrev = instr
self.firstMillIndex = None
self.lastMillIndex = None
self.invertAlt = not self.invertAlt if getattr(obj, "InvertAlt", None) else False
maneuver.addInstructions(commands)
return maneuver.toPath()
@@ -1198,8 +1227,6 @@ class TaskDressupLeadInOut(SimpleEditPanel):
def setupSpinBoxes(self):
self.connectWidget("InvertIn", self.form.chkInvertDirectionIn)
self.connectWidget("InvertOut", self.form.chkInvertDirectionOut)
self.connectWidget("PercentageRadiusIn", self.form.dspPercentageRadiusIn)
self.connectWidget("PercentageRadiusOut", self.form.dspPercentageRadiusOut)
self.connectWidget("StyleIn", self.form.cboStyleIn)
self.connectWidget("StyleOut", self.form.cboStyleOut)
self.radiusIn = PathGuiUtil.QuantitySpinBox(self.form.dspRadiusIn, self.obj, "RadiusIn")
@@ -1384,7 +1411,12 @@ class CommandPathDressupLeadInOut:
return False
if not selection[0].isDerivedFrom("Path::Feature"):
return False
if selection[0].Name.startswith("Job"):
baseOp = PathDressup.baseOp(selection[0])
if not hasattr(baseOp, "ClearanceHeight"):
return False
if not hasattr(baseOp, "SafeHeight"):
return False
if not hasattr(baseOp, "StartDepth"):
return False
return True