From c862fc0065b35ee34835b519af9f7ff54aaf7fe4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20H=C3=BCdepohl?= Date: Sun, 12 Jun 2016 15:23:11 +0200 Subject: [PATCH] Path: add plunge angle feature for PathProfile With the new property "PlungeAngle", one can specify the inclination of a ramp into the material, instead of plunging straight down. The original behaviour and default is set to 90.0 degrees. A value of zero makes the tool descent exactly one layer depth down per turn. --- .../Path/Gui/Resources/panels/ProfileEdit.ui | 10 ++ src/Mod/Path/PathScripts/PathProfile.py | 9 +- src/Mod/Path/PathScripts/PathUtils.py | 132 ++++++++++++++---- 3 files changed, 119 insertions(+), 32 deletions(-) diff --git a/src/Mod/Path/Gui/Resources/panels/ProfileEdit.ui b/src/Mod/Path/Gui/Resources/panels/ProfileEdit.ui index 825fb1a777..515b614791 100644 --- a/src/Mod/Path/Gui/Resources/panels/ProfileEdit.ui +++ b/src/Mod/Path/Gui/Resources/panels/ProfileEdit.ui @@ -435,6 +435,16 @@ + + + + Plunge Angle + + + + + + diff --git a/src/Mod/Path/PathScripts/PathProfile.py b/src/Mod/Path/PathScripts/PathProfile.py index acbdef7079..67f1cd0711 100644 --- a/src/Mod/Path/PathScripts/PathProfile.py +++ b/src/Mod/Path/PathScripts/PathProfile.py @@ -98,6 +98,7 @@ class ObjectProfile: obj.addProperty("App::PropertyDistance", "RollRadius", "Profile", "Radius at start and end") obj.addProperty("App::PropertyDistance", "OffsetExtra", "Profile", "Extra value to stay away from final profile- good for roughing toolpath") obj.addProperty("App::PropertyLength", "SegLen", "Profile", "Tesselation value for tool paths made from beziers, bsplines, and ellipses") + obj.addProperty("App::PropertyAngle", "PlungeAngle", "Profile", "Plunge angle with which the tool enters the work piece. Straight down is 90 degrees, if set small enough or zero the tool will descent exactly one layer depth down per turn") obj.addProperty("App::PropertyVectorList", "locs", "Tags", "List of holding tag locations") @@ -183,7 +184,7 @@ class ObjectProfile: wire, obj.Side, self.radius, clockwise, obj.ClearanceHeight.Value, obj.StepDown, obj.StartDepth.Value, obj.FinalDepth.Value, FirstEdge, PathClosed, obj.SegLen.Value, - self.vertFeed, self.horizFeed) + self.vertFeed, self.horizFeed, PlungeAngle=obj.PlungeAngle.Value) return output @@ -466,6 +467,7 @@ class CommandPathProfile: FreeCADGui.doCommand('obj.OffsetExtra = 0.0') FreeCADGui.doCommand('obj.Direction = "CW"') FreeCADGui.doCommand('obj.UseComp = False') + FreeCADGui.doCommand('obj.PlungeAngle = 90.0') FreeCADGui.doCommand('PathScripts.PathUtils.addToProject(obj)') FreeCAD.ActiveDocument.commitTransaction() @@ -509,6 +511,8 @@ class TaskPanel: self.obj.SegLen = self.form.segLen.value() if hasattr(self.obj, "RollRadius"): self.obj.RollRadius = self.form.rollRadius.value() + if hasattr(self.obj, "PlungeAngle"): + self.obj.PlungeAngle = str(self.form.plungeAngle.value()) if hasattr(self.obj, "UseComp"): self.obj.UseComp = self.form.useCompensation.isChecked() if hasattr(self.obj, "UseStartPoint"): @@ -523,7 +527,7 @@ class TaskPanel: self.obj.Direction = str(self.form.direction.currentText()) self.obj.Proxy.execute(self.obj) - def setFields(self): + def setFields(self): self.form.startDepth.setText(str(self.obj.StartDepth.Value)) self.form.finalDepth.setText(str(self.obj.FinalDepth.Value)) self.form.safeHeight.setText(str(self.obj.SafeHeight.Value)) @@ -532,6 +536,7 @@ class TaskPanel: self.form.extraOffset.setValue(self.obj.OffsetExtra.Value) self.form.segLen.setValue(self.obj.SegLen.Value) self.form.rollRadius.setValue(self.obj.RollRadius.Value) + self.form.plungeAngle.setValue(self.obj.PlungeAngle.Value) self.form.useCompensation.setChecked(self.obj.UseComp) self.form.useStartPoint.setChecked(self.obj.UseStartPoint) self.form.useEndPoint.setChecked(self.obj.UseEndPoint) diff --git a/src/Mod/Path/PathScripts/PathUtils.py b/src/Mod/Path/PathScripts/PathUtils.py index 200febc440..cc5e9b82c9 100644 --- a/src/Mod/Path/PathScripts/PathUtils.py +++ b/src/Mod/Path/PathScripts/PathUtils.py @@ -184,18 +184,21 @@ def reverseEdge(e): return newedge -def convert(toolpath, Side, radius, clockwise=False, Z=0.0, firstedge=None, vf=1.0, hf=2.0): +def convert(toolpath, Side, radius, clockwise=False, Z=0.0, firstedge=None, vf=1.0, hf=2.0, PlungeAngle=90.0, Zprevious=None, StopLength=None): '''convert(toolpath,Side,radius,clockwise=False,Z=0.0,firstedge=None) Converts lines and arcs to G1,G2,G3 moves. Returns a string.''' - last = None - output = "" - # create the path from the offset shape - for edge in toolpath: - if not last: - # set the first point - last = edge.Vertexes[0].Point - # FreeCAD.Console.PrintMessage("last pt= " + str(last)+ "\n") - output += "G1 X" + str(fmt(last.x)) + " Y" + str(fmt(last.y)) + \ - " Z" + str(fmt(Z)) + " F" + str(vf) + "\n" + + if PlungeAngle != 90.0: + if Zprevious is None: + raise Exception("Cannot use PlungeAngle != 90.0 degrees without parameter Zprevious") + tanA = math.tan(math.pi * PlungeAngle / 180.0) + minA = (Zprevious - Z) / sum(edge.Length for edge in toolpath) + if tanA < minA: + tanA = minA + #FreeCAD.Console.PrintMessage('Increasing ramp angle to {0} degrees, to be able to make a full round\n'.format(math.atan(tanA) * 180.0 / math.pi)) + else: + Zprevious = Z + + def edge_to_path(lastpt, edge, Z): if isinstance(edge.Curve, Part.Circle): # FreeCAD.Console.PrintMessage("arc\n") arcstartpt = edge.valueAt(edge.FirstParameter) @@ -204,14 +207,14 @@ def convert(toolpath, Side, radius, clockwise=False, Z=0.0, firstedge=None, vf=1 arcendpt = edge.valueAt(edge.LastParameter) # arcchkpt = edge.valueAt(edge.LastParameter * .99) - if DraftVecUtils.equals(last, arcstartpt): + if DraftVecUtils.equals(lastpt, arcstartpt): startpt = arcstartpt endpt = arcendpt else: startpt = arcendpt endpt = arcstartpt center = edge.Curve.Center - relcenter = center.sub(last) + relcenter = center.sub(lastpt) # FreeCAD.Console.PrintMessage("arc startpt= " + str(startpt)+ "\n") # FreeCAD.Console.PrintMessage("arc midpt= " + str(midpt)+ "\n") # FreeCAD.Console.PrintMessage("arc endpt= " + str(endpt)+ "\n") @@ -219,27 +222,82 @@ def convert(toolpath, Side, radius, clockwise=False, Z=0.0, firstedge=None, vf=1 [(startpt.x, startpt.y), (midpt.x, midpt.y), (endpt.x, endpt.y)]) # FreeCAD.Console.PrintMessage("arc_cw="+ str(arc_cw)+"\n") if arc_cw: - output += "G2" + output = "G2" else: - output += "G3" + output = "G3" output += " X" + str(fmt(endpt.x)) + " Y" + \ str(fmt(endpt.y)) + " Z" + str(fmt(Z)) + " F" + str(hf) output += " I" + str(fmt(relcenter.x)) + " J" + \ str(fmt(relcenter.y)) + " K" + str(fmt(relcenter.z)) output += "\n" - last = endpt - # FreeCAD.Console.PrintMessage("last pt arc= " + str(last)+ "\n") + lastpt = endpt + # FreeCAD.Console.PrintMessage("last pt arc= " + str(lastpt)+ "\n") else: point = edge.Vertexes[-1].Point - if DraftVecUtils.equals(point, last): # edges can come flipped + if DraftVecUtils.equals(point, lastpt): # edges can come flipped point = edge.Vertexes[0].Point - output += "G1 X" + str(fmt(point.x)) + " Y" + str(fmt(point.y)) + \ + output = "G1 X" + str(fmt(point.x)) + " Y" + str(fmt(point.y)) + \ " Z" + str(fmt(Z)) + " F" + str(hf) + "\n" - last = point + lastpt = point # FreeCAD.Console.PrintMessage("line\n") - # FreeCAD.Console.PrintMessage("last pt line= " + str(last)+ "\n") - return output + # FreeCAD.Console.PrintMessage("last pt line= " + str(lastpt)+ "\n") + return lastpt, output + lastpt = None + output = "" + path_length = 0.0 + Z_cur = Zprevious + + # create the path from the offset shape + for edge in toolpath: + if not lastpt: + # set the first point + lastpt = edge.Vertexes[0].Point + # FreeCAD.Console.PrintMessage("last pt= " + str(lastpt)+ "\n") + output += "G1 X" + str(fmt(lastpt.x)) + " Y" + str(fmt(lastpt.y)) + \ + " Z" + str(fmt(Z_cur)) + " F" + str(vf) + "\n" + + if StopLength: + if path_length + edge.Length > StopLength: + # have to split current edge in two + t0 = edge.FirstParameter + t1 = edge.LastParameter + dL = StopLength - path_length + t = t0 + (t1 - t0) * dL / edge.Length + assert(t0 < t < t1) + edge = edge.split(t).Edges[0] + path_length = StopLength + else: + path_length += edge.Length + else: + path_length += edge.Length + + if Z_cur > Z: + Z_next = Zprevious - path_length * tanA + if Z_next < Z: + # have to split current edge in two + t0 = edge.FirstParameter + t1 = edge.LastParameter + dZ = Z_cur - Z + t = t0 + (t1 - t0) * (dZ / tanA) / edge.Length + assert(t0 < t < t1) + subwire = edge.split(t) + assert(len(subwire.Edges) == 2) + Z_cur = Z + lastpt, codes = edge_to_path(lastpt, subwire.Edges[0], Z_cur) + output += codes + edge = subwire.Edges[1] + else: + Z_cur = Z_next + + lastpt, codes = edge_to_path(lastpt, edge, Z_cur) + output += codes + + if StopLength: + if path_length >= StopLength: + break + + return output def SortPath(wire, Side, radius, clockwise, firstedge=None, SegLen=0.5): '''SortPath(wire,Side,radius,clockwise,firstedge=None,SegLen =0.5) Sorts the wire and reverses it, if needed. Splits arcs over 180 degrees in two. Returns the reordered offset of the wire. ''' @@ -308,9 +366,9 @@ def SortPath(wire, Side, radius, clockwise, firstedge=None, SegLen=0.5): return offset -def MakePath(wire, Side, radius, clockwise, ZClearance, StepDown, ZStart, ZFinalDepth, firstedge=None, PathClosed=True, SegLen=0.5, VertFeed=1.0, HorizFeed=2.0): +def MakePath(wire, Side, radius, clockwise, ZClearance, StepDown, ZStart, ZFinalDepth, firstedge=None, PathClosed=True, SegLen=0.5, VertFeed=1.0, HorizFeed=2.0, PlungeAngle=90.0): ''' makes the path - just a simple profile for now ''' - offset = SortPath(wire, Side, radius, clockwise, firstedge, SegLen=0.5) + offset = SortPath(wire, Side, radius, clockwise, firstedge, SegLen=SegLen) if len(offset.Edges) == 0: return "" @@ -319,26 +377,40 @@ def MakePath(wire, Side, radius, clockwise, ZClearance, StepDown, ZStart, ZFinal paths += "G0 Z" + str(ZClearance) + "\n" first = toolpath[0].Vertexes[0].Point paths += "G0 X" + str(fmt(first.x)) + "Y" + str(fmt(first.y)) + "\n" + Zprevious = ZStart ZCurrent = ZStart - StepDown if PathClosed: while ZCurrent > ZFinalDepth: paths += convert(toolpath, Side, radius, clockwise, - ZCurrent, firstedge, VertFeed, HorizFeed) + ZCurrent, firstedge, VertFeed, HorizFeed, PlungeAngle=PlungeAngle, Zprevious=Zprevious) + Zprevious = ZCurrent ZCurrent = ZCurrent - abs(StepDown) paths += convert(toolpath, Side, radius, clockwise, - ZFinalDepth, firstedge, VertFeed, HorizFeed) - paths += "G0 Z" + str(ZClearance) + ZFinalDepth, firstedge, VertFeed, HorizFeed, PlungeAngle=PlungeAngle, Zprevious=Zprevious) else: while ZCurrent > ZFinalDepth: paths += convert(toolpath, Side, radius, clockwise, - ZCurrent, firstedge, VertFeed, HorizFeed) + ZCurrent, firstedge, VertFeed, HorizFeed, PlungeAngle=PlungeAngle, Zprevious=Zprevious) paths += "G0 Z" + str(ZClearance) paths += "G0 X" + str(fmt(first.x)) + "Y" + \ str(fmt(first.y)) + "\n" + Zprevious = ZCurrent ZCurrent = ZCurrent - abs(StepDown) paths += convert(toolpath, Side, radius, clockwise, - ZFinalDepth, firstedge, VertFeed, HorizFeed) - paths += "G0 Z" + str(ZClearance) + ZFinalDepth, firstedge, VertFeed, HorizFeed, PlungeAngle=PlungeAngle, Zprevious=Zprevious) + + + # have to do one last pass to clear the remaining ramp + if PlungeAngle != 90.0: + tanA = math.tan(math.pi * PlungeAngle / 180.0) + if tanA <= 0.0: + StopLength=None + else: + StopLength=abs(StepDown/tanA) + paths += convert(toolpath, Side, radius, clockwise, + ZFinalDepth, firstedge, VertFeed, HorizFeed, StopLength=StopLength) + + paths += "G0 Z" + str(ZClearance) return paths # the next two functions are for automatically populating tool