345 lines
15 KiB
Python
345 lines
15 KiB
Python
# -*- coding: utf-8 -*-
|
|
# ***************************************************************************
|
|
# * *
|
|
# * Copyright (c) 2019 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 *
|
|
# * *
|
|
# ***************************************************************************
|
|
|
|
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
|
|
import math
|
|
|
|
from PySide import QtCore
|
|
|
|
__title__ = "Path Thread Milling Operation"
|
|
__author__ = "sliptonic (Brad Collette)"
|
|
__url__ = "http://www.freecadweb.org"
|
|
__doc__ = "Path thread milling operation."
|
|
|
|
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
|
|
#PathLog.trackModule(PathLog.thisModule())
|
|
|
|
# Qt translation handling
|
|
def translate(context, text, disambig=None):
|
|
return QtCore.QCoreApplication.translate(context, text, disambig)
|
|
|
|
|
|
def radiiInternal(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
|
|
# As it turns out metric and imperial standard threads follow the same rules.
|
|
# The determining factor is the height of the full 60 degree triangle H.
|
|
# - The minor diameter is 1/4 * H smaller than the pitch diameter.
|
|
# - The major diameter is 3/8 * H bigger than the pitch diameter
|
|
# 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
|
|
# 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)
|
|
|
|
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 = float(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'
|
|
ThreadTypeImperialInternal = 'Imperial - internal'
|
|
DirectionClimb = 'Climb'
|
|
DirectionConventional = 'Conventional'
|
|
|
|
ThreadOrientations = [LeftHand, RightHand]
|
|
ThreadTypes = [ThreadTypeCustom, ThreadTypeMetricInternal, ThreadTypeImperialInternal]
|
|
Directions = [DirectionClimb, DirectionConventional]
|
|
|
|
def circularHoleFeatures(self, obj):
|
|
return PathOp.FeatureBaseGeometry
|
|
|
|
def initCircularHoleOperation(self, obj):
|
|
obj.addProperty("App::PropertyEnumeration", "ThreadOrientation", "Thread", QtCore.QT_TRANSLATE_NOOP("PathThreadMilling", "Set thread orientation"))
|
|
obj.ThreadOrientation = self.ThreadOrientations
|
|
obj.addProperty("App::PropertyEnumeration", "ThreadType", "Thread", QtCore.QT_TRANSLATE_NOOP("PathThreadMilling", "Currently only internal"))
|
|
obj.ThreadType = self.ThreadTypes
|
|
obj.addProperty("App::PropertyString", "ThreadName", "Thread", QtCore.QT_TRANSLATE_NOOP("PathThreadMilling", "Devfines which standard thread was chosen"))
|
|
obj.addProperty("App::PropertyLength", "MajorDiameter", "Thread", QtCore.QT_TRANSLATE_NOOP("PathThreadMilling", "Set thread's major diameter"))
|
|
obj.addProperty("App::PropertyLength", "MinorDiameter", "Thread", QtCore.QT_TRANSLATE_NOOP("PathThreadMilling", "Set thread's minor diameter"))
|
|
obj.addProperty("App::PropertyLength", "Pitch", "Thread", QtCore.QT_TRANSLATE_NOOP("PathThreadMilling", "Set thread's pitch - used for metric threads"))
|
|
obj.addProperty("App::PropertyInteger", "TPI", "Thread", QtCore.QT_TRANSLATE_NOOP("PathThreadMilling", "Set thread's tpi - used for imperial threads"))
|
|
obj.addProperty("App::PropertyInteger", "ThreadFit", "Thread", QtCore.QT_TRANSLATE_NOOP("PathThreadMilling", "Set how many passes are used to cut the thread"))
|
|
obj.addProperty("App::PropertyInteger", "Passes", "Operation", QtCore.QT_TRANSLATE_NOOP("PathThreadMilling", "Set how many passes are used to cut the thread"))
|
|
obj.addProperty("App::PropertyEnumeration", "Direction", "Operation", QtCore.QT_TRANSLATE_NOOP("PathThreadMilling", "Direction of thread cutting operation"))
|
|
obj.addProperty("App::PropertyBool", "LeadInOut", "Operation", QtCore.QT_TRANSLATE_NOOP("PathThreadMilling", "Set to True to get lead in and lead out arcs at the start and end of the thread cut"))
|
|
obj.addProperty("App::PropertyLink", "ClearanceOp", "Operation", QtCore.QT_TRANSLATE_NOOP("PathThreadMilling", "Operation to clear the inside of the thread"))
|
|
obj.Direction = self.Directions
|
|
|
|
# Rotation related properties
|
|
if not hasattr(obj, 'EnableRotation'):
|
|
obj.addProperty("App::PropertyEnumeration", "EnableRotation", "Rotation", QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable rotation to gain access to pockets/areas not normal to Z axis."))
|
|
obj.EnableRotation = ['Off', 'A(x)', 'B(y)', 'A & B']
|
|
|
|
|
|
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 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):
|
|
# 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:
|
|
obj.Passes = 1
|
|
rPass = (rMajor - rMinor) / obj.Passes
|
|
passes = [rMajor]
|
|
for i in range(1, obj.Passes):
|
|
passes.append(rMajor - rPass * i)
|
|
return list(reversed(passes))
|
|
|
|
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': obj.ClearanceHeight.Value, 'F': self.vertRapid}))
|
|
|
|
for radius in threadPasses(obj.Passes, radiiInternal, obj.MajorDiameter.Value, obj.MinorDiameter.Value, float(self.tool.Diameter), float(self.tool.Crest)):
|
|
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()
|
|
if self.isToolSupported(obj, self.tool):
|
|
self.commandlist.append(Path.Command("(Begin Thread Milling)"))
|
|
|
|
(cmd, zStart, zFinal) = self.threadSetup(obj)
|
|
pitch = obj.Pitch.Value
|
|
if obj.TPI > 0:
|
|
pitch = 25.4 / obj.TPI
|
|
if pitch <= 0:
|
|
PathLog.error("Cannot create thread with pitch {}".format(pitch))
|
|
return
|
|
|
|
# rapid to clearance height
|
|
for loc in holes:
|
|
self.executeThreadMill(obj, FreeCAD.Vector(loc['x'], loc['y'], 0), cmd, zStart, zFinal, pitch)
|
|
else:
|
|
PathLog.error("No suitable Tool found for thread milling operation")
|
|
|
|
|
|
def opSetDefaultValues(self, obj, job):
|
|
obj.ThreadOrientation = self.RightHand
|
|
obj.ThreadType = self.ThreadTypeMetricInternal
|
|
obj.ThreadFit = 50
|
|
obj.Pitch = 1
|
|
obj.TPI = 0
|
|
obj.Passes = 1
|
|
obj.Direction = self.DirectionClimb
|
|
obj.LeadInOut = True
|
|
|
|
def isToolSupported(self, obj, tool):
|
|
'''Thread milling only supports thread milling cutters.'''
|
|
return hasattr(tool, 'Diameter') and hasattr(tool, 'Crest')
|
|
|
|
|
|
def SetupProperties():
|
|
setup = []
|
|
setup.append("ThreadOrientation")
|
|
setup.append("ThreadType")
|
|
setup.append("ThreadName")
|
|
setup.append("ThreadFit")
|
|
setup.append("MajorDiameter")
|
|
setup.append("MinorDiameter")
|
|
setup.append("Pitch")
|
|
setup.append("TPI")
|
|
setup.append("Passes")
|
|
setup.append("Direction")
|
|
setup.append("LeadInOut")
|
|
return setup
|
|
|
|
|
|
def Create(name, obj=None):
|
|
'''Create(name) ... Creates and returns a thread milling operation.'''
|
|
if obj is None:
|
|
obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name)
|
|
obj.Proxy = ObjectThreadMilling(obj, name)
|
|
if obj.Proxy:
|
|
obj.Proxy.findAllHoles(obj)
|
|
return obj
|
|
|