From c5df5d5ca2659b00a3f46a4b27befbea2c2d9542 Mon Sep 17 00:00:00 2001 From: David Kaufman Date: Fri, 5 Sep 2025 12:24:42 -0400 Subject: [PATCH] [CAM] LeadInOut new features (#22669) * CAM: Improve LeadInOut * rebuild/fix task panel UI * migrate old LeadInOut parameters to the new properties * Keep original Lead-in and/or Lead-out * LeadInOut Rename None/Original to No Retract/None * merge in changes from tarman/leadovertravel * LeadInOut update names to No Change/Suppress Retraction * Remove IncludeLayers option from LeadInOut (always true) * [CAM] Fix offset entrance UI string * [CAM] improve handling of step angle on lead in/out ArcZ * resolve merge conflicts * finish pulling in tarman's updates * switch back from 'No Change' style to enable checkbox * 'Suppress Retraction' -> 'No Retract' * fix documentation string for lead in/out dressup --------- Co-authored-by: tarman3 --- .../Resources/panels/DressUpLeadInOutEdit.ui | 402 +++--- src/Mod/CAM/Path/Base/Language.py | 22 +- src/Mod/CAM/Path/Dressup/Gui/LeadInOut.py | 1096 ++++++++++++++--- .../CAM/PathPythonGui/simple_edit_panel.py | 11 +- 4 files changed, 1172 insertions(+), 359 deletions(-) diff --git a/src/Mod/CAM/Gui/Resources/panels/DressUpLeadInOutEdit.ui b/src/Mod/CAM/Gui/Resources/panels/DressUpLeadInOutEdit.ui index d256ef50c2..baab2f8dab 100644 --- a/src/Mod/CAM/Gui/Resources/panels/DressUpLeadInOutEdit.ui +++ b/src/Mod/CAM/Gui/Resources/panels/DressUpLeadInOutEdit.ui @@ -7,7 +7,7 @@ 0 0 359 - 534 + 555 @@ -21,152 +21,240 @@ - - - - - Enable lead-in move - - - Enable lead-in - - - - - - - Style - - - - - - - - 80 - 0 - - - - - 16777215 - 16777215 - - - - - - - - Length/radius - - - - - - - Length or radius of the lead-in - - - 0.100000000000000 - - - - - - - - - - Extend - - - - - - - Extends the lead-in distance - - - - - - - + + + Lead In + + + true + + + + + + + + Style + + + + + + + + 80 + 0 + + + + + 16777215 + 16777215 + + + + + + + + Radius/length (% tool radius) + + + + + + + Length of the Lead-in, as a percentage of tool radius + + + 0 + + + 1.000000000000000 + + + 999999.000000000000000 + + + 10.000000000000000 + + + + + + + + + + Angle + + + + + + + Angular extent of the lead in arc (degrees) + + + 180.000000000000000 + + + 10.000000000000000 + + + + + + + + + + Offset Entrance Location + + + + + + + -999999.000000000000000 + + + 999999.000000000000000 + + + + + + + + + + Invert Direction + + + + + + + - - - - - Enable lead-out move - - - Enable lead out - - - - - - - Style - - - - - - - - 80 - 0 - - - - - 16777215 - 16777215 - - - - - - - - Length/radius - - - - - - - Length or radius of the lead-out - - - 0.100000000000000 - - - - - - - - - - Extend - - - - - - - Extends the lead-out distance - - - - - - - + + + Lead Out + + + true + + + + + + + + Style + + + + + + + + 80 + 0 + + + + + 16777215 + 16777215 + + + + + + + + Radius/length (% tool radius) + + + + + + + Length of the Lead-out, as a percentage of tool radius + + + 0 + + + 1.000000000000000 + + + 999999.000000000000000 + + + 10.000000000000000 + + + + + + + + + + Angle + + + + + + + Angular extent of the lead out arc (degrees) + + + 180.000000000000000 + + + 10.000000000000000 + + + + + + + + + + Offset Exit Location + + + + + + + -999999.000000000000000 + + + 999999.000000000000000 + + + + + + + + + + Invert Direction + + + + + + + @@ -197,12 +285,19 @@ - - - Keep the tool down in the path - + - Keep tool down + Retract Threshold + + + + + + + 999999.000000000000000 + + + @@ -223,13 +318,6 @@ - - - Gui::InputField - QLineEdit -
Gui/InputField.h
-
-
diff --git a/src/Mod/CAM/Path/Base/Language.py b/src/Mod/CAM/Path/Base/Language.py index d32e8723a6..225c19e708 100644 --- a/src/Mod/CAM/Path/Base/Language.py +++ b/src/Mod/CAM/Path/Base/Language.py @@ -37,7 +37,7 @@ class Instruction(object): def __init__(self, begin, cmd, param=None): self.begin = begin - if type(cmd) == Path.Command: + if isinstance(cmd, Path.Command): self.cmd = Path.Name self.param = Path.Parameters else: @@ -72,8 +72,15 @@ class Instruction(object): return False def isPlunge(self): - """isPlunge() ... return true if this moves up or down""" - return self.isMove() and not Path.Geom.isRoughly(self.begin.z, self.z(self.begin.z)) + """isPlunge() ... return true if this moves is vertical""" + if self.isMove(): + if ( + Path.Geom.isRoughly(self.begin.x, self.x(self.begin.x)) + and Path.Geom.isRoughly(self.begin.y, self.y(self.begin.y)) + and not Path.Geom.isRoughly(self.begin.z, self.z(self.begin.z)) + ): + return True + return False def leadsInto(self, instr): """leadsInto(instr) ... return true if instr is a continuation of self""" @@ -130,6 +137,12 @@ class MoveStraight(Instruction): def isMove(self): return True + def isStraight(self): + return True + + def isArc(self): + return False + def isRapid(self): return self.cmd in Path.Geom.CmdMoveRapid @@ -159,6 +172,9 @@ class MoveArc(Instruction): def isArc(self): return True + def isStraight(self): + return False + def isCW(self): return self.arcDirection() < 0 diff --git a/src/Mod/CAM/Path/Dressup/Gui/LeadInOut.py b/src/Mod/CAM/Path/Dressup/Gui/LeadInOut.py index 32ca7f09cc..7eca7130d8 100644 --- a/src/Mod/CAM/Path/Dressup/Gui/LeadInOut.py +++ b/src/Mod/CAM/Path/Dressup/Gui/LeadInOut.py @@ -28,6 +28,7 @@ import Path import Path.Base.Language as PathLanguage import Path.Dressup.Utils as PathDressup import PathScripts.PathUtils as PathUtils +import copy import math __doc__ = """LeadInOut Dressup USE ROLL-ON ROLL-OFF to profile""" @@ -44,14 +45,23 @@ if False: else: Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule()) +lead_styles = [ + # common options first + QT_TRANSLATE_NOOP("CAM_DressupLeadInOut", "Arc"), + QT_TRANSLATE_NOOP("CAM_DressupLeadInOut", "Line"), + # additional options, alphabetical order + QT_TRANSLATE_NOOP("CAM_DressupLeadInOut", "Arc3d"), + QT_TRANSLATE_NOOP("CAM_DressupLeadInOut", "ArcZ"), + QT_TRANSLATE_NOOP("CAM_DressupLeadInOut", "Helix"), + QT_TRANSLATE_NOOP("CAM_DressupLeadInOut", "Line3d"), + QT_TRANSLATE_NOOP("CAM_DressupLeadInOut", "LineZ"), + QT_TRANSLATE_NOOP("CAM_DressupLeadInOut", "No Retract"), + QT_TRANSLATE_NOOP("CAM_DressupLeadInOut", "Vertical"), +] + class ObjectDressup: def __init__(self, obj): - lead_styles = [ - QT_TRANSLATE_NOOP("CAM_DressupLeadInOut", "Arc"), - QT_TRANSLATE_NOOP("CAM_DressupLeadInOut", "Tangent"), - QT_TRANSLATE_NOOP("CAM_DressupLeadInOut", "Perpendicular"), - ] self.obj = obj obj.addProperty( "App::PropertyLink", @@ -63,31 +73,21 @@ class ObjectDressup: "App::PropertyBool", "LeadIn", "Path", - QT_TRANSLATE_NOOP("App::Property", "Calculate roll-on to toolpath"), + QT_TRANSLATE_NOOP("App::Property", "Modify lead in to toolpath"), ) obj.addProperty( "App::PropertyBool", "LeadOut", "Path", - QT_TRANSLATE_NOOP("App::Property", "Calculate roll-off from toolpath"), + QT_TRANSLATE_NOOP("App::Property", "Modify lead out from toolpath"), ) obj.addProperty( - "App::PropertyBool", - "KeepToolDown", + "App::PropertyLength", + "RetractThreshold", "Path", - QT_TRANSLATE_NOOP("App::Property", "Keep the tool down in toolpath"), - ) - obj.addProperty( - "App::PropertyDistance", - "LengthIn", - "Path", - QT_TRANSLATE_NOOP("App::Property", "Length or radius of the approach"), - ) - obj.addProperty( - "App::PropertyDistance", - "LengthOut", - "Path", - QT_TRANSLATE_NOOP("App::Property", "Length or radius of the exit"), + QT_TRANSLATE_NOOP( + "App::Property", "Set distance which will attempts to avoid unnecessary retractions" + ), ) obj.addProperty( "App::PropertyEnumeration", @@ -103,29 +103,59 @@ class ObjectDressup: QT_TRANSLATE_NOOP("App::Property", "The style of motion out of the toolpath"), ) obj.StyleOut = lead_styles - obj.addProperty( - "App::PropertyDistance", - "ExtendLeadIn", - "Path", - QT_TRANSLATE_NOOP("App::Property", "Extends lead in distance"), - ) - obj.addProperty( - "App::PropertyDistance", - "ExtendLeadOut", - "Path", - QT_TRANSLATE_NOOP("App::Property", "Extends lead out distance"), - ) obj.addProperty( "App::PropertyBool", "RapidPlunge", "Path", QT_TRANSLATE_NOOP("App::Property", "Perform plunges with G0"), ) + obj.addProperty( + "App::PropertyAngle", + "AngleIn", + "Path Lead-in", + QT_TRANSLATE_NOOP("App::Property", "Angle of the Lead-In (1..90)"), + ) + obj.addProperty( + "App::PropertyAngle", + "AngleOut", + "Path Lead-out", + QT_TRANSLATE_NOOP("App::Property", "Angle of the Lead-Out (1..90)"), + ) + obj.addProperty( + "App::PropertyInteger", + "PercentageRadiusIn", + "Path Lead-in", + QT_TRANSLATE_NOOP("App::Property", "Determine length of the Lead-In"), + ) + obj.addProperty( + "App::PropertyInteger", + "PercentageRadiusOut", + "Path Lead-out", + QT_TRANSLATE_NOOP("App::Property", "Determine length of the Lead-Out"), + ) obj.addProperty( "App::PropertyBool", - "IncludeLayers", - "Path", - QT_TRANSLATE_NOOP("App::Property", "Apply Lead in/out to layers within an operation"), + "InvertIn", + "Path Lead-in", + QT_TRANSLATE_NOOP("App::Property", "Invert Lead-In direction"), + ) + obj.addProperty( + "App::PropertyBool", + "InvertOut", + "Path Lead-out", + QT_TRANSLATE_NOOP("App::Property", "Invert Lead-Out direction"), + ) + obj.addProperty( + "App::PropertyDistance", + "OffsetIn", + "Path Lead-in", + QT_TRANSLATE_NOOP("App::Property", "Move start point"), + ) + obj.addProperty( + "App::PropertyDistance", + "OffsetOut", + "Path Lead-out", + QT_TRANSLATE_NOOP("App::Property", "Move end point"), ) obj.Proxy = self @@ -136,49 +166,64 @@ class ObjectDressup: return None def setup(self, obj): - obj.LengthIn = PathDressup.toolController(obj.Base).Tool.Diameter * 0.75 - obj.LengthOut = PathDressup.toolController(obj.Base).Tool.Diameter * 0.75 obj.LeadIn = True obj.LeadOut = True - obj.KeepToolDown = False + obj.AngleIn = 45 + obj.AngleOut = 45 + obj.InvertIn = False + obj.InvertOut = False + obj.PercentageRadiusIn = 150 + obj.PercentageRadiusOut = 150 + obj.RapidPlunge = False obj.StyleIn = "Arc" obj.StyleOut = "Arc" - obj.ExtendLeadIn = 0 - obj.ExtendLeadOut = 0 - obj.RapidPlunge = False - obj.IncludeLayers = True def execute(self, obj): if not obj.Base: + obj.Path = Path.Path() return if not obj.Base.isDerivedFrom("Path::Feature"): + obj.Path = Path.Path() return if not obj.Base.Path: + obj.Path = Path.Path() return - if obj.LengthIn <= 0: - Path.Log.error( - translate("CAM_DressupLeadInOut", "Length/radius positive not Null") + "\n" - ) - obj.LengthIn = 0.1 + if obj.PercentageRadiusIn < 1: + obj.PercentageRadiusIn = 1 + if obj.PercentageRadiusOut < 1: + obj.PercentageRadiusOut = 1 - if obj.LengthOut <= 0: - Path.Log.error( - translate("CAM_DressupLeadInOut", "Length/radius positive not Null") + "\n" - ) - obj.LengthOut = 0.1 + limit_angle_in = 1 if "Arc" in obj.StyleIn or "Helix" == obj.StyleIn else 0 + limit_angle_out = 1 if "Arc" in obj.StyleOut or "Helix" == obj.StyleOut else 0 + if obj.AngleIn > 180: + obj.AngleIn = 180 + if obj.AngleIn < limit_angle_in: + obj.AngleIn = limit_angle_in + + if obj.AngleOut > 180: + obj.AngleOut = 180 + if obj.AngleOut < limit_angle_out: + obj.AngleOut = limit_angle_out + + hideModes = { + "Angle": ["No Retract", "Vertical"], + "Invert": ["No Retract", "ArcZ", "LineZ", "Vertical"], + "Offset": ["No Retract"], + "PercentageRadius": ["No Retract", "Vertical"], + } + for k, v in hideModes.items(): + obj.setEditorMode(k + "In", 2 if obj.StyleIn in v else 0) + obj.setEditorMode(k + "Out", 2 if obj.StyleOut in v else 0) obj.Path = self.generateLeadInOutCurve(obj) def onDocumentRestored(self, obj): - """onDocumentRestored(obj)… Called automatically when document is restored.""" - lead_styles = [ - QT_TRANSLATE_NOOP("CAM_DressupLeadInOut", "Arc"), - QT_TRANSLATE_NOOP("CAM_DressupLeadInOut", "Tangent"), - QT_TRANSLATE_NOOP("CAM_DressupLeadInOut", "Perpendicular"), - ] + """onDocumentRestored(obj) ... Called automatically when document is restored.""" + styleOn = styleOff = None if hasattr(obj, "StyleOn"): # Replace StyleOn by StyleIn + styleOn = obj.StyleOn obj.addProperty( "App::PropertyEnumeration", "StyleIn", @@ -186,10 +231,11 @@ class ObjectDressup: QT_TRANSLATE_NOOP("App::Property", "The style of motion into the toolpath"), ) obj.StyleIn = lead_styles - obj.StyleIn = obj.StyleOn obj.removeProperty("StyleOn") + obj.StyleIn = "Arc" if hasattr(obj, "StyleOff"): # Replace StyleOff by StyleOut + styleOff = obj.StyleOff obj.addProperty( "App::PropertyEnumeration", "StyleOut", @@ -197,95 +243,332 @@ class ObjectDressup: QT_TRANSLATE_NOOP("App::Property", "The style of motion out of the toolpath"), ) obj.StyleOut = lead_styles - obj.StyleOut = obj.StyleOff obj.removeProperty("StyleOff") + obj.StyleOut = "Arc" + + if not hasattr(obj, "AngleIn"): + obj.addProperty( + "App::PropertyAngle", + "AngleIn", + "Path Lead-in", + QT_TRANSLATE_NOOP("App::Property", "Angle of the Lead-In (1..90)"), + ) + obj.AngleIn = 45 + if not hasattr(obj, "AngleOut"): + obj.addProperty( + "App::PropertyAngle", + "AngleOut", + "Path Lead-out", + QT_TRANSLATE_NOOP("App::Property", "Angle of the Lead-Out (1..90)"), + ) + obj.AngleOut = 45 + + if styleOn: + if styleOn == "Arc": + obj.StyleIn = "Arc" + obj.AngleIn = 90 + elif styleOn in ["Perpendicular", "Tangent"]: + obj.StyleIn = "Line" + obj.AngleIn = 90 if styleOn == "Perpendicular" else 0 + + if styleOff: + if styleOff == "Arc": + obj.StyleOut = "Arc" + obj.AngleOut = 90 + elif styleOff in ["Perpendicular", "Tangent"]: + obj.StyleOut = "Line" + obj.AngleOut = 90 if styleOff == "Perpendicular" else 0 + + toolRadius = PathDressup.toolController(obj.Base).Tool.Diameter.Value / 2 if hasattr(obj, "Length"): - # Replace Length by LengthIn + # Replace Length by PercentageRadiusIn + obj.addProperty( + "App::PropertyInteger", + "PercentageRadiusIn", + "Path Lead-in", + QT_TRANSLATE_NOOP("App::Property", "Determine length of the Lead-In"), + ) + obj.PercentageRadiusIn = int(obj.Length / toolRadius * 100) + obj.removeProperty("Length") + if hasattr(obj, "LengthOut"): + # Replace LengthOut by PercentageRadiusOut + obj.addProperty( + "App::PropertyInteger", + "PercentageRadiusOut", + "Path Lead-out", + QT_TRANSLATE_NOOP("App::Property", "Determine length of the Lead-Out"), + ) + obj.PercentageRadiusOut = int(obj.LengthOut / toolRadius * 100) + obj.removeProperty("LengthOut") + + # The new features do not have a good analog for ExtendLeadIn/Out, so these old values will be ignored + if hasattr(obj, "ExtendLeadIn"): + # Remove ExtendLeadIn property + obj.removeProperty("ExtendLeadIn") + if hasattr(obj, "ExtendLeadOut"): + # Remove ExtendLeadOut property + obj.removeProperty("ExtendLeadOut") + if hasattr(obj, "IncludeLayers"): + obj.removeProperty("IncludeLayers") + + if not hasattr(obj, "InvertIn"): + obj.addProperty( + "App::PropertyBool", + "InvertIn", + "Path Lead-in", + QT_TRANSLATE_NOOP("App::Property", "Invert Lead-In direction"), + ) + if not hasattr(obj, "InvertOut"): + obj.addProperty( + "App::PropertyBool", + "InvertOut", + "Path Lead-out", + QT_TRANSLATE_NOOP("App::Property", "Invert Lead-Out direction"), + ) + if not hasattr(obj, "OffsetIn"): obj.addProperty( "App::PropertyDistance", - "LengthIn", - "Path", - QT_TRANSLATE_NOOP("App::Property", "Length or radius of the approach"), + "OffsetIn", + "Path Lead-in", + QT_TRANSLATE_NOOP("App::Property", "Move start point"), ) - obj.LengthIn = obj.Length - obj.removeProperty("Length") + if not hasattr(obj, "OffsetOut"): + obj.addProperty( + "App::PropertyDistance", + "OffsetOut", + "Path Lead-out", + QT_TRANSLATE_NOOP("App::Property", "Move end point"), + ) + if not hasattr(obj, "RetractThreshold"): + obj.addProperty( + "App::PropertyLength", + "RetractThreshold", + "Path", + QT_TRANSLATE_NOOP( + "App::Property", + "Set distance which will attempts to avoid unnecessary retractions", + ), + ) + if hasattr(obj, "KeepToolDown"): + if obj.KeepToolDown: + obj.RetractThreshold = 999999 + obj.removeProperty("KeepToolDown") - def getDirectionOfPath(self, obj): + # Get direction for lead-in/lead-out in XY plane + def getLeadDir(self, obj, invert=False): + output = math.pi / 2 op = PathDressup.baseOp(obj.Base) side = op.Side if hasattr(op, "Side") else "Inside" direction = op.Direction if hasattr(op, "Direction") else "CCW" + if (side == "Inside" and direction == "CW") or (side == "Outside" and direction == "CCW"): + output = -output + if invert: + output = -output - if side == "Outside": - return "left" if direction == "CW" else "right" - else: - return "right" if direction == "CW" else "left" + return output - def getArcDirection(self, obj): - direction = self.getDirectionOfPath(obj) - return math.pi / 2 if direction == "left" else -math.pi / 2 + # Get direction of original path + def getPathDir(self, obj): + # only CW or CCW is matter + op = PathDressup.baseOp(obj.Base) + direction = op.Direction if hasattr(op, "Direction") else "CCW" + output = math.pi / 2 + if direction == "CW": + output = -output - def getTravelStart(self, obj, pos, first): + return output + + # Create safety movements to start point + def getTravelStart(self, obj, pos, first, inInstrPrev, outInstrPrev): op = PathDressup.baseOp(obj.Base) vertfeed = PathDressup.toolController(obj.Base).VertFeed.Value - travel = [] + commands = [] + posPrev = outInstrPrev.positionEnd() if outInstrPrev else App.Vector() + posPrevXY = App.Vector(posPrev.x, posPrev.y, 0) + posXY = App.Vector(pos.x, pos.y, 0) + distance = posPrevXY.distanceToPoint(posXY) - # begin positions for travel and plunge moves are not used anywhere, - # skipping them makes our life a lot easier + if first or (distance > obj.RetractThreshold): + # move to clearance height + commands.append(PathLanguage.MoveStraight(None, "G00", {"Z": op.ClearanceHeight.Value})) - # move to clearance height - if first: - travel.append(PathLanguage.MoveStraight(None, "G0", {"Z": op.ClearanceHeight.Value})) + # move to mill X/Y-position (without move Z) + commands.append(PathLanguage.MoveStraight(None, "G00", {"X": pos.x, "Y": pos.y})) - # move to correct xy-position - travel.append(PathLanguage.MoveStraight(None, "G0", {"X": pos.x, "Y": pos.y})) + if distance > obj.RetractThreshold: + # move vertical down to mill position + if obj.RapidPlunge: + # move to mill position rapidly + commands.append(PathLanguage.MoveStraight(None, "G00", {"Z": pos.z})) + else: + # move to mill position in one or two steps + if first: + # move down to SafeHeight + commands.append( + PathLanguage.MoveStraight(None, "G00", {"Z": op.SafeHeight.Value}) + ) + commands.append(PathLanguage.MoveStraight(None, "G01", {"Z": pos.z, "F": vertfeed})) + + elif obj.StyleOut == "Helix": + # move by helix to next mill position + if obj.StyleIn == "Helix": + halfStepZ = (posPrev.z - pos.z) / 2 + stepOutZ = halfStepZ * outInstrPrev.arcAngle() / math.pi + lastZMove = stepOutZ + else: + stepOutZ = posPrev.z - pos.z + lastZMove = 0 + outInstrPrev.param["Z"] = posPrev.z - stepOutZ + if not Path.Geom.pointsCoincide(posPrevXY, posXY): + if obj.RapidPlunge: + commands.append( + PathLanguage.MoveStraight( + outInstrPrev.positionEnd(), + "G00", + {"X": pos.x, "Y": pos.y, "Z": pos.z + lastZMove}, + ) + ) + else: + commands.append( + PathLanguage.MoveStraight( + outInstrPrev.positionEnd(), + "G01", + {"X": pos.x, "Y": pos.y, "Z": pos.z + lastZMove, "F": vertfeed}, + ) + ) - # move to correct z-position (either rapidly or in two steps) - if obj.RapidPlunge: - travel.append(PathLanguage.MoveStraight(None, "G0", {"Z": pos.z})) else: - if first or not obj.KeepToolDown: - travel.append(PathLanguage.MoveStraight(None, "G0", {"Z": op.SafeHeight.Value})) - travel.append(PathLanguage.MoveStraight(None, "G1", {"Z": pos.z, "F": vertfeed})) + # move to next mill position by short path + if obj.RapidPlunge: + commands.append( + PathLanguage.MoveStraight(None, "G00", {"X": pos.x, "Y": pos.y, "Z": pos.z}) + ) + else: + commands.append( + PathLanguage.MoveStraight( + None, "G01", {"X": pos.x, "Y": pos.y, "Z": pos.z, "F": vertfeed} + ) + ) - return travel + return commands - def getTravelEnd(self, obj, pos, last): + # Create commands with movements to clearance height + def getTravelEnd(self, obj): + commands = [] op = PathDressup.baseOp(obj.Base) - travel = [] + z = op.ClearanceHeight.Value + commands.append(PathLanguage.MoveStraight(None, "G00", {"Z": z})) - # move to clearance height - if last or not obj.KeepToolDown: - travel.append(PathLanguage.MoveStraight(None, "G0", {"Z": op.ClearanceHeight.Value})) - - return travel + return commands + # Create vector object from angle def angleToVector(self, angle): return App.Vector(math.cos(angle), math.sin(angle), 0) - def createArcMove(self, obj, begin, end, c): + # Create arc in XY plane with automatic detection G2|G3 + def createArcMove(self, obj, begin, end, offset, invert=False): horizfeed = PathDressup.toolController(obj.Base).HorizFeed.Value - - param = {"X": end.x, "Y": end.y, "I": c.x, "J": c.y, "F": horizfeed} - if self.getArcDirection(obj) > 0: - return PathLanguage.MoveArcCCW(begin, "G3", param) + param = {"X": end.x, "Y": end.y, "Z": end.z, "I": offset.x, "J": offset.y, "F": horizfeed} + if self.getLeadDir(obj, invert) > 0: + command = PathLanguage.MoveArcCCW(begin, "G3", param) else: - return PathLanguage.MoveArcCW(begin, "G2", param) + command = PathLanguage.MoveArcCW(begin, "G2", param) + return command + + # Create arc in XY plane with manually set G2|G3 + def createArcMoveN(self, obj, begin, end, offset, cmdName): + horizfeed = PathDressup.toolController(obj.Base).HorizFeed.Value + param = {"X": end.x, "Y": end.y, "I": offset.x, "J": offset.y, "F": horizfeed} + if cmdName == "G2": + command = PathLanguage.MoveArcCW(begin, cmdName, param) + else: + command = PathLanguage.MoveArcCCW(begin, cmdName, param) + + return command + + # Create line movement G1 def createStraightMove(self, obj, begin, end): horizfeed = PathDressup.toolController(obj.Base).HorizFeed.Value + param = {"X": end.x, "Y": end.y, "Z": end.z, "F": horizfeed} + command = PathLanguage.MoveStraight(begin, "G1", param) - param = {"X": end.x, "Y": end.y, "F": horizfeed} - return PathLanguage.MoveStraight(begin, "G1", param) + return command - def getLeadStart(self, obj, move, first): - lead = [] - begin = move.positionBegin() + # Get optimal step angle for iteration ArcZ + def getStepAngleArcZ(self, obj, radius): + job = PathUtils.findParentJob(obj) + minArcLength = job.GeometryTolerance.Value * 2 + maxArcLength = 1 + stepAngle = math.pi / 60 + stepArcLength = stepAngle * radius + if stepArcLength > maxArcLength: + # limit max arc length by 1 mm + stepAngle = maxArcLength / radius + elif stepArcLength < minArcLength: + # limit min arc length by geometry tolerance + stepAngle = minArcLength / radius - def prepend(instr): - nonlocal lead - nonlocal begin - lead.insert(0, instr) - begin = lead[0].positionBegin() + return stepAngle + + # Create vertical arc with move Down by line segments + def createArcZMoveDown(self, obj, begin, end, radius): + commands = [] + horizfeed = PathDressup.toolController(obj.Base).HorizFeed.Value + angle = math.acos((radius - begin.z + end.z) / radius) # start angle + stepAngle = self.getStepAngleArcZ(obj, radius) + iters = math.ceil(angle / stepAngle) + iterBegin = copy.copy(begin) # start point of short segment + iter = 1 + v = end - begin + n = math.hypot(v.x, v.y) + u = v / n + while iter <= iters: + if iter < iters: + angle -= stepAngle + distance = n - radius * math.sin(angle) + iterEnd = begin + u * distance + iterEnd.z = end.z + radius * (1 - math.cos(angle)) + else: + # exclude error of calculations for the last iteration + iterEnd = copy.copy(end) + param = {"X": iterEnd.x, "Y": iterEnd.y, "Z": iterEnd.z, "F": horizfeed} + commands.append(PathLanguage.MoveStraight(iterBegin, "G1", param)) + iterBegin = copy.copy(iterEnd) + iter += 1 + + return commands + + # Create vertical arc with move Up by line segments + def createArcZMoveUp(self, obj, begin, end, radius): + commands = [] + horizfeed = PathDressup.toolController(obj.Base).HorizFeed.Value + angleMax = math.acos((radius - end.z + begin.z) / radius) # finish angle + stepAngle = self.getStepAngleArcZ(obj, radius) + iters = math.ceil(angleMax / stepAngle) + iterBegin = copy.copy(begin) # start point of short segment + iter = 1 + v = end - begin + n = math.hypot(v.x, v.y) + u = v / n + angle = 0 # start angle + while iter <= iters: + if iter < iters: + angle += stepAngle + distance = radius * math.sin(angle) + iterEnd = begin + u * distance + iterEnd.z = begin.z + radius * (1 - math.cos(angle)) + else: + # exclude the error of calculations of the last point + iterEnd = copy.copy(end) + param = {"X": iterEnd.x, "Y": iterEnd.y, "Z": iterEnd.z, "F": horizfeed} + commands.append(PathLanguage.MoveStraight(iterBegin, "G1", param)) + iterBegin = copy.copy(iterEnd) + iter += 1 + + return commands + + def getLeadStart(self, obj, move, first, inInstrPrev, outInstrPrev): # tangent begin move # <----_-----x-------------------x @@ -294,41 +577,114 @@ class ObjectDressup: # | | # x v - if obj.LeadIn: - length = obj.LengthIn.Value - angle = move.anglesOfTangents()[0] - tangent = -self.angleToVector(angle) * length - normal = self.angleToVector(angle + self.getArcDirection(obj)) * length + lead = [] + begin = move.positionBegin() - # prepend the selected lead-in - if obj.StyleIn == "Arc": - arcbegin = begin + tangent + normal - prepend(self.createArcMove(obj, arcbegin, begin, -tangent)) - elif obj.StyleIn == "Tangent": - prepend(self.createStraightMove(obj, begin + tangent, begin)) - else: # obj.StyleIn == "Perpendicular" - prepend(self.createStraightMove(obj, begin + normal, begin)) + if obj.StyleIn not in ["No Retract", "Vertical"]: + toolRadius = PathDressup.toolController(obj.Base).Tool.Diameter.Value / 2 + angleIn = math.radians(obj.AngleIn.Value) + length = obj.PercentageRadiusIn * toolRadius / 100 + angleTangent = move.anglesOfTangents()[0] + normalMax = ( + self.angleToVector(angleTangent + self.getLeadDir(obj, obj.InvertIn)) * length + ) - extend = obj.ExtendLeadIn.Value - if extend != 0: - # prepend extension - extendbegin = begin + normal / length * extend - prepend(self.createStraightMove(obj, extendbegin, begin)) + # Here you can find description of the calculations + # https://forum.freecad.org/viewtopic.php?t=97641 - # prepend travel moves - lead = self.getTravelStart(obj, begin, first) + lead + # prepend "Arc" style lead-in - arc in XY + # Arc3d the same as Arc, but increased Z start point + if obj.StyleIn in ["Arc", "Arc3d", "Helix"]: + # tangent and normal vectors in XY plane + arcRadius = length + tangentLength = math.sin(angleIn) * arcRadius + normalLength = arcRadius * (1 - math.cos(angleIn)) + tangent = -self.angleToVector(angleTangent) * tangentLength + normal = ( + self.angleToVector(angleTangent + self.getLeadDir(obj, obj.InvertIn)) + * normalLength + ) + arcBegin = begin + tangent + normal + arcCenter = begin + normalMax + arcOffset = arcCenter - arcBegin + lead.append(self.createArcMove(obj, arcBegin, begin, arcOffset, obj.InvertIn)) + + # prepend "Line" style lead-in - line in XY + # Line3d the same as Line, but increased Z start point + elif obj.StyleIn in ["Line", "Line3d"]: + # tangent and normal vectors in XY plane + tangentLength = math.cos(angleIn) * length + normalLength = math.sin(angleIn) * length + tangent = -self.angleToVector(angleTangent) * tangentLength + normal = ( + self.angleToVector(angleTangent + self.getLeadDir(obj, obj.InvertIn)) + * normalLength + ) + lineBegin = begin + tangent + normal + lead.append(self.createStraightMove(obj, lineBegin, begin)) + + # prepend "LineZ" style lead-in - vertical inclined line + # Should be apply only on straight Path segment + elif obj.StyleIn == "LineZ": + # tangent vector in XY plane + # normal vector is vertical + op = PathDressup.baseOp(obj.Base) + normalLengthMax = op.SafeHeight.Value - begin.z + normalLength = math.sin(angleIn) * length + # do not exceed Normal vector max length + normalLength = min(normalLength, normalLengthMax) + tangentLength = normalLength / math.tan(angleIn) + tangent = -self.angleToVector(angleTangent) * tangentLength + normal = App.Vector(0, 0, normalLength) + lineBegin = begin + tangent + normal + lead.append(self.createStraightMove(obj, lineBegin, begin)) + + # prepend "ArcZ" style lead-in - vertical Arc + # Should be apply only on straight Path segment + elif obj.StyleIn == "ArcZ": + # tangent vector in XY plane + # normal vector is vertical + op = PathDressup.baseOp(obj.Base) + arcRadius = length + normalLengthMax = op.SafeHeight.Value - begin.z + normalLength = arcRadius * (1 - math.cos(angleIn)) + if normalLength > normalLengthMax: + # do not exceed Normal vector max length + normalLength = normalLengthMax + # recalculate angle for limited normal length + angleIn = math.acos((arcRadius - normalLength) / arcRadius) + tangentLength = arcRadius * math.sin(angleIn) + tangent = -self.angleToVector(angleTangent) * tangentLength + normal = App.Vector(0, 0, normalLength) + arcBegin = begin + tangent + normal + lead.extend(self.createArcZMoveDown(obj, arcBegin, begin, arcRadius)) + + # replace 'begin' position by first lead-in command + begin = lead[0].positionBegin() + + if obj.StyleIn in ["Arc3d", "Line3d"]: + # up Z start point for Arc3d and Line3d + op = PathDressup.baseOp(obj.Base) + if inInstrPrev and inInstrPrev.z() > begin.z: + begin.z = inInstrPrev.z() + else: + begin.z = op.StartDepth.Value + lead[0].setPositionBegin(begin) + + # get complete start travel moves + if obj.StyleIn != "No Retract": + travelToStart = self.getTravelStart(obj, begin, first, inInstrPrev, outInstrPrev) + else: + # exclude any lead-in commands + horizfeed = PathDressup.toolController(obj.Base).HorizFeed.Value + param = {"X": begin.x, "Y": begin.y, "Z": begin.z, "F": horizfeed} + travelToStart = [PathLanguage.MoveStraight(None, "G01", param)] + + lead = travelToStart + lead return lead - def getLeadEnd(self, obj, move, last): - lead = [] - end = move.positionEnd() - - def append(instr): - nonlocal lead - nonlocal end - lead.append(instr) - end = lead[-1].positionEnd() + def getLeadEnd(self, obj, move, last, inInstrPrev, outInstrPrev): # move end tangent # x-------------------x-----_----> @@ -337,43 +693,290 @@ class ObjectDressup: # | | # v x - if obj.LeadOut: - length = obj.LengthOut.Value - angle = move.anglesOfTangents()[1] - tangent = self.angleToVector(angle) * length - normal = self.angleToVector(angle + self.getArcDirection(obj)) * length + lead = [] + end = move.positionEnd() - # append the selected lead-out - if obj.StyleOut == "Arc": - arcend = end + tangent + normal - append(self.createArcMove(obj, end, arcend, normal)) - elif obj.StyleOut == "Tangent": - append(self.createStraightMove(obj, end, end + tangent)) - else: # obj.StyleOut == "Perpendicular" - append(self.createStraightMove(obj, end, end + normal)) + if obj.StyleOut not in ["No Retract", "Vertical"]: + toolRadius = PathDressup.toolController(obj.Base).Tool.Diameter.Value / 2 + angleOut = math.radians(obj.AngleOut.Value) + length = obj.PercentageRadiusOut * toolRadius / 100 + angleTangent = move.anglesOfTangents()[1] + normalMax = ( + self.angleToVector(angleTangent + self.getLeadDir(obj, obj.InvertOut)) * length + ) - extend = obj.ExtendLeadOut.Value - if extend != 0: - # append extension - extendend = end + normal / length * extend - append(self.createStraightMove(obj, end, extendend)) + # Here you can find description of the calculations + # https://forum.freecad.org/viewtopic.php?t=97641 - # append travel moves - lead += self.getTravelEnd(obj, end, last) + # append "Arc" style lead-out - arc in XY + # Arc3d the same as Arc, but increased Z start point + if obj.StyleOut in ["Arc", "Arc3d", "Helix"]: + # tangent and normal vectors in XY plane + arcRadius = length + tangentLength = math.sin(angleOut) * arcRadius + normalLength = arcRadius * (1 - math.cos(angleOut)) + tangent = self.angleToVector(angleTangent) * tangentLength + normal = ( + self.angleToVector(angleTangent + self.getLeadDir(obj, obj.InvertOut)) + * normalLength + ) + arcEnd = end + tangent + normal + lead.append(self.createArcMove(obj, end, arcEnd, normalMax, obj.InvertOut)) + + # append "Line" style lead-out + # Line3d the same as Line, but increased Z start point + elif obj.StyleOut in ["Line", "Line3d"]: + # tangent and normal vectors in XY plane + tangentLength = math.cos(angleOut) * length + normalLength = math.sin(angleOut) * length + tangent = self.angleToVector(angleTangent) * tangentLength + normal = ( + self.angleToVector(angleTangent + self.getLeadDir(obj, obj.InvertOut)) + * normalLength + ) + lineEnd = end + tangent + normal + lead.append(self.createStraightMove(obj, end, lineEnd)) + + # append "LineZ" style lead-out - vertical inclined line + # Should be apply only on straight Path segment + elif obj.StyleOut == "LineZ": + # tangent vector in XY plane + # normal vector is vertical + op = PathDressup.baseOp(obj.Base) + normalLengthMax = op.StartDepth.Value - end.z + normalLength = math.sin(angleOut) * length + # do not exceed Normal vector max length + normalLength = min(normalLength, normalLengthMax) + tangentLength = normalLength / math.tan(angleOut) + tangent = self.angleToVector(angleTangent) * tangentLength + normal = App.Vector(0, 0, normalLength) + lineEnd = end + tangent + normal + lead.append(self.createStraightMove(obj, end, lineEnd)) + + # prepend "ArcZ" style lead-out - vertical Arc + # Should be apply only on straight Path segment + elif obj.StyleOut == "ArcZ": + # tangent vector in XY plane + # normal vector is vertical + op = PathDressup.baseOp(obj.Base) + arcRadius = length + normalLengthMax = op.SafeHeight.Value - end.z + normalLength = arcRadius * (1 - math.cos(angleOut)) + if normalLength > normalLengthMax: + # do not exceed Normal vector max length + normalLength = normalLengthMax + # recalculate angle for limited normal length + angleOut = math.acos((arcRadius - normalLength) / arcRadius) + tangentLength = arcRadius * math.sin(angleOut) + tangent = self.angleToVector(angleTangent) * tangentLength + normal = App.Vector(0, 0, normalLength) + arcEnd = end + tangent + normal + lead.extend(self.createArcZMoveUp(obj, end, arcEnd, arcRadius)) + + if obj.StyleOut in ["Arc3d", "Line3d"]: + # Up Z end point for Arc3d and Line3d + op = PathDressup.baseOp(obj.Base) + if outInstrPrev and outInstrPrev.positionBegin().z > end.z: + lead[-1].param["Z"] = outInstrPrev.positionBegin().z + else: + lead[-1].param["Z"] = op.StartDepth.Value + + # append travel moves to cleareance height after finish all profiles + if last and obj.StyleOut != "No Retract": + lead += self.getTravelEnd(obj) return lead - def isCuttingMove(self, obj, instr): - return ( - instr.isMove() - and not instr.isRapid() - and (not obj.IncludeLayers or not instr.isPlunge()) - ) + # Check command + def isCuttingMove(self, instr): + result = instr.isMove() and not instr.isRapid() and not instr.isPlunge() + return result - def findLastCuttingMoveIndex(self, obj, source): + # Get direction of non cut movements + def getMoveDir(self, instr): + if instr.positionBegin().z > instr.positionEnd().z: + return "Down" + elif instr.positionBegin().z < instr.positionEnd().z: + return "Up" + elif instr.pathLength() != 0: + return "Hor" + else: + # move command without change position + 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(obj, source[i]): + if self.isCuttingMove(source[i]): return i + + return None + + # Get finish index of mill command for one profile + def findLastCutMultiProfileIndex(self, source, startIndex): + for i in range(startIndex, len(source), +1): + if not self.isCuttingMove(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() + + if Path.Geom.pointsCoincide(startPoint, endPoint): + # closed profile + # get extra commands from end of the closed profile + measuredLength = 0 + for i, instr in enumerate(reversed(source[start : end + 1])): + instrLength = instr.pathLength() + + if Path.Geom.isRoughly(measuredLength + instrLength, length, 1): + # get needed length and not need to cut last command + commands = source[end - i : end + 1] + return commands + + elif measuredLength + instrLength > length: + # measured length exceed needed length and need cut command + commands = source[end - i + 1 : end + 1] + newLength = length - measuredLength + newInstr = self.cutInstrBegin(obj, instr, newLength) + commands.insert(0, newInstr) + return commands + + measuredLength += instrLength + + else: + # open profile + # extend first move + instr = source[start] + newLength = length + instr.pathLength() + newInstr = self.cutInstrBegin(obj, instr, newLength) + return [newInstr] + + return None + + # Increse 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): + # closed profile + # get extra commands from begin of the closed profile + measuredLength = 0 + for i, instr in enumerate(source[start:end]): + instrLength = instr.pathLength() + + if Path.Geom.isRoughly(measuredLength + instrLength, length, 1): + # get needed length and not need to cut last command + commands = source[start : start + i + 1] + return commands + + elif measuredLength + instrLength > length: + # measured length exceed needed length and need cut command + commands = source[start : start + i] + newLength = length - measuredLength + newInstr = self.cutInstrEnd(obj, instr, newLength) + commands.append(newInstr) + return commands + + measuredLength += instrLength + + else: + # open profile + # extend last move + instr = source[end] + newLength = length + instr.pathLength() + newInstr = self.cutInstrEnd(obj, instr, newLength) + return [newInstr] + + return None + + # Cut travel end by distance (negative overtravel out) + def cutTravelEnd(self, obj, source, cutLength): + measuredLength = 0 + + 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 + + elif measuredLength > cutLength: + # measured length exceed needed cut length and need cut command + commands = source[: -i - 1] + newLength = measuredLength - cutLength + newInstr = self.cutInstrEnd(obj, instr, newLength) + commands.append(newInstr) + return commands + + return None + + # Change end point of instruction + def cutInstrEnd(self, obj, instr, newLength): + command = None + # Cut straight move from begin + if instr.isStraight(): + begin = instr.positionBegin() + end = instr.positionEnd() + v = end - begin + n = math.hypot(v.x, v.y) + u = v / n + cutEnd = begin + u * newLength + command = self.createStraightMove(obj, begin, cutEnd) + + # Cut arc move from begin + elif instr.isArc(): + angleTangent = instr.anglesOfTangents()[0] + arcBegin = instr.positionBegin() + arcOffset = App.Vector(instr.i(), instr.j(), instr.k()) + arcRadius = instr.arcRadius() + arcAngle = newLength / arcRadius + tangentLength = math.sin(arcAngle) * arcRadius + normalLength = arcRadius * (1 - math.cos(arcAngle)) + tangent = self.angleToVector(angleTangent) * tangentLength + normal = self.angleToVector(angleTangent + self.getPathDir(obj)) * normalLength + arcEnd = arcBegin + tangent + normal + cmdName = "G2" if instr.isCW() else "G3" + command = self.createArcMoveN(obj, arcBegin, arcEnd, arcOffset, cmdName) + + return command + + # Change start point of instruction + def cutInstrBegin(self, obj, instr, newLength): + # Cut straignt move from begin + if instr.isStraight(): + begin = instr.positionBegin() + end = instr.positionEnd() + v = end - begin + n = math.hypot(v.x, v.y) + u = v / n + newBegin = end - u * newLength + command = self.createStraightMove(obj, newBegin, end) + return command + + # Cut arc move from begin + elif instr.isArc(): + angleTangent = instr.anglesOfTangents()[1] + arcEnd = instr.positionEnd() + arcCenter = instr.xyCenter() + arcRadius = instr.arcRadius() + arcAngle = newLength / arcRadius + tangentLength = math.sin(arcAngle) * arcRadius + normalLength = arcRadius * (1 - math.cos(arcAngle)) + tangent = -self.angleToVector(angleTangent) * tangentLength + normal = self.angleToVector(angleTangent + self.getPathDir(obj)) * normalLength + arcBegin = arcEnd + tangent + normal + arcOffset = arcCenter - arcBegin + cmdName = "G2" if instr.isCW() else "G3" + command = self.createArcMoveN(obj, arcBegin, arcEnd, arcOffset, cmdName) + return command + return None def generateLeadInOutCurve(self, obj): @@ -383,32 +986,120 @@ class ObjectDressup: # Knowing weather 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. - first = True - lastCuttingMoveIndex = self.findLastCuttingMoveIndex(obj, source) + first = True # prepare first move at cleareance height + firstMillIndex = None # Index start mill instruction for one profile + lastCuttingMoveIndex = self.findLastCuttingMoveIndex(source) + inInstrPrev = None + outInstrPrev = None + measuredLength = 0 # for negative OffsetIn + skipCounter = 0 # for negative OffsetIn + commands = [] + moveDir = None + + # Process all instructions for i, instr in enumerate(source): - if not self.isCuttingMove(obj, instr): - # non-move instructions get added verbatim - if not instr.isMove(): - maneuver.addInstruction(instr) - # skip travel and plunge moves, travel moves will be added in - # getLeadStart and getLeadEnd + # Process not mill instruction + if not self.isCuttingMove(instr): + if not instr.isMove(): + # non-move instruction get added verbatim + commands.append(instr) + else: + moveDir = self.getMoveDir(instr) + if not obj.LeadIn and (moveDir in ["Down", "Hor"] or first): + # keep original Lead-in movements + commands.append(instr) + elif not obj.LeadOut and moveDir in ["Up"] and not first: + # keep original Lead-out movements + commands.append(instr) + # skip travel and plunge moves if LeadInOut will be process + # travel moves will be added in getLeadStart and getLeadEnd continue - if first or not self.isCuttingMove(obj, source[i - 1]): - # add lead start and travel moves - maneuver.addInstructions(self.getLeadStart(obj, instr, first)) + # measuring length for one profile + if self.isCuttingMove(instr): + measuredLength += instr.pathLength() + + # Process Lead-In + if first or not self.isCuttingMove(source[i - 1 - skipCounter]): + if obj.LeadIn: + # Process negative Offset Lead-In (cut travel from begin) + if obj.OffsetIn.Value < 0 and obj.StyleIn != "No Retract": + if measuredLength <= abs(obj.OffsetIn.Value): + # skip mill instruction + skipCounter += 1 # count skipped instructions + continue + else: + skipCounter = 0 + # cut mill instruction + 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, + ) + if overtravelIn: + commands.extend( + self.getLeadStart( + obj, overtravelIn[0], first, inInstrPrev, outInstrPrev + ) + ) + commands.extend(overtravelIn) + else: + commands.extend( + self.getLeadStart(obj, instr, first, inInstrPrev, outInstrPrev) + ) + firstMillIndex = i if not firstMillIndex else firstMillIndex + inInstrPrev = commands[-1] first = False - # add current move - maneuver.addInstruction(instr) + # Add mill instruction + commands.append(instr) + + # Process Lead-Out + last = bool(i == 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": + commands = self.cutTravelEnd(obj, commands, abs(obj.OffsetOut.Value)) + + # 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 + if overtravelOut: + commands.extend(overtravelOut) - last = i == lastCuttingMoveIndex - if last or not self.isCuttingMove(obj, source[i + 1]): # add lead end and travel moves - maneuver.addInstructions(self.getLeadEnd(obj, instr, last)) + leadEndInstr = self.getLeadEnd(obj, commands[-1], last, inInstrPrev, outInstrPrev) + commands.extend(leadEndInstr) + # Last mill position to check RetractThreshold + if leadEndInstr: + outInstrPrev = leadEndInstr[-1] + else: + outInstrPrev = instr + + maneuver.addInstructions(commands) return maneuver.toPath() @@ -417,19 +1108,30 @@ class TaskDressupLeadInOut(SimpleEditPanel): _ui_file = ":/panels/DressUpLeadInOutEdit.ui" def setupUi(self): - self.connectWidget("LeadIn", self.form.chkLeadIn) - self.connectWidget("LeadOut", self.form.chkLeadOut) - self.connectWidget("LengthIn", self.form.dspLenIn) - self.connectWidget("LengthOut", self.form.dspLenOut) - self.connectWidget("ExtendLeadIn", self.form.dspExtendIn) - self.connectWidget("ExtendLeadOut", self.form.dspExtendOut) + 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.connectWidget("AngleIn", self.form.dspAngleIn) + self.connectWidget("AngleOut", self.form.dspAngleOut) + self.connectWidget("OffsetIn", self.form.dspOffsetIn) + self.connectWidget("OffsetOut", self.form.dspOffsetOut) self.connectWidget("RapidPlunge", self.form.chkRapidPlunge) - self.connectWidget("IncludeLayers", self.form.chkLayers) - self.connectWidget("KeepToolDown", self.form.chkKeepToolDown) self.setFields() + styleEnum = self.obj.getEnumerationsOfProperty("StyleIn") + + def handleGroupBoxCheck(): + self.obj.LeadIn = self.form.groupBoxIn.isChecked() + self.obj.LeadOut = self.form.groupBoxOut.isChecked() + + self.form.groupBoxIn.setChecked(self.obj.LeadIn) + self.form.groupBoxOut.setChecked(self.obj.LeadOut) + self.form.groupBoxIn.clicked.connect(handleGroupBoxCheck) + self.form.groupBoxOut.clicked.connect(handleGroupBoxCheck) + class ViewProviderDressup: def __init__(self, vobj): @@ -489,7 +1191,7 @@ class CommandPathDressupLeadInOut: "MenuText": QT_TRANSLATE_NOOP("CAM_DressupLeadInOut", "Lead In/Out"), "ToolTip": QT_TRANSLATE_NOOP( "CAM_DressupLeadInOut", - "Creates a cutter radius compensation G41/G42 entry dressup object from a selected path", + "Creates entry and exit motions for a selected path", ), } diff --git a/src/Mod/CAM/PathPythonGui/simple_edit_panel.py b/src/Mod/CAM/PathPythonGui/simple_edit_panel.py index a581b85451..0c03ba86f5 100644 --- a/src/Mod/CAM/PathPythonGui/simple_edit_panel.py +++ b/src/Mod/CAM/PathPythonGui/simple_edit_panel.py @@ -5,7 +5,11 @@ from PySide import QtGui translate = FreeCAD.Qt.translate PROP_TYPE_QTYES = ["App::PropertyDistance", "App::PropertyAngle"] -PROP_TYPE_NUMERIC = PROP_TYPE_QTYES + ["App::PropertyPercent", "App:PropertyFloat"] +PROP_TYPE_NUMERIC = PROP_TYPE_QTYES + [ + "App::PropertyPercent", + "App::PropertyInteger", + "App:PropertyFloat", +] class SimpleEditPanel: @@ -30,7 +34,10 @@ class SimpleEditPanel: def getFields(self): for prop_name, (get_field, set_field) in self._fc.items(): - setattr(self.obj, prop_name, get_field()) + val = get_field() + if isinstance(getattr(self.obj, prop_name), int): + val = int(val) + setattr(self.obj, prop_name, val) def setFields(self): for prop_name, (get_field, set_field) in self._fc.items():