CAM: Convert tapping operation to experimental feature, Add tap pitch support, improve tapping logic, and update toolbit schema and legacy linuxcnc post
CAM/App/PathSegmentWalker.cpp - Add G74 to drill/tap/bore G-code recognition for tapping cycles CAM/InitGui.py - Move CAM_Tapping command behind experimental feature flag - Only group drilling/tapping commands if both are enabled CAM/Path/Base/Generator/tapping.py - Add pitch and spindle_speed parameters to tapping.generate - Output S (spindle speed) and F (pitch) in generated G-code CAM/Path/Op/Tapping.py - Require Pitch property for tap tools and SpindleSpeed for tool controllers - Pass pitch and spindle speed to tapping.generate - Use SpindleDirection to determine right/left hand tap CAM/Path/Post/scripts/linuxcnc_post.py - Handle G84/G74 tapping cycles: convert pitch and spindle speed to feed rate - Remove F and S from output and recalculate F as needed CAM/Path/Tool/shape/models/tap.py - Add Pitch property to ToolBitShapeTap schema - CAM/Path/Tool/toolbit/models/tap.py - Show pitch and rotation in tap tool summary - Use is_imperial_pitch to format pitch as TPI or mm CAM/Path/Tool/toolbit/util.py - Add is_imperial_pitch utility to classify pitch as imperial or metric CAM/Tools/Bit/375-16_Tap.fctb - Remove unused parameters (Coating, Rotation, TPI, Type) - Keep only relevant tap parameters for new schema
This commit is contained in:
@@ -313,8 +313,9 @@ void PathSegmentWalker::walk(PathSegmentVisitor& cb, const Base::Vector3d& start
|
||||
// relative mode
|
||||
absolutecenter = false;
|
||||
}
|
||||
else if ((name == "G73") || (name == "G81") || (name == "G82") || (name == "G83")
|
||||
|| (name == "G84") || (name == "G85") || (name == "G86") || (name == "G89")) {
|
||||
else if ((name == "G73") || (name == "G74") || (name == "G81") || (name == "G82")
|
||||
|| (name == "G83") || (name == "G84") || (name == "G85") || (name == "G86")
|
||||
|| (name == "G89")) {
|
||||
// drill,tap,bore
|
||||
double r = 0;
|
||||
if (cmd.has("R")) {
|
||||
|
||||
@@ -148,7 +148,7 @@ class CAMWorkbench(Workbench):
|
||||
]
|
||||
threedopcmdlist = ["CAM_Pocket3D"]
|
||||
engravecmdlist = ["CAM_Engrave", "CAM_Deburr", "CAM_Vcarve"]
|
||||
drillingcmdlist = ["CAM_Drilling", "CAM_Tapping"]
|
||||
drillingcmdlist = ["CAM_Drilling"]
|
||||
modcmdlist = ["CAM_OperationCopy", "CAM_Array", "CAM_SimpleCopy"]
|
||||
dressupcmdlist = [
|
||||
"CAM_DressupArray",
|
||||
@@ -175,14 +175,21 @@ class CAMWorkbench(Workbench):
|
||||
QT_TRANSLATE_NOOP("CAM_EngraveTools", "Engraving Operations"),
|
||||
),
|
||||
)
|
||||
drillingcmdgroup = ["CAM_DrillingTools"]
|
||||
FreeCADGui.addCommand(
|
||||
"CAM_DrillingTools",
|
||||
PathCommandGroup(
|
||||
drillingcmdlist,
|
||||
QT_TRANSLATE_NOOP("CAM_DrillingTools", "Drilling Operations"),
|
||||
),
|
||||
)
|
||||
if Path.Preferences.experimentalFeaturesEnabled():
|
||||
drillingcmdlist.append("CAM_Tapping")
|
||||
|
||||
if set(["CAM_Drilling", "CAM_Tapping"]).issubset(drillingcmdlist):
|
||||
drillingcmdgroup = ["CAM_DrillingTools"]
|
||||
FreeCADGui.addCommand(
|
||||
"CAM_DrillingTools",
|
||||
PathCommandGroup(
|
||||
drillingcmdlist,
|
||||
QT_TRANSLATE_NOOP("CAM_DrillingTools", "Drilling Operations"),
|
||||
),
|
||||
)
|
||||
else:
|
||||
drillingcmdgroup = drillingcmdlist
|
||||
|
||||
dressupcmdgroup = ["CAM_DressupTools"]
|
||||
FreeCADGui.addCommand(
|
||||
"CAM_DressupTools",
|
||||
|
||||
@@ -38,7 +38,15 @@ else:
|
||||
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
|
||||
|
||||
|
||||
def generate(edge, dwelltime=0.0, repeat=1, retractheight=None, righthand=True):
|
||||
def generate(
|
||||
edge,
|
||||
dwelltime=0.0,
|
||||
repeat=1,
|
||||
retractheight=None,
|
||||
righthand=True,
|
||||
pitch=None,
|
||||
spindle_speed=None,
|
||||
):
|
||||
"""
|
||||
Generates Gcode for tapping a single hole.
|
||||
|
||||
@@ -90,6 +98,8 @@ def generate(edge, dwelltime=0.0, repeat=1, retractheight=None, righthand=True):
|
||||
cmdParams["Y"] = startPoint.y
|
||||
cmdParams["Z"] = endPoint.z
|
||||
cmdParams["R"] = retractheight if retractheight is not None else startPoint.z
|
||||
cmdParams["S"] = spindle_speed if spindle_speed is not None else 1.0 # Sanity default
|
||||
cmdParams["F"] = float(pitch) if pitch is not None else 100.0 # Sanity default
|
||||
|
||||
if repeat < 1:
|
||||
raise ValueError("repeat must be 1 or greater")
|
||||
|
||||
@@ -147,13 +147,11 @@ class ObjectTapping(PathCircularHoleBase.ObjectOp):
|
||||
Path.Log.track()
|
||||
machine = PathMachineState.MachineState()
|
||||
|
||||
if not hasattr(obj.ToolController.Tool, "Pitch") or not hasattr(
|
||||
obj.ToolController.Tool, "TPI"
|
||||
):
|
||||
if not hasattr(obj.ToolController.Tool, "Pitch"):
|
||||
Path.Log.error(
|
||||
translate(
|
||||
"Path_Tapping",
|
||||
"Tapping Operation requires a Tap tool with Pitch or TPI",
|
||||
"Tapping Operation requires a Tap tool with Pitch",
|
||||
)
|
||||
)
|
||||
return
|
||||
@@ -192,7 +190,6 @@ class ObjectTapping(PathCircularHoleBase.ObjectOp):
|
||||
|
||||
# iterate the edgelist and generate gcode
|
||||
for edge in edgelist:
|
||||
|
||||
Path.Log.debug(edge)
|
||||
|
||||
# move to hole location
|
||||
@@ -222,11 +219,40 @@ class ObjectTapping(PathCircularHoleBase.ObjectOp):
|
||||
repeat = 1 # technical debt: Add a repeat property for user control
|
||||
|
||||
# Get attribute from obj.tool, assign default and set to bool for passing to generate
|
||||
isRightHand = getattr(obj.ToolController.Tool, "Rotation", "Right Hand") == "Right Hand"
|
||||
isRightHand = (
|
||||
getattr(obj.ToolController.Tool, "SpindleDirection", "Forward") == "Forward"
|
||||
)
|
||||
|
||||
# Get pitch in mm as a float (no unit string)
|
||||
pitch = getattr(obj.ToolController.Tool, "Pitch", None)
|
||||
if pitch is None or pitch == 0:
|
||||
Path.Log.error(
|
||||
translate(
|
||||
"Path_Tapping",
|
||||
"Tapping Operation requires a Tap tool with non-zero Pitch",
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
spindle_speed = getattr(obj.ToolController, "SpindleSpeed", None)
|
||||
if spindle_speed is None or spindle_speed == 0:
|
||||
Path.Log.error(
|
||||
translate(
|
||||
"Path_Tapping",
|
||||
"Tapping Operation requires a ToolController with non-zero SpindleSpeed",
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
try:
|
||||
tappingcommands = tapping.generate(
|
||||
edge, dwelltime, repeat, obj.RetractHeight.Value, isRightHand
|
||||
edge,
|
||||
dwelltime,
|
||||
repeat,
|
||||
obj.RetractHeight.Value,
|
||||
isRightHand,
|
||||
pitch,
|
||||
spindle_speed,
|
||||
)
|
||||
|
||||
except ValueError as e: # any targets that fail the generator are ignored
|
||||
|
||||
@@ -202,7 +202,6 @@ def export(objectslist, filename, argstring):
|
||||
gcode += linenumber() + UNITS + "\n"
|
||||
|
||||
for obj in objectslist:
|
||||
|
||||
# Skip inactive operations
|
||||
if not PathUtil.activeForOp(obj):
|
||||
continue
|
||||
@@ -321,7 +320,6 @@ def parse(pathobj):
|
||||
out += parse(p)
|
||||
return out
|
||||
else: # parsing simple path
|
||||
|
||||
# groups might contain non-path things like stock.
|
||||
if not hasattr(pathobj, "Path"):
|
||||
return out
|
||||
@@ -337,7 +335,6 @@ def parse(pathobj):
|
||||
#
|
||||
# for c in PathUtils.getPathWithPlacement(pathobj).Commands:
|
||||
for c in pathobj.Path.Commands:
|
||||
|
||||
outstring = []
|
||||
command = c.Name
|
||||
outstring.append(command)
|
||||
@@ -350,6 +347,34 @@ def parse(pathobj):
|
||||
if c.Name.startswith("(") and not OUTPUT_COMMENTS: # command is a comment
|
||||
continue
|
||||
|
||||
# Handle G84/G74 tapping cycles
|
||||
if command in ("G84", "G74") and "F" in c.Parameters:
|
||||
pitch_mm = float(c.Parameters["F"])
|
||||
c.Parameters.pop("F") # Remove F from output, we'll handle it
|
||||
|
||||
# Get spindle speed (from S param or last known value)
|
||||
spindle_speed = None
|
||||
if "S" in c.Parameters:
|
||||
spindle_speed = float(c.Parameters["S"])
|
||||
c.Parameters.pop("S")
|
||||
|
||||
# Convert pitch to inches if needed
|
||||
if UNITS == "G20": # imperial
|
||||
pitch = pitch_mm / 25.4
|
||||
else:
|
||||
pitch = pitch_mm
|
||||
|
||||
# Calculate feed rate
|
||||
if spindle_speed is not None:
|
||||
feed_rate = pitch * spindle_speed
|
||||
speed = Units.Quantity(feed_rate, UNIT_SPEED_FORMAT)
|
||||
outstring.append(
|
||||
"F" + format(float(speed.getValueAs(UNIT_SPEED_FORMAT)), precision_string)
|
||||
)
|
||||
else:
|
||||
# No spindle speed found, output pitch as F
|
||||
outstring.append("F" + format(pitch, precision_string))
|
||||
|
||||
# Now add the remaining parameters in order
|
||||
for param in params:
|
||||
if param in c.Parameters:
|
||||
@@ -428,8 +453,6 @@ def parse(pathobj):
|
||||
# append the line to the final output
|
||||
for w in outstring:
|
||||
out += w + COMMAND_SPACE
|
||||
# Note: Do *not* strip `out`, since that forces the allocation
|
||||
# of a contiguous string & thus quadratic complexity.
|
||||
out += "\n"
|
||||
|
||||
return out
|
||||
|
||||
@@ -56,6 +56,10 @@ class ToolBitShapeTap(ToolBitShape):
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Tip angle"),
|
||||
"App::PropertyAngle",
|
||||
),
|
||||
"Pitch": (
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Thread pitch"),
|
||||
"App::PropertyLength",
|
||||
),
|
||||
}
|
||||
|
||||
@property
|
||||
|
||||
@@ -24,6 +24,7 @@ import Path
|
||||
from ...shape import ToolBitShapeTap
|
||||
from ..mixins import RotaryToolBitMixin, CuttingToolMixin
|
||||
from .base import ToolBit
|
||||
from ..util import is_imperial_pitch
|
||||
|
||||
|
||||
class ToolBitTap(ToolBit, CuttingToolMixin, RotaryToolBitMixin):
|
||||
@@ -39,7 +40,34 @@ class ToolBitTap(ToolBit, CuttingToolMixin, RotaryToolBitMixin):
|
||||
diameter = self.get_property_str("Diameter", "?", precision=3)
|
||||
flutes = self.get_property("Flutes")
|
||||
cutting_edge_length = self.get_property_str("CuttingEdgeLength", "?", precision=3)
|
||||
pitch_raw = self.get_property("Pitch")
|
||||
|
||||
spindle_direction = self.get_property_str("SpindleDirection", "Forward")
|
||||
if spindle_direction == "Forward":
|
||||
rotation = "Right Hand"
|
||||
elif spindle_direction == "Reverse":
|
||||
rotation = "Left Hand"
|
||||
else:
|
||||
rotation = spindle_direction
|
||||
|
||||
if isinstance(pitch_raw, FreeCAD.Units.Quantity):
|
||||
pitch_mm = pitch_raw.getValueAs("mm")
|
||||
else:
|
||||
pitch_mm = FreeCAD.Units.Quantity(str(pitch_raw)).getValueAs("mm")
|
||||
|
||||
if pitch_raw:
|
||||
try:
|
||||
if is_imperial_pitch(pitch_raw):
|
||||
tpi = round(25.4 / pitch_mm, 2)
|
||||
pitch = f"{int(tpi) if tpi == int(tpi) else tpi} TPI"
|
||||
else:
|
||||
pitch = f"{pitch_mm} mm"
|
||||
except Exception:
|
||||
pitch = str(pitch_raw)
|
||||
else:
|
||||
pitch = "?"
|
||||
|
||||
return FreeCAD.Qt.translate(
|
||||
"CAM", f"{diameter} tap, {flutes}-flute, {cutting_edge_length} cutting edge"
|
||||
"CAM",
|
||||
f"{diameter} {pitch} {rotation} tap, {flutes}-flute, {cutting_edge_length} cutting edge",
|
||||
)
|
||||
|
||||
@@ -47,3 +47,33 @@ def format_value(value: FreeCAD.Units.Quantity | int | float | None, precision:
|
||||
return value.getUserPreferred()[0]
|
||||
return value.UserString
|
||||
return str(value)
|
||||
|
||||
|
||||
def is_imperial_pitch(pitch_mm, tol=1e-6):
|
||||
"""
|
||||
Classify a pitch in mm as imperial vs metric.
|
||||
Rule:
|
||||
- If pitch_mm is ~2 decimal places clean -> metric,
|
||||
unless it corresponds to an exact whole-number TPI.
|
||||
- Otherwise, treat as imperial.
|
||||
"""
|
||||
import math
|
||||
|
||||
try:
|
||||
mm = float(pitch_mm)
|
||||
except Exception:
|
||||
return False
|
||||
if mm <= 0:
|
||||
return False
|
||||
|
||||
# Check if it's "two-decimal clean"
|
||||
two_dec_clean = abs(mm - round(mm, 2)) <= tol
|
||||
|
||||
# Compute TPI
|
||||
tpi = 25.4 / mm
|
||||
whole_tpi = round(tpi)
|
||||
is_whole_tpi = math.isclose(tpi, whole_tpi, abs_tol=1e-6)
|
||||
|
||||
if two_dec_clean and not is_whole_tpi:
|
||||
return False # metric
|
||||
return True # imperial
|
||||
|
||||
@@ -4,17 +4,13 @@
|
||||
"shape": "tap.fcstd",
|
||||
"shape-type": "tap",
|
||||
"parameter": {
|
||||
"Coating": "None",
|
||||
"CuttingEdgeLength": "1.063 \"",
|
||||
"Diameter": "0.375 \"",
|
||||
"Flutes": "3",
|
||||
"Length": "2.500 \"",
|
||||
"Pitch": "0.000 in",
|
||||
"Rotation": "Right Hand",
|
||||
"ShankDiameter": "0.250 \"",
|
||||
"TPI": "16",
|
||||
"TipAngle": "90.000 \u00b0",
|
||||
"Type": "HSS"
|
||||
"TipAngle": "90.000 \u00b0"
|
||||
},
|
||||
"attribute": {}
|
||||
}
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user