Download src/Mod/CAM/Path/Op/ThreadMilling.py from AbdulElahGwaith/FreeCAD: direct link, hf CLI and curl.
- Browser
- Download file 20 kB
-
https://huggingface.co/AbdulElahGwaith/FreeCAD/resolve/main/src/Mod/CAM/Path/Op/ThreadMilling.py
- Command line
-
hf download hf://AbdulElahGwaith/FreeCAD/src/Mod/CAM/Path/Op/ThreadMilling.py
-
curl -L -o ThreadMilling.py https://huggingface.co/AbdulElahGwaith/FreeCAD/resolve/main/src/Mod/CAM/Path/Op/ThreadMilling.py
20 kB
| # SPDX-License-Identifier: LGPL-2.1-or-later | |
| # *************************************************************************** | |
| # * 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 * | |
| # * * | |
| # *************************************************************************** | |
| import FreeCAD | |
| import Path | |
| import Path.Base.Generator.threadmilling as threadmilling | |
| import Path.Op.Base as PathOp | |
| import Path.Op.CircularHoleBase as PathCircularHoleBase | |
| import math | |
| from PySide.QtCore import QT_TRANSLATE_NOOP | |
| __title__ = "CAM Thread Milling Operation" | |
| __author__ = "sliptonic (Brad Collette)" | |
| __url__ = "https://www.freecad.org" | |
| __doc__ = "CAM thread milling operation." | |
| # math.sqrt(3)/2 ... 60deg triangle height | |
| SQRT_3_DIVIDED_BY_2 = 0.8660254037844386 | |
| if False: | |
| Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) | |
| Path.Log.trackModule(Path.Log.thisModule()) | |
| else: | |
| Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule()) | |
| translate = FreeCAD.Qt.translate | |
| # Constants | |
| LeftHand = "LeftHand" | |
| RightHand = "RightHand" | |
| ThreadTypeCustomExternal = "CustomExternal" | |
| ThreadTypeCustomInternal = "CustomInternal" | |
| ThreadTypeImperialExternal2A = "ImperialExternal2A" | |
| ThreadTypeImperialExternal3A = "ImperialExternal3A" | |
| ThreadTypeImperialInternal2B = "ImperialInternal2B" | |
| ThreadTypeImperialInternal3B = "ImperialInternal3B" | |
| ThreadTypeMetricExternal4G6G = "MetricExternal4G6G" | |
| ThreadTypeMetricExternal6G = "MetricExternal6G" | |
| ThreadTypeMetricInternal6H = "MetricInternal6H" | |
| DirectionClimb = "Climb" | |
| DirectionConventional = "Conventional" | |
| ThreadOrientations = [LeftHand, RightHand] | |
| ThreadTypeData = { | |
| ThreadTypeImperialExternal2A: "imperial-external-2A.csv", | |
| ThreadTypeImperialExternal3A: "imperial-external-3A.csv", | |
| ThreadTypeImperialInternal2B: "imperial-internal-2B.csv", | |
| ThreadTypeImperialInternal3B: "imperial-internal-3B.csv", | |
| ThreadTypeMetricExternal4G6G: "metric-external-4G6G.csv", | |
| ThreadTypeMetricExternal6G: "metric-external-6G.csv", | |
| ThreadTypeMetricInternal6H: "metric-internal-6H.csv", | |
| } | |
| ThreadTypesExternal = [ | |
| ThreadTypeCustomExternal, | |
| ThreadTypeImperialExternal2A, | |
| ThreadTypeImperialExternal3A, | |
| ThreadTypeMetricExternal4G6G, | |
| ThreadTypeMetricExternal6G, | |
| ] | |
| ThreadTypesInternal = [ | |
| ThreadTypeCustomInternal, | |
| ThreadTypeImperialInternal2B, | |
| ThreadTypeImperialInternal3B, | |
| ThreadTypeMetricInternal6H, | |
| ] | |
| ThreadTypesImperial = [ | |
| ThreadTypeImperialExternal2A, | |
| ThreadTypeImperialExternal3A, | |
| ThreadTypeImperialInternal2B, | |
| ThreadTypeImperialInternal3B, | |
| ] | |
| ThreadTypesMetric = [ | |
| ThreadTypeMetricExternal4G6G, | |
| ThreadTypeMetricExternal6G, | |
| ThreadTypeMetricInternal6H, | |
| ] | |
| ThreadTypes = ThreadTypesInternal + ThreadTypesExternal | |
| Directions = [DirectionClimb, DirectionConventional] | |
| def _isThreadInternal(obj): | |
| return obj.ThreadType in ThreadTypesInternal | |
| def threadSetupInternal(obj, zTop, zBottom): | |
| Path.Log.track() | |
| if obj.ThreadOrientation == RightHand: | |
| # Right hand thread, G2, top down -> conventional milling | |
| if obj.Direction == DirectionConventional: | |
| return ("G2", zTop, zBottom) | |
| # For climb milling we need to cut the thread from the bottom up | |
| # in the opposite direction -> G3 | |
| return ("G3", zBottom, zTop) | |
| # Left hand thread, G3, top down -> climb milling | |
| if obj.Direction == DirectionClimb: | |
| return ("G3", zTop, zBottom) | |
| # for conventional milling, cut bottom up with G2 | |
| return ("G2", zBottom, zTop) | |
| def threadSetupExternal(obj, zTop, zBottom): | |
| Path.Log.track() | |
| if obj.ThreadOrientation == RightHand: | |
| # right hand thread, G2, top down -> climb milling | |
| if obj.Direction == DirectionClimb: | |
| return ("G2", zTop, zBottom) | |
| # for conventional, mill bottom up the other way around | |
| return ("G3", zBottom, zTop) | |
| # left hand thread, G3, top down -> conventional milling | |
| if obj.Direction == DirectionConventional: | |
| return ("G3", zTop, zBottom) | |
| # for climb milling need to go bottom up and the other way | |
| return ("G2", zBottom, zTop) | |
| def threadSetup(obj): | |
| """Return (cmd, zbegin, zend) of thread milling operation""" | |
| Path.Log.track() | |
| zTop = obj.StartDepth.Value | |
| zBottom = obj.FinalDepth.Value | |
| if _isThreadInternal(obj): | |
| return threadSetupInternal(obj, zTop, zBottom) | |
| else: | |
| return threadSetupExternal(obj, zTop, zBottom) | |
| def threadRadii(internal, majorDia, minorDia, toolDia, toolCrest): | |
| """threadRadii(majorDia, minorDia, toolDia, toolCrest) ... returns the minimum and maximum radius for thread.""" | |
| Path.Log.track(internal, 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 | |
| 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 | |
| radii = ((minorDia - toolDia) / 2.0, toolTip - toolDia / 2.0) | |
| else: | |
| # mill outside in | |
| innerTip = minorDia / 2.0 - H / 4.0 | |
| # Compensate for the crest of the tool | |
| toolTip = innerTip - toolCrest * SQRT_3_DIVIDED_BY_2 | |
| radii = ((majorDia + toolDia) / 2.0, toolTip + toolDia / 2.0) | |
| Path.Log.track(radii) | |
| return radii | |
| def threadPasses(count, radii, internal, majorDia, minorDia, toolDia, toolCrest): | |
| Path.Log.track(count, radii, internal, majorDia, minorDia, toolDia, toolCrest) | |
| # the logic goes as follows, total area to be removed: | |
| # A = H * W ... where H is the depth and W is half the width of a thread | |
| # H = k * sin(30) = k * 1/2 -> k = 2 * H | |
| # W = k * cos(30) = k * sqrt(3)/2 | |
| # -> W = (2 * H) * sqrt(3) / 2 = H * sqrt(3) | |
| # A = sqrt(3) * H^2 | |
| # Each pass has to remove the same area | |
| # An = A / count = sqrt(3) * H^2 / count | |
| # Because each successive pass doesn't have to remove the aera of the previous | |
| # passes the result for the height: | |
| # Ai = (i + 1) * An = (i + 1) * sqrt(3) * Hi^2 = sqrt(3) * H^2 / count | |
| # Hi = sqrt(H^2 * (i + 1) / count) | |
| # Hi = H * sqrt((i + 1) / count) | |
| minor, major = radii(internal, majorDia, minorDia, toolDia, toolCrest) | |
| H = float(major - minor) | |
| Hi = [H * math.sqrt((i + 1) / count) for i in range(count)] | |
| # For external threads threadRadii returns the radii in reverse order because that's | |
| # the order in which they have to get milled. As a result H ends up being negative | |
| # and the math for internal and external threads is identical. | |
| passes = [minor + h for h in Hi] | |
| Path.Log.debug(f"threadPasses({minor}, {major}) -> H={H} : {Hi} --> {passes}") | |
| return passes | |
| def elevatorRadius(obj, center, internal, tool): | |
| """elevatorLocation(obj, center, internal, tool) ... return suitable location for the tool elevator""" | |
| Path.Log.track(center, internal, tool.Diameter) | |
| if internal: | |
| dy = float(obj.MinorDiameter - tool.Diameter) / 2 - 1 | |
| if dy < 0: | |
| if obj.MinorDiameter < tool.Diameter: | |
| Path.Log.error( | |
| "The selected tool is too big (d={}) for milling a thread with minor diameter D={}".format( | |
| tool.Diameter, obj.MinorDiameter | |
| ) | |
| ) | |
| dy = 0 | |
| else: | |
| dy = float(obj.MajorDiameter + tool.Diameter) / 2 + 1 | |
| return dy | |
| class ObjectThreadMilling(PathCircularHoleBase.ObjectOp): | |
| """Proxy object for thread milling operation.""" | |
| def propertyEnumerations(self, dataType="data"): | |
| """helixOpPropertyEnumerations(dataType="data")... return property enumeration lists of specified dataType. | |
| Args: | |
| dataType = 'data', 'raw', 'translated' | |
| Notes: | |
| 'data' is list of internal string literals used in code | |
| 'raw' is list of (translated_text, data_string) tuples | |
| 'translated' is list of translated string literals | |
| """ | |
| Path.Log.track() | |
| # Enumeration lists for App::PropertyEnumeration properties | |
| enums = { | |
| "ThreadType": [ | |
| ( | |
| translate("CAM_ThreadMilling", "Custom External"), | |
| ThreadTypeCustomExternal, | |
| ), | |
| ( | |
| translate("CAM_ThreadMilling", "Custom Internal"), | |
| ThreadTypeCustomInternal, | |
| ), | |
| ( | |
| translate("CAM_ThreadMilling", "Imperial External (2A)"), | |
| ThreadTypeImperialExternal2A, | |
| ), | |
| ( | |
| translate("CAM_ThreadMilling", "Imperial External (3A)"), | |
| ThreadTypeImperialExternal3A, | |
| ), | |
| ( | |
| translate("CAM_ThreadMilling", "Imperial Internal (2B)"), | |
| ThreadTypeImperialInternal2B, | |
| ), | |
| ( | |
| translate("CAM_ThreadMilling", "Imperial Internal (3B)"), | |
| ThreadTypeImperialInternal3B, | |
| ), | |
| ( | |
| translate("CAM_ThreadMilling", "Metric External (4G6G)"), | |
| ThreadTypeMetricExternal4G6G, | |
| ), | |
| ( | |
| translate("CAM_ThreadMilling", "Metric External (6G)"), | |
| ThreadTypeMetricExternal6G, | |
| ), | |
| ( | |
| translate("CAM_ThreadMilling", "Metric Internal (6H)"), | |
| ThreadTypeMetricInternal6H, | |
| ), | |
| ], | |
| "ThreadOrientation": [ | |
| ( | |
| translate("CAM_ThreadMilling", "LeftHand"), | |
| LeftHand, | |
| ), | |
| ( | |
| translate("CAM_ThreadMilling", "RightHand"), | |
| RightHand, | |
| ), | |
| ], | |
| "Direction": [ | |
| ( | |
| translate("CAM_ThreadMilling", "Climb"), | |
| DirectionClimb, | |
| ), | |
| ( | |
| translate("CAM_ThreadMilling", "Conventional"), | |
| DirectionConventional, | |
| ), | |
| ], | |
| } | |
| if dataType == "raw": | |
| return enums | |
| data = list() | |
| idx = 0 if dataType == "translated" else 1 | |
| Path.Log.debug(enums) | |
| for k, v in enumerate(enums): | |
| data.append((v, [tup[idx] for tup in enums[v]])) | |
| Path.Log.debug(data) | |
| return data | |
| def circularHoleFeatures(self, obj): | |
| Path.Log.track() | |
| return PathOp.FeatureBaseGeometry | |
| def initCircularHoleOperation(self, obj): | |
| Path.Log.track() | |
| obj.addProperty( | |
| "App::PropertyEnumeration", | |
| "ThreadOrientation", | |
| "Thread", | |
| QT_TRANSLATE_NOOP("App::Property", "Set thread orientation"), | |
| ) | |
| # obj.ThreadOrientation = ThreadOrientations | |
| obj.addProperty( | |
| "App::PropertyEnumeration", | |
| "ThreadType", | |
| "Thread", | |
| QT_TRANSLATE_NOOP("App::Property", "Currently only internal"), | |
| ) | |
| # obj.ThreadType = ThreadTypes | |
| obj.addProperty( | |
| "App::PropertyString", | |
| "ThreadName", | |
| "Thread", | |
| QT_TRANSLATE_NOOP("App::Property", "Defines which standard thread was chosen"), | |
| ) | |
| obj.addProperty( | |
| "App::PropertyLength", | |
| "MajorDiameter", | |
| "Thread", | |
| QT_TRANSLATE_NOOP("App::Property", "Set thread's major diameter"), | |
| ) | |
| obj.addProperty( | |
| "App::PropertyLength", | |
| "MinorDiameter", | |
| "Thread", | |
| QT_TRANSLATE_NOOP("App::Property", "Set thread's minor diameter"), | |
| ) | |
| obj.addProperty( | |
| "App::PropertyLength", | |
| "Pitch", | |
| "Thread", | |
| QT_TRANSLATE_NOOP("App::Property", "Set thread's pitch - used for metric threads"), | |
| ) | |
| obj.addProperty( | |
| "App::PropertyInteger", | |
| "TPI", | |
| "Thread", | |
| QT_TRANSLATE_NOOP( | |
| "App::Property", | |
| "Set thread's TPI (turns per inch) - used for imperial threads", | |
| ), | |
| ) | |
| obj.addProperty( | |
| "App::PropertyInteger", | |
| "ThreadFit", | |
| "Thread", | |
| QT_TRANSLATE_NOOP( | |
| "App::Property", | |
| "Override to control how loose or tight the threads are milled", | |
| ), | |
| ) | |
| obj.addProperty( | |
| "App::PropertyInteger", | |
| "Passes", | |
| "Operation", | |
| QT_TRANSLATE_NOOP("App::Property", "Set how many passes are used to cut the thread"), | |
| ) | |
| obj.addProperty( | |
| "App::PropertyEnumeration", | |
| "Direction", | |
| "Operation", | |
| QT_TRANSLATE_NOOP("App::Property", "Direction of thread cutting operation"), | |
| ) | |
| obj.addProperty( | |
| "App::PropertyBool", | |
| "LeadInOut", | |
| "Operation", | |
| 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.addProperty( | |
| "App::PropertyLink", | |
| "ClearanceOp", | |
| "Operation", | |
| QT_TRANSLATE_NOOP("App::Property", "Operation to clear the inside of the thread"), | |
| ) | |
| for n in self.propertyEnumerations(): | |
| setattr(obj, n[0], n[1]) | |
| def threadPassRadii(self, obj): | |
| Path.Log.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): | |
| Path.Log.track(obj.Label, loc, gcode, zStart, zFinal, pitch) | |
| elevator = elevatorRadius(obj, loc, _isThreadInternal(obj), self.tool) | |
| move2clearance = Path.Command("G0", {"Z": obj.ClearanceHeight.Value, "F": self.vertRapid}) | |
| self.commandlist.append(move2clearance) | |
| start = None | |
| for radius in threadPasses( | |
| obj.Passes, | |
| threadRadii, | |
| _isThreadInternal(obj), | |
| obj.MajorDiameter.Value, | |
| obj.MinorDiameter.Value, | |
| float(self.tool.Diameter), | |
| float(self.tool.Crest), | |
| ): | |
| if not start is None: | |
| # and not _isThreadInternal(obj): | |
| # and not obj.LeadInOut: | |
| # external thread without lead in/out have to go up and over | |
| # in other words we need a move to clearance and not take any | |
| # shortcuts when moving to the elevator position | |
| self.commandlist.append(move2clearance) | |
| start = None | |
| commands, start = threadmilling.generate( | |
| loc, | |
| gcode, | |
| zStart, | |
| zFinal, | |
| pitch, | |
| radius, | |
| obj.LeadInOut, | |
| elevator, | |
| start, | |
| ) | |
| 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): | |
| Path.Log.track() | |
| if self.isToolSupported(obj, self.tool): | |
| self.commandlist.append(Path.Command("(Begin Thread Milling)")) | |
| (cmd, zStart, zFinal) = threadSetup(obj) | |
| pitch = obj.Pitch.Value | |
| if obj.TPI > 0: | |
| pitch = 25.4 / obj.TPI | |
| if pitch <= 0: | |
| Path.Log.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: | |
| Path.Log.error("No suitable Tool found for thread milling operation") | |
| def opSetDefaultValues(self, obj, job): | |
| Path.Log.track() | |
| obj.ThreadOrientation = RightHand | |
| obj.ThreadType = ThreadTypeMetricInternal6H | |
| obj.ThreadFit = 50 | |
| obj.Pitch = 1 | |
| obj.TPI = 0 | |
| obj.Passes = 1 | |
| obj.Direction = DirectionClimb | |
| obj.LeadInOut = False | |
| def isToolSupported(self, obj, tool): | |
| """Thread milling only supports thread milling cutters.""" | |
| support = hasattr(tool, "Diameter") and hasattr(tool, "Crest") | |
| Path.Log.track(tool.Label, support) | |
| return support | |
| 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 | |