First shot at external thread milling

This commit is contained in:
Markus Lampert
2022-02-15 23:03:25 -08:00
committed by mlampert
parent ca3c8185e0
commit 7c2a8a92fb
6 changed files with 553 additions and 110 deletions

View File

@@ -36,7 +36,10 @@ __author__ = "sliptonic (Brad Collette)"
__url__ = "http://www.freecadweb.org"
__doc__ = "Path thread milling operation."
if False:
# math.sqrt(3)/2 ... 60deg triangle height
SQRT_3_DIVIDED_BY_2 = 0.8660254037844386
if True:
PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
PathLog.trackModule(PathLog.thisModule())
else:
@@ -45,8 +48,8 @@ else:
translate = FreeCAD.Qt.translate
def radiiInternal(majorDia, minorDia, toolDia, toolCrest=None):
"""internlThreadRadius(majorDia, minorDia, toolDia, toolCrest) ... returns the maximum radius for thread."""
def threadRadii(internal, majorDia, minorDia, toolDia, toolCrest):
"""threadRadii(majorDia, minorDia, toolDia, toolCrest) ... returns the minimum and maximum radius for thread."""
PathLog.track(majorDia, minorDia, toolDia, toolCrest)
if toolCrest is None:
toolCrest = 0.0
@@ -57,22 +60,28 @@ def radiiInternal(majorDia, minorDia, toolDia, toolCrest=None):
# Since we already have the outer diameter it's simpler to just add 1/8 * H
# to get the outer tip of the thread.
H = ((majorDia - minorDia) / 2.0) * 1.6 # (D - d)/2 = 5/8 * H
outerTip = majorDia / 2.0 + H / 8.0
if internal:
# mill inside out
outerTip = majorDia / 2.0 + H / 8.0
# Compensate for the crest of the tool
toolTip = outerTip - toolCrest * SQRT_3_DIVIDED_BY_2
return ((minorDia - toolDia) / 2.0, toolTip - toolDia / 2.0)
# mill outside in
innerTip = minorDia / 2.0 - H / 4.0
# Compensate for the crest of the tool
toolTip = (
outerTip - toolCrest * 0.8660254037844386
) # math.sqrt(3)/2 ... 60deg triangle height
return ((minorDia - toolDia) / 2.0, toolTip - toolDia / 2.0)
toolTip = innerTip - toolCrest * SQRT_3_DIVIDED_BY_2
return ((majorDia + toolDia) / 2.0, toolTip + toolDia / 2.0)
def threadPasses(count, radii, majorDia, minorDia, toolDia, toolCrest=None):
PathLog.track(count, radii, majorDia, minorDia, toolDia, toolCrest)
minor, major = radii(majorDia, minorDia, toolDia, toolCrest)
def threadPasses(count, radii, internal, majorDia, minorDia, toolDia, toolCrest):
PathLog.track(count, radii, internal, majorDia, minorDia, toolDia, toolCrest)
minor, major = radii(internal, majorDia, minorDia, toolDia, toolCrest)
dr = float(major - minor) / count
return [major - dr * (count - (i + 1)) for i in range(count)]
if internal:
return [minor + dr * (i + 1) for i in range(count)]
return [major - dr * (i + 1) for i in range(count)]
class _InternalThread(object):
class _ThreadInternal(object):
"""Helper class for dealing with different thread types"""
def __init__(self, cmd, zStart, zFinal, pitch):
@@ -112,9 +121,9 @@ class _InternalThread(object):
return self.pitch > 0
def internalThreadCommands(loc, cmd, zStart, zFinal, pitch, radius, leadInOut):
"""internalThreadCommands(loc, cmd, zStart, zFinal, pitch, radius) ... returns the g-code to mill the given internal thread"""
thread = _InternalThread(cmd, zStart, zFinal, pitch)
def threadCommandsInternal(loc, cmd, zStart, zFinal, pitch, radius, leadInOut):
"""threadCommandsInternal(loc, cmd, zStart, zFinal, pitch, radius) ... returns the g-code to mill the given internal thread"""
thread = _ThreadInternal(cmd, zStart, zFinal, pitch)
yMin = loc.y - radius
yMax = loc.y + radius
@@ -171,23 +180,92 @@ def internalThreadCommands(loc, cmd, zStart, zFinal, pitch, radius, leadInOut):
return path
def threadCommandsExternal(loc, cmd, zStart, zFinal, pitch, radius, leadInOut):
"""threadCommandsExternal(loc, cmd, zStart, zFinal, pitch, radius) ... returns the g-code to mill the given internal thread"""
thread = _ThreadInternal(cmd, zStart, zFinal, pitch)
yMin = loc.y - radius
yMax = loc.y + radius
path = []
# at this point the tool is at a safe height (depending on the previous thread), so we can move
# into position first, and then drop to the start height. If there is any material in the way this
# op hasn't been setup properly.
path.append(Path.Command("G0", {"X": loc.x, "Y": loc.y}))
path.append(Path.Command("G0", {"Z": thread.zStart}))
if leadInOut:
path.append(Path.Command(thread.cmd, {"Y": yMax, "J": (yMax - loc.y) / 2}))
else:
path.append(Path.Command("G1", {"Y": yMax}))
z = thread.zStart
r = -radius
i = 0
while True:
z = thread.zStart + i * thread.hPitch
if thread.overshoots(z):
break
if 0 == (i & 0x01):
y = yMin
else:
y = yMax
path.append(Path.Command(thread.cmd, {"Y": y, "Z": z + thread.hPitch, "J": r}))
r = -r
i = i + 1
z = thread.zStart + i * thread.hPitch
if PathGeom.isRoughly(z, thread.zFinal):
x = loc.x
else:
n = math.fabs(thread.zFinal - thread.zStart) / thread.hPitch
k = n - int(n)
dy = math.cos(k * math.pi)
dx = math.sin(k * math.pi)
y = thread.adjustY(loc.y, r * dy)
x = thread.adjustX(loc.x, r * dx)
path.append(
Path.Command(thread.cmd, {"X": x, "Y": y, "Z": thread.zFinal, "J": r})
)
if leadInOut:
path.append(
Path.Command(
thread.cmd,
{"X": loc.x, "Y": loc.y, "I": (loc.x - x) / 2, "J": (loc.y - y) / 2},
)
)
else:
path.append(Path.Command("G1", {"X": loc.x, "Y": loc.y}))
return path
class ObjectThreadMilling(PathCircularHoleBase.ObjectOp):
"""Proxy object for thread milling operation."""
LeftHand = "LeftHand"
RightHand = "RightHand"
ThreadTypeCustom = "Custom"
ThreadTypeMetricInternal = "MetricInternal"
ThreadTypeCustomExternal = "CustomExternal"
ThreadTypeCustomInternal = "CustomInternal"
ThreadTypeImperialExternal = "ImperialExternal"
ThreadTypeImperialInternal = "ImperialInternal"
ThreadTypeMetricExternal = "MetricExternal"
ThreadTypeMetricInternal = "MetricInternal"
DirectionClimb = "Climb"
DirectionConventional = "Conventional"
ThreadOrientations = [LeftHand, RightHand]
ThreadTypes = [
ThreadTypeCustom,
ThreadTypeMetricInternal,
ThreadTypesInternal = [
ThreadTypeCustomInternal,
ThreadTypeImperialInternal,
]
ThreadTypeMetricInternal,
]
ThreadTypesExternal = [
ThreadTypeCustomExternal,
ThreadTypeImperialExternal,
ThreadTypeMetricExternal,
]
ThreadTypes = ThreadTypesInternal + ThreadTypesExternal
Directions = [DirectionClimb, DirectionConventional]
@classmethod
@@ -200,25 +278,26 @@ class ObjectThreadMilling(PathCircularHoleBase.ObjectOp):
'raw' is list of (translated_text, data_string) tuples
'translated' is list of translated string literals
"""
PathLog.track()
# Enumeration lists for App::PropertyEnumeration properties
enums = {
"ThreadType": [
(translate("Path_ThreadMilling", "Custom"), "Custom"),
(translate("Path_ThreadMilling", "Metric Internal"), "MetricInternal"),
(
translate("Path_ThreadMilling", "Imperial Internal"),
"ImperialInternal",
),
], # this is the direction that the profile runs
(translate("Path_ThreadMilling", "Custom External"), ObjectThreadMilling.ThreadTypeCustomExternal),
(translate("Path_ThreadMilling", "Custom Internal"), ObjectThreadMilling.ThreadTypeCustomInternal),
(translate("Path_ThreadMilling", "Imperial Internal"), ObjectThreadMilling.ThreadTypeImperialInternal),
(translate("Path_ThreadMilling", "Imperial External"), ObjectThreadMilling.ThreadTypeImperialExternal),
(translate("Path_ThreadMilling", "Metric External"), ObjectThreadMilling.ThreadTypeMetricExternal),
(translate("Path_ThreadMilling", "Metric Internal"), ObjectThreadMilling.ThreadTypeMetricInternal),
],
"ThreadOrientation": [
(translate("Path_ThreadMilling", "LeftHand"), "LeftHand"),
(translate("Path_ThreadMilling", "RightHand"), "RightHand"),
], # side of profile that cutter is on in relation to direction of profile
(translate("Path_ThreadMilling", "LeftHand"), ObjectThreadMilling.LeftHand),
(translate("Path_ThreadMilling", "RightHand"), ObjectThreadMilling.RightHand),
],
"Direction": [
(translate("Path_ThreadMilling", "Climb"), "Climb"),
(translate("Path_ThreadMilling", "Conventional"), "Conventional"),
], # side of profile that cutter is on in relation to direction of profile
(translate("Path_ThreadMilling", "Climb"), ObjectThreadMilling.DirectionClimb),
(translate("Path_ThreadMilling", "Conventional"), ObjectThreadMilling.DirectionConventional),
],
}
if dataType == "raw":
@@ -236,9 +315,11 @@ class ObjectThreadMilling(PathCircularHoleBase.ObjectOp):
return data
def circularHoleFeatures(self, obj):
PathLog.track()
return PathOp.FeatureBaseGeometry
def initCircularHoleOperation(self, obj):
PathLog.track()
obj.addProperty(
"App::PropertyEnumeration",
"ThreadOrientation",
@@ -333,48 +414,11 @@ class ObjectThreadMilling(PathCircularHoleBase.ObjectOp):
for n in self.propertyEnumerations():
setattr(obj, n[0], n[1])
def threadStartDepth(self, obj):
if obj.ThreadOrientation == self.RightHand:
if obj.Direction == self.DirectionClimb:
PathLog.track(obj.Label, obj.FinalDepth)
return obj.FinalDepth
PathLog.track(obj.Label, obj.StartDepth)
return obj.StartDepth
if obj.Direction == self.DirectionClimb:
PathLog.track(obj.Label, obj.StartDepth)
return obj.StartDepth
PathLog.track(obj.Label, obj.FinalDepth)
return obj.FinalDepth
def _isThreadInternal(self, obj):
return obj.ThreadType in self.ThreadTypesInternal
def threadFinalDepth(self, obj):
PathLog.track(obj.Label)
if obj.ThreadOrientation == self.RightHand:
if obj.Direction == self.DirectionClimb:
PathLog.track(obj.Label, obj.StartDepth)
return obj.StartDepth
PathLog.track(obj.Label, obj.FinalDepth)
return obj.FinalDepth
if obj.Direction == self.DirectionClimb:
PathLog.track(obj.Label, obj.FinalDepth)
return obj.FinalDepth
PathLog.track(obj.Label, obj.StartDepth)
return obj.StartDepth
def threadDirectionCmd(self, obj):
PathLog.track(obj.Label)
if obj.ThreadOrientation == self.RightHand:
if obj.Direction == self.DirectionClimb:
PathLog.track(obj.Label, "G2")
return "G2"
PathLog.track(obj.Label, "G3")
return "G3"
if obj.Direction == self.DirectionClimb:
PathLog.track(obj.Label, "G3")
return "G3"
PathLog.track(obj.Label, "G2")
return "G2"
def threadSetup(self, obj):
def _threadSetupInternal(self, obj):
PathLog.track()
# the thing to remember is that Climb, for an internal thread must always be G3
if obj.Direction == self.DirectionClimb:
if obj.ThreadOrientation == self.RightHand:
@@ -384,6 +428,18 @@ class ObjectThreadMilling(PathCircularHoleBase.ObjectOp):
return ("G2", obj.StartDepth.Value, obj.FinalDepth.Value)
return ("G2", obj.FinalDepth.Value, obj.StartDepth.Value)
def threadSetup(self, obj):
PathLog.track()
cmd, zbegin, zend = self._threadSetupInternal(obj)
if obj.ThreadType in self.ThreadTypesInternal:
return (cmd, zbegin, zend)
# need to reverse direction for external threads
if cmd == 'G2':
return ('G3', zbegin, zend)
return ('G2', zbegin, zend)
def threadPassRadii(self, obj):
PathLog.track(obj.Label)
rMajor = (obj.MajorDiameter.Value - self.tool.Diameter) / 2.0
@@ -405,15 +461,15 @@ class ObjectThreadMilling(PathCircularHoleBase.ObjectOp):
for radius in threadPasses(
obj.Passes,
radiiInternal,
threadRadii,
self._isThreadInternal(obj),
obj.MajorDiameter.Value,
obj.MinorDiameter.Value,
float(self.tool.Diameter),
float(self.tool.Crest),
):
commands = internalThreadCommands(
loc, gcode, zStart, zFinal, pitch, radius, obj.LeadInOut
)
commands = threadCommandsInternal(loc, gcode, zStart, zFinal, pitch, radius, obj.LeadInOut)
for cmd in commands:
p = cmd.Parameters
if cmd.Name in ["G0"]:
@@ -454,6 +510,7 @@ class ObjectThreadMilling(PathCircularHoleBase.ObjectOp):
PathLog.error("No suitable Tool found for thread milling operation")
def opSetDefaultValues(self, obj, job):
PathLog.track()
obj.ThreadOrientation = self.RightHand
obj.ThreadType = self.ThreadTypeMetricInternal
obj.ThreadFit = 50
@@ -465,7 +522,9 @@ class ObjectThreadMilling(PathCircularHoleBase.ObjectOp):
def isToolSupported(self, obj, tool):
"""Thread milling only supports thread milling cutters."""
return hasattr(tool, "Diameter") and hasattr(tool, "Crest")
support = hasattr(tool, "Diameter") and hasattr(tool, "Crest")
PathLog.track(tool.Name, support)
return support
def SetupProperties():

View File

@@ -40,7 +40,7 @@ __author__ = "sliptonic (Brad Collette)"
__url__ = "http://www.freecadweb.org"
__doc__ = "UI and Command for Path Thread Milling Operation."
if False:
if True:
PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
PathLog.trackModule(PathLog.thisModule())
else:
@@ -49,17 +49,23 @@ else:
translate = FreeCAD.Qt.translate
def fillThreads(combo, dataFile):
combo.blockSignals(True)
combo.clear()
def fillThreads(form, dataFile, defaultSelect):
form.threadName.blockSignals(True)
select = form.threadName.currentText()
PathLog.debug("select = '{}'".format(select))
form.threadName.clear()
with open(
"{}Mod/Path/Data/Threads/{}.csv".format(FreeCAD.getHomePath(), dataFile)
) as fp:
reader = csv.DictReader(fp)
for row in reader:
combo.addItem(row["name"], row)
combo.setEnabled(True)
combo.blockSignals(False)
form.threadName.addItem(row['name'], row)
if select:
form.threadName.setCurrentText(select)
elif defaultSelect:
form.threadName.setCurrentText(defaultSelect)
form.threadName.setEnabled(True)
form.threadName.blockSignals(False)
class TaskPanelOpPage(PathCircularHoleBaseGui.TaskPanelOpPage):
@@ -134,25 +140,35 @@ class TaskPanelOpPage(PathCircularHoleBaseGui.TaskPanelOpPage):
self.pitch.updateSpinBox()
self.setupToolController(obj, self.form.toolController)
self._updateFromThreadType()
def _isThreadMetric(self):
def _isThreadCustom(self):
return (
self.form.threadType.currentData()
== PathThreadMilling.ObjectThreadMilling.ThreadTypeMetricInternal
in [PathThreadMilling.ObjectThreadMilling.ThreadTypeCustomInternal, PathThreadMilling.ObjectThreadMilling.ThreadTypeCustomExternal]
)
def _isThreadImperial(self):
return (
self.form.threadType.currentData()
== PathThreadMilling.ObjectThreadMilling.ThreadTypeImperialInternal
in [PathThreadMilling.ObjectThreadMilling.ThreadTypeImperialInternal, PathThreadMilling.ObjectThreadMilling.ThreadTypeImperialExternal]
)
def _isThreadMetric(self):
return (
self.form.threadType.currentData()
in [PathThreadMilling.ObjectThreadMilling.ThreadTypeMetricInternal, PathThreadMilling.ObjectThreadMilling.ThreadTypeMetricExternal]
)
def _isThreadInternal(self):
return self.form.threadType.currentData() in PathThreadMilling.ObjectThreadMilling.ThreadTypesInternal
def _isThreadExternal(self):
return self.form.threadType.currentData() in PathThreadMilling.ObjectThreadMilling.ThreadTypesExternal
def _updateFromThreadType(self):
if (
self.form.threadType.currentData()
== PathThreadMilling.ObjectThreadMilling.ThreadTypeCustom
):
if self._isThreadCustom():
self.form.threadName.setEnabled(False)
self.form.threadFit.setEnabled(False)
self.form.threadFitLabel.setEnabled(False)
@@ -169,7 +185,10 @@ class TaskPanelOpPage(PathCircularHoleBaseGui.TaskPanelOpPage):
self.form.threadTPI.setEnabled(False)
self.form.threadTPILabel.setEnabled(False)
self.form.threadTPI.setValue(0)
fillThreads(self.form.threadName, "metric-internal")
if self._isThreadInternal():
fillThreads(self.form, "metric-internal", self.obj.ThreadName)
else:
fillThreads(self.form, "metric-external", self.obj.ThreadName)
if self._isThreadImperial():
self.form.threadFit.setEnabled(True)
@@ -179,7 +198,7 @@ class TaskPanelOpPage(PathCircularHoleBaseGui.TaskPanelOpPage):
self.form.threadTPI.setEnabled(True)
self.form.threadTPILabel.setEnabled(True)
self.pitch.updateSpinBox(0)
fillThreads(self.form.threadName, "imperial-internal")
fillThreads(self.form, "imperial-internal", self.obj.ThreadName)
def _updateFromThreadName(self):
thread = self.form.threadName.currentData()