Added actual thread milling path generation.

This commit is contained in:
Markus Lampert
2019-08-14 23:29:18 -07:00
parent 0a96a861c3
commit 15141e33dc
3 changed files with 180 additions and 40 deletions

View File

@@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>482</width>
<height>736</height>
<height>756</height>
</rect>
</property>
<property name="windowTitle">
@@ -167,7 +167,7 @@
<bool>false</bool>
</property>
<property name="text">
<string>Flat</string>
<string>Crest</string>
</property>
</widget>
</item>
@@ -231,6 +231,13 @@
</item>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="leadInOut">
<property name="text">
<string>Lead In/Out</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>

View File

@@ -27,6 +27,7 @@ from __future__ import print_function
import FreeCAD
import Path
import PathScripts.PathCircularHoleBase as PathCircularHoleBase
import PathScripts.PathGeom as PathGeom
import PathScripts.PathLog as PathLog
import PathScripts.PathOp as PathOp
import PathScripts.PathUtils as PathUtils
@@ -48,6 +49,7 @@ def translate(context, text, disambig=None):
def radiiMetricInternal(majorDia, minorDia, toolDia, toolCrest = None):
'''internlThreadRadius(majorDia, minorDia, toolDia, toolCrest) ... returns the maximum radius for thread.'''
PathLog.track(majorDia, minorDia, toolDia, toolCrest)
if toolCrest is None:
toolCrest = 0.0
# see https://www.amesweb.info/Screws/Internal-Metric-Thread-Dimensions-Chart.aspx
@@ -57,22 +59,114 @@ def radiiMetricInternal(majorDia, minorDia, toolDia, toolCrest = None):
return ((minorDia - toolDia) / 2, toolTip - toolDia / 2)
def threadPasses(count, radii, majorDia, minorDia, toolDia, toolCrest = None):
PathLog.track(count, radii, majorDia, minorDia, toolDia, toolCrest)
minor, major = radii(majorDia, minorDia, toolDia, toolCrest)
dr = (major - minor) / count
return [major - dr * (count - (i + 1)) for i in range(count)]
class _InternalThread(object):
'''Helper class for dealing with different thread types'''
def __init__(self, cmd, zStart, zFinal, pitch):
self.cmd = cmd
if zStart < zFinal:
self.pitch = pitch
else:
self.pitch = -pitch
self.hPitch = self.pitch / 2
self.zStart = zStart
self.zFinal = zFinal
def overshoots(self, z):
'''overshoots(z) ... returns true if adding another half helix goes beyond the thread bounds'''
if self.pitch < 0:
return z + self.hPitch < self.zFinal
return z + self.hPitch > self.zFinal
def adjustX(self, x, dx):
'''adjustX(x, dx) ... move x by dx, the direction depends on the thread settings'''
if self.isG3() == (self.pitch > 0):
return x + dx
return x - dx
def adjustY(self, y, dy):
'''adjustY(y, dy) ... move y by dy, the direction depends on the thread settings'''
if self.isG3():
return y - dy
return y - dy
def isG3(self):
'''isG3() ... returns True if this is a G3 command'''
return self.cmd in ['G3', 'G03', 'g3', 'g03']
def isUp(self):
'''isUp() ... returns True if the thread goes from the bottom up'''
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)
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 = 'Metric - internal'
DirectionClimb = 'Climb'
LeftHand = 'LeftHand'
RightHand = 'RightHand'
ThreadTypeCustom = 'Custom'
ThreadTypeMetricInternal = 'Metric - internal'
DirectionClimb = 'Climb'
DirectionConventional = 'Conventional'
ThreadOrientations = [LeftHand, RightHand]
ThreadTypes = [ThreadTypeCustom, ThreadTypeMetricInternal]
Directions = [DirectionClimb]
Directions = [DirectionClimb, DirectionConventional]
def circularHoleFeatures(self, obj):
return PathOp.FeatureBaseGeometry
@@ -87,38 +181,64 @@ class ObjectThreadMilling(PathCircularHoleBase.ObjectOp):
obj.addProperty("App::PropertyLength", "MinorDiameter", "Thread", QtCore.QT_TRANSLATE_NOOP("App::Property", "Set thread's minor diameter"))
obj.addProperty("App::PropertyLength", "Pitch", "Thread", QtCore.QT_TRANSLATE_NOOP("App::Property", "Set thread's pitch"))
obj.addProperty("App::PropertyInteger", "ThreadFit", "Thread", QtCore.QT_TRANSLATE_NOOP("App::Property", "Set how many passes are used to cut the thread"))
obj.addProperty("App::PropertyInteger", "Passes", "Mill", QtCore.QT_TRANSLATE_NOOP("App::Property", "Set how many passes are used to cut the thread"))
obj.addProperty("App::PropertyEnumeration", "Direction", "Mill", QtCore.QT_TRANSLATE_NOOP("App::Property", "Direction of thread cutting operation"))
obj.addProperty("App::PropertyInteger", "Passes", "Operation", QtCore.QT_TRANSLATE_NOOP("App::Property", "Set how many passes are used to cut the thread"))
obj.addProperty("App::PropertyEnumeration", "Direction", "Operation", QtCore.QT_TRANSLATE_NOOP("App::Property", "Direction of thread cutting operation"))
obj.addProperty("App::PropertyBool", "LeadInOut", "Operation", QtCore.QT_TRANSLATE_NOOP("App::Property", "Set to True to get lead in and lead out arcs at the start and end of the thread cut"))
obj.Direction = self.Directions
def threadStartDepth(self, obj):
if self.ThreadDirection == self.RightHand:
if self.Direction == self.DirectionClimb:
return self.FinalDepth
return self.StartDepth
if self.Direction == self.DirectionClimb:
return self.StartDepth
return self.FinalDepth
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 threadFinalDepth(self, obj):
if self.ThreadDirection == self.RightHand:
if self.Direction == self.DirectionClimb:
return self.StartDepth
return self.FinalDepth
if self.Direction == self.DirectionClimb:
return self.FinalDepth
return self.StartDepth
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):
if self.ThreadDirection == self.RightHand:
if self.Direction == self.DirectionClimb:
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 self.Direction == self.DirectionClimb:
if obj.Direction == self.DirectionClimb:
PathLog.track(obj.Label, 'G3')
return 'G3'
PathLog.track(obj.Label, 'G2')
return 'G2'
def threadSetup(self, obj):
# 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:
return ('G3', obj.FinalDepth.Value, obj.StartDepth.Value)
return ('G3', obj.StartDepth.Value, obj.FinalDepth.Value)
if obj.ThreadOrientation == self.RightHand:
return ('G2', obj.StartDepth.Value, obj.FinalDepth.Value)
return ('G2', obj.FinalDepth.Value, obj.StartDepth.Value)
def threadPassRadii(self, obj):
PathLog.track(obj.Label)
rMajor = (obj.MajorDiameter.Value - self.tool.Diameter) / 2.0
rMinor = (obj.MinorDiameter.Value - self.tool.Diameter) / 2.0
if obj.Passes < 1:
@@ -129,23 +249,30 @@ class ObjectThreadMilling(PathCircularHoleBase.ObjectOp):
passes.append(rMajor - rPass * i)
return list(reversed(passes))
def executeThreadMill(self, obj, loc, cmd, zStart, zFinal, pitch):
hPitch = obj.Pitch.Value / 2.0
if zStart > zFinal:
hPitch = -hPitch
def executeThreadMill(self, obj, loc, gcode, zStart, zFinal, pitch):
PathLog.track(obj.Label, loc, gcode, zStart, zFinal, pitch)
self.commandlist.append(Path.Command('G0', {'Z': zStart, 'F': self.vertRapid}))
for r in threadPasses(obj.Passes, radiiMetricInternal, obj.MajorDiameter, obj.MinorDiameter, self.tool.Diameter, 0):
pass
self.commandlist.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid}))
for radius in threadPasses(obj.Passes, radiiMetricInternal, obj.MajorDiameter.Value, obj.MinorDiameter.Value, self.tool.Diameter, 0):
commands = internalThreadCommands(loc, gcode, zStart, zFinal, pitch, radius, obj.LeadInOut)
for cmd in commands:
p = cmd.Parameters
if cmd.Name in ['G0']:
p.update({'F': self.vertRapid})
if cmd.Name in ['G1', 'G2', 'G3']:
p.update({'F': self.horizFeed})
cmd.Parameters = p
self.commandlist.extend(commands)
self.commandlist.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid}))
def circularHoleExecute(self, obj, holes):
PathLog.track()
self.commandlist.append(Path.Command("(Begin Thread Milling)"))
cmd = self.threadDirectionCmd(obj)
zStart = self.threadStartDepth(obj).Value
zFinal = self.threadFinalDepth(obj).Value
(cmd, zStart, zFinal) = self.threadSetup(obj)
pitch = obj.Pitch.Value
if pitch <= 0:
PathLog.error("Cannot create thread with pitch {}".format(pitch))
@@ -153,9 +280,7 @@ class ObjectThreadMilling(PathCircularHoleBase.ObjectOp):
# rapid to clearance height
for loc in holes:
self.executeThreadMill(obj, loc, cmd, zStart, zFinal, pitch)
self.commandlist.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid}))
self.executeThreadMill(obj, FreeCAD.Vector(loc['x'], loc['y'], 0), cmd, zStart, zFinal, pitch)
def opSetDefaultValues(self, obj, job):
@@ -165,6 +290,7 @@ class ObjectThreadMilling(PathCircularHoleBase.ObjectOp):
obj.Pitch = 1
obj.Passes = 1
obj.Direction = self.DirectionClimb
obj.LeadInOut = True
def SetupProperties():
@@ -178,6 +304,7 @@ def SetupProperties():
setup.append("Pitch")
setup.append("Passes")
setup.append("Direction")
setup.append("LeadInOut")
return setup

View File

@@ -85,6 +85,7 @@ class TaskPanelOpPage(PathCircularHoleBaseGui.TaskPanelOpPage):
obj.ThreadName = self.form.threadName.currentText()
obj.Direction = self.form.opDirection.currentText()
obj.Passes = self.form.opPasses.value()
obj.LeadInOut = self.form.leadInOut.checkState() == QtCore.Qt.Checked
self.updateToolController(obj, self.form.toolController)
@@ -104,6 +105,7 @@ class TaskPanelOpPage(PathCircularHoleBaseGui.TaskPanelOpPage):
self.form.opPasses.setValue(obj.Passes)
self.form.opDirection.setCurrentText(obj.Direction)
self.form.leadInOut.setCheckState(QtCore.Qt.Checked if obj.LeadInOut else QtCore.Qt.Unchecked)
self.majorDia.updateSpinBox()
self.minorDia.updateSpinBox()
@@ -146,6 +148,10 @@ class TaskPanelOpPage(PathCircularHoleBaseGui.TaskPanelOpPage):
signals.append(self.form.threadMajor.editingFinished)
signals.append(self.form.threadMinor.editingFinished)
signals.append(self.form.threadPitch.editingFinished)
signals.append(self.form.threadOrientation.currentIndexChanged)
signals.append(self.form.opDirection.currentIndexChanged)
signals.append(self.form.opPasses.editingFinished)
signals.append(self.form.leadInOut.stateChanged)
signals.append(self.form.toolController.currentIndexChanged)