Refactor PathDrilling to user generator
Uses the drill generator. centralizes feed rate assignment Tracks current machine position with MachineState
This commit is contained in:
95
src/Mod/Path/PathFeedRate.py
Normal file
95
src/Mod/Path/PathFeedRate.py
Normal 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
|
||||
105
src/Mod/Path/PathMachineState.py
Normal file
105
src/Mod/Path/PathMachineState.py
Normal 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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user