# -*- coding: utf-8 -*- # *************************************************************************** # * * # * Copyright (c) 2019 sliptonic * # * * # * 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", "Defines 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 (turns per inch) - 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 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, parentJob=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, parentJob) if obj.Proxy: obj.Proxy.findAllHoles(obj) return obj