From df1a1ab9818c4da40d50b5fb808d999eb2035869 Mon Sep 17 00:00:00 2001 From: Petter Reinholdtsen Date: Tue, 24 Feb 2026 22:48:41 +0100 Subject: [PATCH 1/2] CAM: Implemented Fanuc thread tapping and fixed crash in post processor. The current post processor fail completely for any operation because the ShapeName attribute no longer exist. Changed code to look for attributes present in FreeCAD 1.1 and master branch. Rewrote thread tapping code to work with new tapping support. Switched thread tapping to use feed in distance per minute (G94) instead of earlier distance per revolution to avoid switching between mm/min and mm/rev for different operations. Fixes #27814 --- src/Mod/CAM/Path/Post/scripts/fanuc_post.py | 151 +++++++++++--------- 1 file changed, 83 insertions(+), 68 deletions(-) diff --git a/src/Mod/CAM/Path/Post/scripts/fanuc_post.py b/src/Mod/CAM/Path/Post/scripts/fanuc_post.py index 2d39558af1..62bb9eb1c8 100644 --- a/src/Mod/CAM/Path/Post/scripts/fanuc_post.py +++ b/src/Mod/CAM/Path/Post/scripts/fanuc_post.py @@ -50,7 +50,7 @@ fanuc_post.export(object,"/path/to/file.ncc","") """ # Preamble text will appear at the beginning of the GCODE output file. -DEFAULT_PREAMBLE = """G17 G54 G40 G49 G80 G90 +DEFAULT_PREAMBLE = """G17 G54 G40 G49 G80 G90 G94 """ # Postamble text will appear following the last operation. @@ -152,7 +152,7 @@ TOOL_CHANGE = """G28 G91 Z0 # List of drill G codes where some parameters are required and their # required parameters. -DRILL_OPERATION = ("G73", "G81", "G82", "G83", "G84", "G85") +DRILL_OPERATION = ("G73", "G81", "G82", "G83", "G85") DRILL_PARAM_REQ = ("L", "P", "Q", "R", "Z") @@ -507,85 +507,100 @@ def parse(pathobj): if command == "G0": continue - # if tool a tap, we thread tap, so stop the spindle for now. - # This only trigger when pathobj is a ToolController. + # If tool a tap, we will thread tap, so stop the spindle + # for now as there is no point in starting it to stop it + # in the G74/G84 operation after S29. This only trigger + # when pathobj is a ToolController. if command == "M03" or command == "M3": - if hasattr(pathobj, "Tool") and pathobj.Tool.ShapeName.lower() == "tap": + if ( + hasattr(pathobj, "Tool") + and getattr(pathobj.Tool, "ShapeType", "").lower() == "tap" + ): tapSpeed = int(pathobj.SpindleSpeed) continue - # Convert drill cycles to tap cycles if tool is a tap. + # Handle thread tapping cycles. Uses rigid tapping. # This only trigger when pathobj is a Operation. - if command == "G81" or command == "G83": - if ( - hasattr(pathobj, "ToolController") - and pathobj.ToolController.Tool.ShapeName.lower() == "tap" - ): - command = "G84" - out += linenumber() + "G95\n" - paramstring = "" - for param in ["X", "Y"]: - if param in c.Parameters: - if ( - (not OUTPUT_DOUBLES) - and (param in currLocation) - and (currLocation[param] == c.Parameters[param]) - ): - continue - else: - pos = Units.Quantity(c.Parameters[param], FreeCAD.Units.Length) - paramstring += ( - " " - + param - + format( - float(pos.getValueAs(UNIT_FORMAT)), - precision_string, - ) - ) - if paramstring != "": - out += linenumber() + "G00" + paramstring + "\n" - - if "S" in c.Parameters: - tapSpeed = int(c.Parameters["S"]) - out += "M29 S" + str(tapSpeed) + "\n" - - for param in ["Z", "R"]: - if param in c.Parameters: - if ( - (not OUTPUT_DOUBLES) - and (param in currLocation) - and (currLocation[param] == c.Parameters[param]) - ): - continue - else: - pos = Units.Quantity(c.Parameters[param], FreeCAD.Units.Length) - paramstring += ( - " " - + param - + format( - float(pos.getValueAs(UNIT_FORMAT)), - precision_string, - ) - ) - # in this mode, F is the distance per revolution of the thread (pitch) - # P is the dwell time in seconds at the bottom of the thread - # Q is the peck depth of the threading operation - for param in ["F", "P", "Q"]: - if param in c.Parameters: - value = Units.Quantity(c.Parameters[param], FreeCAD.Units.Length) + if command == "G74" or command == "G84": + pitch_mm = float(c.Parameters["F"]) + # Convert pitch to inches if needed + if UNITS == "G20": # imperial + pitch = pitch_mm / 25.4 + else: + pitch = pitch_mm + paramstring = "" + for param in ["X", "Y"]: + if param in c.Parameters: + if ( + (not OUTPUT_DOUBLES) + and (param in currLocation) + and (currLocation[param] == c.Parameters[param]) + ): + continue + else: + pos = Units.Quantity(c.Parameters[param], FreeCAD.Units.Length) paramstring += ( " " + param + format( - float(value.getValueAs(UNIT_FORMAT)), + float(pos.getValueAs(UNIT_FORMAT)), + precision_string, + ) + ) + if paramstring != "": + out += linenumber() + "G00" + paramstring + "\n" + + if "S" in c.Parameters: + tapSpeed = int(c.Parameters["S"]) + out += "M29 S" + str(tapSpeed) + "\n" + + for param in ["Z", "R"]: + if param in c.Parameters: + if ( + (not OUTPUT_DOUBLES) + and (param in currLocation) + and (currLocation[param] == c.Parameters[param]) + ): + continue + else: + pos = Units.Quantity(c.Parameters[param], FreeCAD.Units.Length) + paramstring += ( + " " + + param + + format( + float(pos.getValueAs(UNIT_FORMAT)), precision_string, ) ) - out += linenumber() + "G84" + paramstring + "\n" - out += linenumber() + "G80\n" - out += linenumber() + "G94\n" - continue + # Calculate feed rate as distance per minute + if tapSpeed is not None: + feed_rate = pitch * tapSpeed + speed = Units.Quantity(feed_rate, UNIT_SPEED_FORMAT) + paramstring += " F" + format( + float(speed.getValueAs(UNIT_SPEED_FORMAT)), precision_string + ) + else: + # No spindle speed found, output pitch as F + paramstring += " F" + format(pitch, precision_string) + + # P is the dwell time in seconds at the bottom of the thread + # Q is the peck depth of the threading operation + for param in ["P", "Q"]: + if param in c.Parameters: + value = Units.Quantity(c.Parameters[param], FreeCAD.Units.Length) + paramstring += ( + " " + + param + + format( + float(value.getValueAs(UNIT_FORMAT)), + precision_string, + ) + ) + + out += linenumber() + command + paramstring + "\n" + out += linenumber() + "G80\n" # End tapping cycle + continue outstring.append(command) From 263bfe9948bc6dddcf86bd79c9bfaa38c51fed83 Mon Sep 17 00:00:00 2001 From: Petter Reinholdtsen Date: Sat, 28 Feb 2026 21:55:28 +0100 Subject: [PATCH 2/2] CAM: Updated Fanuc test and support methods to match working post script. The thread tapping is corrected and the test code is adjusted to match. --- src/Mod/CAM/CAMTests/PostTestMocks.py | 6 +++++- src/Mod/CAM/CAMTests/TestFanucPost.py | 20 +++++++++----------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/Mod/CAM/CAMTests/PostTestMocks.py b/src/Mod/CAM/CAMTests/PostTestMocks.py index c0b7506de0..a112b77cfb 100644 --- a/src/Mod/CAM/CAMTests/PostTestMocks.py +++ b/src/Mod/CAM/CAMTests/PostTestMocks.py @@ -30,7 +30,7 @@ import Path class MockTool: def __init__(self): - self.ShapeName = "endmill" + self.ShapeType = "endmill" class MockToolController: @@ -56,6 +56,7 @@ class MockToolController: [Path.Command(f"M6 T{tool_number}"), Path.Command(f"M3 S{spindle_speed}")] ) + @property def InList(self): return [] @@ -72,6 +73,7 @@ class MockOperation: # Create an empty path by default self.Path = Path.Path() + @property def InList(self): """Mock InList - operations belong to a job.""" return [] @@ -136,7 +138,9 @@ class MockJob: self.Fixtures = ["G54"] self.OrderOutputBy = "Tool" self.SplitOutput = False + self.TypeId = "dummy" + @property def InList(self): """Mock InList for fixture setup.""" return [] diff --git a/src/Mod/CAM/CAMTests/TestFanucPost.py b/src/Mod/CAM/CAMTests/TestFanucPost.py index d1f320d3f5..12724e7227 100644 --- a/src/Mod/CAM/CAMTests/TestFanucPost.py +++ b/src/Mod/CAM/CAMTests/TestFanucPost.py @@ -95,7 +95,7 @@ class TestFanucPost(PathTestUtils.PathTestBase): # Test without header expected = """% (BEGIN PREAMBLE) -G17 G54 G40 G49 G80 G90 +G17 G54 G40 G49 G80 G90 G94 G21 (BEGIN OPERATION: TC: DEFAULT TOOL) (MACHINE UNITS: MM/MIN) @@ -130,7 +130,7 @@ M30 # test without comments expected = """% -G17 G54 G40 G49 G80 G90 +G17 G54 G40 G49 G80 G90 G94 G21 M05 G28 G91 Z0 @@ -168,7 +168,7 @@ M30 # Test without header expected = """% (BEGIN PREAMBLE) -G17 G54 G40 G49 G80 G90 +G17 G54 G40 G49 G80 G90 G94 G21 (BEGIN OPERATION: TC: DEFAULT TOOL) (MACHINE UNITS: MM/MIN) @@ -207,7 +207,7 @@ M30 # test without comments expected = """% -G17 G54 G40 G49 G80 G90 +G17 G54 G40 G49 G80 G90 G94 G21 M05 G28 G91 Z0 @@ -336,18 +336,16 @@ M30 Test threading using drill cycle converted to tapping """ - self.tool_controller.Tool.ShapeName = "tap" + self.tool_controller.Tool.ShapeType = "tap" c = Path.Command("G0 X10 Y10") - c2 = Path.Command("G81 X10 Y10 Z-10 R20 F1 P1 Q1") + c2 = Path.Command("G84 X10 Y10 Z-10 R20 F1 P1 Q1") self.profile_op.Path = Path.Path([c, c2]) self.job.PostProcessorArgs = "--no-header --no-show-editor" gcode = self.post.export()[0][1] self.assertEqual(gcode.splitlines()[18], "G0 X10.000 Y10.000") - self.assertEqual(gcode.splitlines()[19], "G95") - self.assertEqual(gcode.splitlines()[20], "M29 S1000") - self.assertEqual(gcode.splitlines()[21], "G84 Z-10.000 R20.000 F1.000 P1.000 Q1.000") - self.assertEqual(gcode.splitlines()[22], "G80") - self.assertEqual(gcode.splitlines()[23], "G94") + self.assertEqual(gcode.splitlines()[19], "M29 S1000") + self.assertEqual(gcode.splitlines()[20], "G84 Z-10.000 R20.000 F1000.000 P1.000 Q1.000") + self.assertEqual(gcode.splitlines()[21], "G80") def test_comment(self): """