diff --git a/src/Mod/CAM/Path/Dressup/Gui/LeadInOut.py b/src/Mod/CAM/Path/Dressup/Gui/LeadInOut.py index e9bc190649..5d263460e9 100644 --- a/src/Mod/CAM/Path/Dressup/Gui/LeadInOut.py +++ b/src/Mod/CAM/Path/Dressup/Gui/LeadInOut.py @@ -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