diff --git a/src/Mod/Path/PathFeedRate.py b/src/Mod/Path/PathFeedRate.py new file mode 100644 index 0000000000..64ee30f501 --- /dev/null +++ b/src/Mod/Path/PathFeedRate.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2021 sliptonic * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +import FreeCAD +import PathScripts.PathLog as PathLog +import PathMachineState +import PathScripts.PathGeom as PathGeom +import Part + +__title__ = "Feed Rate Helper Utility" +__author__ = "sliptonic (Brad Collette)" +__url__ = "https://www.freecadweb.org" +__doc__ = "Helper for adding Feed Rate to Path Commands" + +""" +TODO: This needs to be able to handle feedrates for axes other than X,Y,Z +""" + +if True: + PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) + PathLog.trackModule(PathLog.thisModule()) +else: + PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) + + +def setFeedRate(commandlist, ToolController): + def _isVertical(currentposition, command): + x = ( + command.Parameters["X"] + if "X" in command.Parameters.keys() + else currentposition.x + ) + y = ( + command.Parameters["Y"] + if "Y" in command.Parameters.keys() + else currentposition.y + ) + z = ( + command.Parameters["Z"] + if "Z" in command.Parameters.keys() + else currentposition.z + ) + endpoint = FreeCAD.Vector(x, y, z) + if currentposition == endpoint: + return True + return PathGeom.isVertical(Part.makeLine(currentposition, endpoint)) + + feedcommands = ["G01", "G1", "G2", "G3", "G02", "G03", "G81", "G82", "G83"] + rapidcommands = ["G0", "G00"] + + machine = PathMachineState.MachineState() + + for command in commandlist: + if command.Name not in feedcommands + rapidcommands: + continue + + if _isVertical(machine.getPosition(), command): + rate = ( + ToolController.VertRapid.Value + if command in rapidcommands + else ToolController.VertFeed.Value + ) + else: + rate = ( + ToolController.HorizRapid.Value + if command in rapidcommands + else ToolController.HorizFeed.Value + ) + + params = command.Parameters + params["F"] = rate + command.Parameters = params + + machine.addCommand(command) + + return commandlist diff --git a/src/Mod/Path/PathMachineState.py b/src/Mod/Path/PathMachineState.py new file mode 100644 index 0000000000..f852c66ef9 --- /dev/null +++ b/src/Mod/Path/PathMachineState.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2021 sliptonic shopinthewoods@gmail.com * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +__title__ = "Path Machine State" +__author__ = "sliptonic (Brad Collette)" +__url__ = "https://www.freecadweb.org" +__doc__ = "Dataclass to implement a machinestate tracker" +__contributors__ = "" + +import PathScripts.PathLog as PathLog +import FreeCAD +from dataclasses import dataclass, field + +if True: + PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) + PathLog.trackModule(PathLog.thisModule()) +else: + PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) + + +@dataclass +class MachineState: + WCSLIST = [ + "G53", + "G54", + "G55", + "G56", + "G57", + "G58", + "G59", + "G59.1", + "G59.2", + "G59.3", + "G59.4", + "G59.5", + "G59.6", + "G59.7", + "G59.8", + "G59.9", + ] + + X: float = field(default=None) + Y: float = field(default=None) + Z: float = field(default=None) + A: float = field(default=None) + B: float = field(default=None) + C: float = field(default=None) + F: float = field(default=None) + Coolant: bool = field(default=False) + WCS: str = field(default="G54") + Spindle: str = field(default="off") + S: int = field(default=0) + T: str = field(default=None) + + def addCommand(self, command): + if command.Name == "M6": + self.T = command.Parameters["T"] + return + + if command.Name in ["M3", "M4"]: + self.S = command.Parameters["S"] + self.SpindApple = "CW" if command.Name == "M3" else "CCW" + return + + if command.Name in ["M2", "M5"]: + self.S = 0 + self.Spindle = "off" + return + + if command.Name in self.WCSLIST: + self.WCS = command.Name + return + + for p in command.Parameters: + self.__setattr__(p, command.Parameters[p]) + + def getPosition(self): + """ + Returns an App.Vector of the current location + """ + x = 0 if self.X is None else self.X + y = 0 if self.Y is None else self.Y + z = 0 if self.Z is None else self.Z + + return FreeCAD.Vector(x, y, z) + diff --git a/src/Mod/Path/PathScripts/PathDrilling.py b/src/Mod/Path/PathScripts/PathDrilling.py index fbcbce910a..f7b626cf48 100644 --- a/src/Mod/Path/PathScripts/PathDrilling.py +++ b/src/Mod/Path/PathScripts/PathDrilling.py @@ -23,15 +23,19 @@ from __future__ import print_function + +from Generators import drill_generator as generator +from PySide import QtCore import FreeCAD +import Part import Path +import PathFeedRate +import PathMachineState import PathScripts.PathCircularHoleBase as PathCircularHoleBase import PathScripts.PathLog as PathLog import PathScripts.PathOp as PathOp import PathScripts.PathUtils as PathUtils -from PySide import QtCore - __title__ = "Path Drilling Operation" __author__ = "sliptonic (Brad Collette)" __url__ = "https://www.freecadweb.org" @@ -39,8 +43,8 @@ __doc__ = "Path Drilling operation." __contributors__ = "IMBack!" -PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) -# PathLog.trackModule(PathLog.thisModule()) +PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) +PathLog.trackModule(PathLog.thisModule()) # Qt translation handling @@ -49,104 +53,203 @@ def translate(context, text, disambig=None): class ObjectDrilling(PathCircularHoleBase.ObjectOp): - '''Proxy object for Drilling operation.''' + """Proxy object for Drilling operation.""" def circularHoleFeatures(self, obj): - '''circularHoleFeatures(obj) ... drilling works on anything, turn on all Base geometries and Locations.''' - return PathOp.FeatureBaseGeometry | PathOp.FeatureLocations | PathOp.FeatureCoolant + """circularHoleFeatures(obj) ... drilling works on anything, turn on all Base geometries and Locations.""" + return ( + PathOp.FeatureBaseGeometry | PathOp.FeatureLocations | PathOp.FeatureCoolant + ) def initCircularHoleOperation(self, obj): - '''initCircularHoleOperation(obj) ... add drilling specific properties to obj.''' - obj.addProperty("App::PropertyLength", "PeckDepth", "Drill", QtCore.QT_TRANSLATE_NOOP("App::Property", "Incremental Drill depth before retracting to clear chips")) - obj.addProperty("App::PropertyBool", "PeckEnabled", "Drill", QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable pecking")) - obj.addProperty("App::PropertyFloat", "DwellTime", "Drill", QtCore.QT_TRANSLATE_NOOP("App::Property", "The time to dwell between peck cycles")) - obj.addProperty("App::PropertyBool", "DwellEnabled", "Drill", QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable dwell")) - obj.addProperty("App::PropertyBool", "AddTipLength", "Drill", QtCore.QT_TRANSLATE_NOOP("App::Property", "Calculate the tip length and subtract from final depth")) - obj.addProperty("App::PropertyEnumeration", "ReturnLevel", "Drill", QtCore.QT_TRANSLATE_NOOP("App::Property", "Controls how tool retracts Default=G99")) - obj.addProperty("App::PropertyDistance", "RetractHeight", "Drill", QtCore.QT_TRANSLATE_NOOP("App::Property", "The height where feed starts and height during retract tool when path is finished while in a peck operation")) - obj.addProperty("App::PropertyEnumeration", "ExtraOffset", "Drill", QtCore.QT_TRANSLATE_NOOP("App::Property", "How far the drill depth is extended")) + """initCircularHoleOperation(obj) ... add drilling specific properties to obj.""" + obj.addProperty( + "App::PropertyLength", + "PeckDepth", + "Drill", + QtCore.QT_TRANSLATE_NOOP( + "App::Property", + "Incremental Drill depth before retracting to clear chips", + ), + ) + obj.addProperty( + "App::PropertyBool", + "PeckEnabled", + "Drill", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable pecking"), + ) + obj.addProperty( + "App::PropertyFloat", + "DwellTime", + "Drill", + QtCore.QT_TRANSLATE_NOOP( + "App::Property", "The time to dwell between peck cycles" + ), + ) + obj.addProperty( + "App::PropertyBool", + "DwellEnabled", + "Drill", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable dwell"), + ) + obj.addProperty( + "App::PropertyBool", + "AddTipLength", + "Drill", + QtCore.QT_TRANSLATE_NOOP( + "App::Property", + "Calculate the tip length and subtract from final depth", + ), + ) + obj.addProperty( + "App::PropertyEnumeration", + "ReturnLevel", + "Drill", + QtCore.QT_TRANSLATE_NOOP( + "App::Property", "Controls how tool retracts Default=G99" + ), + ) + obj.addProperty( + "App::PropertyDistance", + "RetractHeight", + "Drill", + QtCore.QT_TRANSLATE_NOOP( + "App::Property", + "The height where feed starts and height during retract tool when path is finished while in a peck operation", + ), + ) + obj.addProperty( + "App::PropertyEnumeration", + "ExtraOffset", + "Drill", + QtCore.QT_TRANSLATE_NOOP( + "App::Property", "How far the drill depth is extended" + ), + ) - obj.ReturnLevel = ['G99', 'G98'] # Canned Cycle Return Level - obj.ExtraOffset = ['None', 'Drill Tip', '2x Drill Tip'] # Canned Cycle Return Level + obj.ReturnLevel = ["G99", "G98"] # Canned Cycle Return Level + obj.ExtraOffset = [ + "None", + "Drill Tip", + "2x Drill Tip", + ] # Canned Cycle Return Level def circularHoleExecute(self, obj, holes): - '''circularHoleExecute(obj, holes) ... generate drill operation for each hole in holes.''' + """circularHoleExecute(obj, holes) ... generate drill operation for each hole in holes.""" PathLog.track() + machine = PathMachineState.MachineState() self.commandlist.append(Path.Command("(Begin Drilling)")) # rapid to clearance height - self.commandlist.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid})) + command = Path.Command( + "G0", {"Z": obj.ClearanceHeight.Value, "F": self.vertRapid} + ) + machine.addCommand(command) + self.commandlist.append(command) - tiplength = 0.0 - if obj.ExtraOffset == 'Drill Tip': - tiplength = PathUtils.drillTipLength(self.tool) - elif obj.ExtraOffset == '2x Drill Tip': - tiplength = PathUtils.drillTipLength(self.tool) * 2 + self.commandlist.append(Path.Command("G90")) # Absolute distance mode - holes = PathUtils.sort_jobs(holes, ['x', 'y']) - self.commandlist.append(Path.Command('G90')) + # Calculate offsets to add to target edge + endoffset = 0.0 + if obj.ExtraOffset == "Drill Tip": + endoffset = PathUtils.drillTipLength(self.tool) + elif obj.ExtraOffset == "2x Drill Tip": + endoffset = PathUtils.drillTipLength(self.tool) * 2 + + # http://linuxcnc.org/docs/html/gcode/g-code.html#gcode:g98-g99 self.commandlist.append(Path.Command(obj.ReturnLevel)) - cmd = "G81" - cmdParams = {} - cmdParams['Z'] = obj.FinalDepth.Value - tiplength - cmdParams['F'] = self.vertFeed - cmdParams['R'] = obj.RetractHeight.Value + holes = PathUtils.sort_jobs(holes, ["x", "y"]) - if obj.PeckEnabled and obj.PeckDepth.Value > 0: - cmd = "G83" - cmdParams['Q'] = obj.PeckDepth.Value - elif obj.DwellEnabled and obj.DwellTime > 0: - cmd = "G82" - cmdParams['P'] = obj.DwellTime + # This section is technical debt. The computation of the + # target shapes should be factored out for re-use. + # This will likely mean refactoring upstream CircularHoleBase to pass + # spotshapes instead of holes. - # parentJob = PathUtils.findParentJob(obj) - # startHeight = obj.StartDepth.Value + parentJob.SetupSheet.SafeHeightOffset.Value startHeight = obj.StartDepth.Value + self.job.SetupSheet.SafeHeightOffset.Value - for p in holes: - params = {} - params['X'] = p['x'] - params['Y'] = p['y'] + edgelist = [] + for hole in holes: + v1 = FreeCAD.Vector(hole["x"], hole["y"], obj.StartDepth.Value) + v2 = FreeCAD.Vector(hole["x"], hole["y"], obj.FinalDepth.Value - endoffset) + edgelist.append(Part.makeLine(v1, v2)) + + # iterate the edgelist and generate gcode + for edge in edgelist: + + PathLog.debug(edge) # move to hole location - self.commandlist.append(Path.Command('G0', {'X': p['x'], 'Y': p['y'], 'F': self.horizRapid})) - self.commandlist.append(Path.Command('G0', {'Z': startHeight, 'F': self.vertRapid})) - self.commandlist.append(Path.Command('G1', {'Z': obj.StartDepth.Value, 'F': self.vertFeed})) - # Update changes to parameters - params.update(cmdParams) + command = Path.Command( + "G0", {"X": hole["x"], "Y": hole["y"], "F": self.horizRapid} + ) + self.commandlist.append(command) + machine.addCommand(command) - # Perform canned drilling cycle - self.commandlist.append(Path.Command(cmd, params)) + command = Path.Command("G0", {"Z": startHeight, "F": self.vertRapid}) + self.commandlist.append(command) + machine.addCommand(command) - # Cancel canned drilling cycle - self.commandlist.append(Path.Command('G80')) - self.commandlist.append(Path.Command('G0', {'Z': obj.SafeHeight.Value})) + command = Path.Command( + "G1", {"Z": obj.StartDepth.Value, "F": self.vertFeed} + ) + self.commandlist.append(command) + machine.addCommand(command) + + # Technical Debt: We are assuming the edges are aligned. + # This assumption should be corrected and the necessary rotations + # performed to align the edge with the Z axis for drilling + + # Perform drilling + dwelltime = obj.DwellTime if obj.DwellEnabled else 0.0 + peckdepth = obj.PeckDepth.Value if obj.PeckEnabled else 0.0 + repeat = 1 # technical debt: Add a repeat property for user control + + try: + drillcommands = generator.generate(edge, dwelltime, peckdepth, repeat) + + except ValueError as e: # any targets that fail the generator are ignored + PathLog.info(e) + continue + + self.commandlist.extend(drillcommands) + + # Cancel canned drilling cycle + self.commandlist.append(Path.Command("G80")) + command = Path.Command("G0", {"Z": obj.SafeHeight.Value}) + self.commandlist.append(command) + machine.addCommand(command) + + # Apply feedrates to commands + PathFeedRate.setFeedRate(self.commandlist, obj.ToolController) def opSetDefaultValues(self, obj, job): - '''opSetDefaultValues(obj, job) ... set default value for RetractHeight''' + """opSetDefaultValues(obj, job) ... set default value for RetractHeight""" obj.ExtraOffset = "None" - if hasattr(job.SetupSheet, 'RetractHeight'): + if hasattr(job.SetupSheet, "RetractHeight"): obj.RetractHeight = job.SetupSheet.RetractHeight - elif self.applyExpression(obj, 'RetractHeight', 'StartDepth+SetupSheet.SafeHeightOffset'): + elif self.applyExpression( + obj, "RetractHeight", "StartDepth+SetupSheet.SafeHeightOffset" + ): if not job: obj.RetractHeight = 10 else: obj.RetractHeight.Value = obj.StartDepth.Value + 1.0 - if hasattr(job.SetupSheet, 'PeckDepth'): + if hasattr(job.SetupSheet, "PeckDepth"): obj.PeckDepth = job.SetupSheet.PeckDepth - elif self.applyExpression(obj, 'PeckDepth', 'OpToolDiameter*0.75'): + elif self.applyExpression(obj, "PeckDepth", "OpToolDiameter*0.75"): obj.PeckDepth = 1 - if hasattr(job.SetupSheet, 'DwellTime'): + if hasattr(job.SetupSheet, "DwellTime"): obj.DwellTime = job.SetupSheet.DwellTime else: obj.DwellTime = 1 + def SetupProperties(): setup = [] setup.append("PeckDepth") @@ -161,7 +264,7 @@ def SetupProperties(): def Create(name, obj=None, parentJob=None): - '''Create(name) ... Creates and returns a Drilling operation.''' + """Create(name) ... Creates and returns a Drilling operation.""" if obj is None: obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name) @@ -169,4 +272,4 @@ def Create(name, obj=None, parentJob=None): if obj.Proxy: obj.Proxy.findAllHoles(obj) - return obj \ No newline at end of file + return obj