diff --git a/src/Mod/Path/Gui/Resources/panels/PageOpThreadMillingEdit.ui b/src/Mod/Path/Gui/Resources/panels/PageOpThreadMillingEdit.ui index 2e98648175..423c9fbabb 100644 --- a/src/Mod/Path/Gui/Resources/panels/PageOpThreadMillingEdit.ui +++ b/src/Mod/Path/Gui/Resources/panels/PageOpThreadMillingEdit.ui @@ -7,7 +7,7 @@ 0 0 482 - 736 + 756 @@ -167,7 +167,7 @@ false - Flat + Crest @@ -231,6 +231,13 @@ + + + + Lead In/Out + + + diff --git a/src/Mod/Path/PathScripts/PathThreadMilling.py b/src/Mod/Path/PathScripts/PathThreadMilling.py index 0feb748825..db226dfeb0 100644 --- a/src/Mod/Path/PathScripts/PathThreadMilling.py +++ b/src/Mod/Path/PathScripts/PathThreadMilling.py @@ -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 diff --git a/src/Mod/Path/PathScripts/PathThreadMillingGui.py b/src/Mod/Path/PathScripts/PathThreadMillingGui.py index e59f783292..846a5b6e5f 100644 --- a/src/Mod/Path/PathScripts/PathThreadMillingGui.py +++ b/src/Mod/Path/PathScripts/PathThreadMillingGui.py @@ -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)