| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| |
|
| | 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." |
| |
|
| | |
| | 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 |
| |
|
| | |
| | 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: |
| | |
| | if obj.Direction == DirectionConventional: |
| | return ("G2", zTop, zBottom) |
| | |
| | |
| | return ("G3", zBottom, zTop) |
| | |
| | if obj.Direction == DirectionClimb: |
| | return ("G3", zTop, zBottom) |
| | |
| | return ("G2", zBottom, zTop) |
| |
|
| |
|
| | def threadSetupExternal(obj, zTop, zBottom): |
| | Path.Log.track() |
| | if obj.ThreadOrientation == RightHand: |
| | |
| | if obj.Direction == DirectionClimb: |
| | return ("G2", zTop, zBottom) |
| | |
| | return ("G3", zBottom, zTop) |
| | |
| | if obj.Direction == DirectionConventional: |
| | return ("G3", zTop, zBottom) |
| | |
| | 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 |
| | |
| | |
| | |
| | |
| | |
| | |
| | H = ((majorDia - minorDia) / 2.0) * 1.6 |
| | if internal: |
| | |
| | outerTip = majorDia / 2.0 + H / 8.0 |
| | |
| | toolTip = outerTip - toolCrest * SQRT_3_DIVIDED_BY_2 |
| | radii = ((minorDia - toolDia) / 2.0, toolTip - toolDia / 2.0) |
| | else: |
| | |
| | innerTip = minorDia / 2.0 - H / 4.0 |
| | |
| | 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) |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | minor, major = radii(internal, majorDia, minorDia, toolDia, toolCrest) |
| | H = float(major - minor) |
| | Hi = [H * math.sqrt((i + 1) / count) for i in range(count)] |
| |
|
| | |
| | |
| | |
| | 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.""" |
| |
|
| | @classmethod |
| | 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() |
| |
|
| | |
| | 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.addProperty( |
| | "App::PropertyEnumeration", |
| | "ThreadType", |
| | "Thread", |
| | QT_TRANSLATE_NOOP("App::Property", "Currently only internal"), |
| | ) |
| | |
| | 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: |
| | |
| | |
| | |
| | |
| | |
| | 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 |
| |
|
| | |
| | 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 |
| |
|