freecad-cam/Mod/CAM/Path/Base/Generator/threadmilling.py
2026-02-01 01:59:24 +01:00

199 lines
7.4 KiB
Python

# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2022 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 math
from PySide.QtCore import QT_TRANSLATE_NOOP
__title__ = "CAM Thread Milling generator"
__author__ = "sliptonic (Brad Collette)"
__url__ = "https://www.freecad.org"
__doc__ = "CAM thread milling operation."
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
def _comment(path, msg):
"""Nice for debugging to insert markers into generated g-code"""
if False:
path.append(Path.Command("(------- {} -------)".format(msg)))
class _Thread(object):
"""Helper class for dealing with different thread types"""
def __init__(self, cmd, zStart, zFinal, pitch, internal):
self.cmd = cmd
if zStart < zFinal:
self.pitch = pitch
else:
self.pitch = -pitch
self.hPitch = self.pitch / 2
self.zStart = zStart
self.zFinal = zFinal
self.internal = internal
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 g4Opposite(self):
return "G2" if self.isG3() else "G3"
def g4LeadInOut(self):
if self.internal:
return self.cmd
return self.g4Opposite()
def g4Start2Elevator(self):
return self.g4Opposite()
def generate(center, cmd, zStart, zFinal, pitch, radius, leadInOut, elevator, start):
"""generate(center, cmd, zStart, zFinal, pitch, radius, leadInOut, elevator, start) ... returns the g-code to mill the given internal thread"""
thread = _Thread(cmd, zStart, zFinal, pitch, radius > elevator)
yMin = center.y - radius
yMax = center.y + radius
path = []
# at this point the tool is at a safe heiht (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.
if leadInOut:
_comment(path, "lead-in")
if start is None:
path.append(Path.Command("G0", {"X": center.x, "Y": center.y + elevator}))
path.append(Path.Command("G0", {"Z": thread.zStart}))
else:
path.append(
Path.Command(
thread.g4Start2Elevator(),
{
"X": center.x,
"Y": center.y + elevator,
"Z": thread.zStart,
"I": (center.x - start.x) / 2,
"J": (center.y + elevator - start.y) / 2,
"K": (thread.zStart - start.z) / 2,
},
)
)
path.append(
Path.Command(
thread.g4LeadInOut(),
{"Y": yMax, "J": (yMax - (center.y + elevator)) / 2},
)
)
_comment(path, "lead-in")
else:
if start is None:
path.append(Path.Command("G0", {"X": center.x, "Y": center.y + elevator}))
path.append(Path.Command("G0", {"Z": thread.zStart}))
else:
path.append(
Path.Command("G0", {"X": center.x, "Y": center.y + elevator, "Z": thread.zStart})
)
path.append(Path.Command("G1", {"Y": yMax}))
z = thread.zStart
r = -radius
i = 0
while not Path.Geom.isRoughly(z, thread.zFinal):
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 = z + thread.hPitch
if Path.Geom.isRoughly(z, thread.zFinal):
x = center.x
y = yMin if (i & 0x01) else yMax
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(center.y, r * dy)
x = thread.adjustX(center.x, r * dx)
_comment(path, "finish-thread")
path.append(Path.Command(thread.cmd, {"X": x, "Y": y, "Z": thread.zFinal, "J": r}))
_comment(path, "finish-thread")
a = math.atan2(y - center.y, x - center.x)
dx = math.cos(a) * (radius - elevator)
dy = math.sin(a) * (radius - elevator)
Path.Log.debug("")
Path.Log.debug("a={}: dx={:.2f}, dy={:.2f}".format(a / math.pi * 180, dx, dy))
elevatorX = x - dx
elevatorY = y - dy
Path.Log.debug("({:.2f}, {:.2f}) -> ({:.2f}, {:.2f})".format(x, y, elevatorX, elevatorY))
if leadInOut:
_comment(path, "lead-out")
path.append(
Path.Command(
thread.g4LeadInOut(),
{"X": elevatorX, "Y": elevatorY, "I": -dx / 2, "J": -dy / 2},
)
)
_comment(path, "lead-out")
else:
path.append(Path.Command("G1", {"X": elevatorX, "Y": elevatorY}))
return (path, FreeCAD.Vector(elevatorX, elevatorY, thread.zFinal))