Refactor PathDrilling to user generator

Uses the drill generator.
centralizes feed rate assignment
Tracks current machine position with MachineState
This commit is contained in:
sliptonic
2021-11-08 18:54:19 -06:00
parent cada185a70
commit 5b23af3d4b
3 changed files with 365 additions and 62 deletions

View File

@@ -0,0 +1,95 @@
# -*- 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 *
# * *
# ***************************************************************************
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

View File

@@ -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)

View File

@@ -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
return obj