From d9561a8574a744cb5fbb5436d9ab0826ca9e70bf Mon Sep 17 00:00:00 2001 From: Lawrence Woestman Date: Mon, 10 Feb 2025 10:35:25 -0800 Subject: [PATCH] CAM: Fixed F parameter handling for only rotary axes with tests --- .../CAMTests/TestRefactoredTestPostGCodes.py | 420 ++++++++++++++++++ src/Mod/CAM/Path/Post/UtilsParse.py | 78 +++- 2 files changed, 494 insertions(+), 4 deletions(-) diff --git a/src/Mod/CAM/CAMTests/TestRefactoredTestPostGCodes.py b/src/Mod/CAM/CAMTests/TestRefactoredTestPostGCodes.py index eae6809276..a583095712 100644 --- a/src/Mod/CAM/CAMTests/TestRefactoredTestPostGCodes.py +++ b/src/Mod/CAM/CAMTests/TestRefactoredTestPostGCodes.py @@ -1771,3 +1771,423 @@ G90 f"B{b_inches_output_expected} C{c_inches_output_expected}", "--inches", ) + + ############################################################################# + + def test19120(self): + """Test F parameter handling.""" + # + # Note that FreeCAD internal units are (mm or degrees) per second + # and postprocessor outputs are usually in (mm or degrees) per minute + # + axes_test_values = ( + # test using all possible axes, all same, then all changing + ( + [ + Path.Command("G1 X10 Y20 Z30 A40 B50 C60 U70 V80 W90 F1.23456"), + Path.Command("G1 X10 Y20 Z30 A40 B50 C60 U70 V80 W90 F1.23456"), + Path.Command("G1 X20 Y30 Z10 A70 B80 C90 U40 V50 W60 F1.23456"), + ], + """G90 +G21 +G54 +M6 T1 +G1 X10.000 Y20.000 Z30.000 A40.000 B50.000 C60.000 U70.000 V80.000 W90.000 F74.074 +G1 X10.000 Y20.000 Z30.000 A40.000 B50.000 C60.000 U70.000 V80.000 W90.000 F74.074 +G1 X20.000 Y30.000 Z10.000 A70.000 B80.000 C90.000 U40.000 V50.000 W60.000 F74.074 +""", + """G90 +G20 +G54 +M6 T1 +G1 X0.3937 Y0.7874 Z1.1811 A40.0000 B50.0000 C60.0000 U2.7559 V3.1496 W3.5433 F2.9163 +G1 X0.3937 Y0.7874 Z1.1811 A40.0000 B50.0000 C60.0000 U2.7559 V3.1496 W3.5433 F74.0736 +G1 X0.7874 Y1.1811 Z0.3937 A70.0000 B80.0000 C90.0000 U1.5748 V1.9685 W2.3622 F2.9163 +""", + ), + # test using all possible axes, just X changing + ( + [ + Path.Command("G1 X10 Y20 Z30 A40 B50 C60 U70 V80 W90 F1.23456"), + Path.Command("G1 X20 Y20 Z30 A40 B50 C60 U70 V80 W90 F1.23456"), + ], + """G90 +G21 +G54 +M6 T1 +G1 X10.000 Y20.000 Z30.000 A40.000 B50.000 C60.000 U70.000 V80.000 W90.000 F74.074 +G1 X20.000 Y20.000 Z30.000 A40.000 B50.000 C60.000 U70.000 V80.000 W90.000 F74.074 +""", + """G90 +G20 +G54 +M6 T1 +G1 X0.3937 Y0.7874 Z1.1811 A40.0000 B50.0000 C60.0000 U2.7559 V3.1496 W3.5433 F2.9163 +G1 X0.7874 Y0.7874 Z1.1811 A40.0000 B50.0000 C60.0000 U2.7559 V3.1496 W3.5433 F2.9163 +""", + ), + # test using all possible axes, just Y changing + ( + [ + Path.Command("G1 X10 Y20 Z30 A40 B50 C60 U70 V80 W90 F1.23456"), + Path.Command("G1 X10 Y30 Z30 A40 B50 C60 U70 V80 W90 F1.23456"), + ], + """G90 +G21 +G54 +M6 T1 +G1 X10.000 Y20.000 Z30.000 A40.000 B50.000 C60.000 U70.000 V80.000 W90.000 F74.074 +G1 X10.000 Y30.000 Z30.000 A40.000 B50.000 C60.000 U70.000 V80.000 W90.000 F74.074 +""", + """G90 +G20 +G54 +M6 T1 +G1 X0.3937 Y0.7874 Z1.1811 A40.0000 B50.0000 C60.0000 U2.7559 V3.1496 W3.5433 F2.9163 +G1 X0.3937 Y1.1811 Z1.1811 A40.0000 B50.0000 C60.0000 U2.7559 V3.1496 W3.5433 F2.9163 +""", + ), + # test using all possible axes, just Z changing + ( + [ + Path.Command("G1 X10 Y20 Z30 A40 B50 C60 U70 V80 W90 F1.23456"), + Path.Command("G1 X10 Y20 Z10 A40 B50 C60 U70 V80 W90 F1.23456"), + ], + """G90 +G21 +G54 +M6 T1 +G1 X10.000 Y20.000 Z30.000 A40.000 B50.000 C60.000 U70.000 V80.000 W90.000 F74.074 +G1 X10.000 Y20.000 Z10.000 A40.000 B50.000 C60.000 U70.000 V80.000 W90.000 F74.074 +""", + """G90 +G20 +G54 +M6 T1 +G1 X0.3937 Y0.7874 Z1.1811 A40.0000 B50.0000 C60.0000 U2.7559 V3.1496 W3.5433 F2.9163 +G1 X0.3937 Y0.7874 Z0.3937 A40.0000 B50.0000 C60.0000 U2.7559 V3.1496 W3.5433 F2.9163 +""", + ), + # test using all possible axes, just A changing + ( + [ + Path.Command("G1 X10 Y20 Z30 A40 B50 C60 U70 V80 W90 F1.23456"), + Path.Command("G1 X10 Y20 Z30 A70 B50 C60 U70 V80 W90 F1.23456"), + ], + """G90 +G21 +G54 +M6 T1 +G1 X10.000 Y20.000 Z30.000 A40.000 B50.000 C60.000 U70.000 V80.000 W90.000 F74.074 +G1 X10.000 Y20.000 Z30.000 A70.000 B50.000 C60.000 U70.000 V80.000 W90.000 F74.074 +""", + """G90 +G20 +G54 +M6 T1 +G1 X0.3937 Y0.7874 Z1.1811 A40.0000 B50.0000 C60.0000 U2.7559 V3.1496 W3.5433 F2.9163 +G1 X0.3937 Y0.7874 Z1.1811 A70.0000 B50.0000 C60.0000 U2.7559 V3.1496 W3.5433 F74.0736 +""", + ), + # test using all possible axes, just B changing + ( + [ + Path.Command("G1 X10 Y20 Z30 A40 B50 C60 U70 V80 W90 F1.23456"), + Path.Command("G1 X10 Y20 Z30 A40 B80 C60 U70 V80 W90 F1.23456"), + ], + """G90 +G21 +G54 +M6 T1 +G1 X10.000 Y20.000 Z30.000 A40.000 B50.000 C60.000 U70.000 V80.000 W90.000 F74.074 +G1 X10.000 Y20.000 Z30.000 A40.000 B80.000 C60.000 U70.000 V80.000 W90.000 F74.074 +""", + """G90 +G20 +G54 +M6 T1 +G1 X0.3937 Y0.7874 Z1.1811 A40.0000 B50.0000 C60.0000 U2.7559 V3.1496 W3.5433 F2.9163 +G1 X0.3937 Y0.7874 Z1.1811 A40.0000 B80.0000 C60.0000 U2.7559 V3.1496 W3.5433 F74.0736 +""", + ), + # test using all possible axes, just C changing + ( + [ + Path.Command("G1 X10 Y20 Z30 A40 B50 C60 U70 V80 W90 F1.23456"), + Path.Command("G1 X10 Y20 Z30 A40 B50 C90 U70 V80 W90 F1.23456"), + ], + """G90 +G21 +G54 +M6 T1 +G1 X10.000 Y20.000 Z30.000 A40.000 B50.000 C60.000 U70.000 V80.000 W90.000 F74.074 +G1 X10.000 Y20.000 Z30.000 A40.000 B50.000 C90.000 U70.000 V80.000 W90.000 F74.074 +""", + """G90 +G20 +G54 +M6 T1 +G1 X0.3937 Y0.7874 Z1.1811 A40.0000 B50.0000 C60.0000 U2.7559 V3.1496 W3.5433 F2.9163 +G1 X0.3937 Y0.7874 Z1.1811 A40.0000 B50.0000 C90.0000 U2.7559 V3.1496 W3.5433 F74.0736 +""", + ), + # test using all possible axes, just U changing + ( + [ + Path.Command("G1 X10 Y20 Z30 A40 B50 C60 U70 V80 W90 F1.23456"), + Path.Command("G1 X10 Y20 Z30 A40 B50 C60 U40 V80 W90 F1.23456"), + ], + """G90 +G21 +G54 +M6 T1 +G1 X10.000 Y20.000 Z30.000 A40.000 B50.000 C60.000 U70.000 V80.000 W90.000 F74.074 +G1 X10.000 Y20.000 Z30.000 A40.000 B50.000 C60.000 U40.000 V80.000 W90.000 F74.074 +""", + """G90 +G20 +G54 +M6 T1 +G1 X0.3937 Y0.7874 Z1.1811 A40.0000 B50.0000 C60.0000 U2.7559 V3.1496 W3.5433 F2.9163 +G1 X0.3937 Y0.7874 Z1.1811 A40.0000 B50.0000 C60.0000 U1.5748 V3.1496 W3.5433 F2.9163 +""", + ), + # test using all possible axes, just V changing + ( + [ + Path.Command("G1 X10 Y20 Z30 A40 B50 C60 U70 V80 W90 F1.23456"), + Path.Command("G1 X10 Y20 Z30 A40 B50 C60 U70 V50 W90 F1.23456"), + ], + """G90 +G21 +G54 +M6 T1 +G1 X10.000 Y20.000 Z30.000 A40.000 B50.000 C60.000 U70.000 V80.000 W90.000 F74.074 +G1 X10.000 Y20.000 Z30.000 A40.000 B50.000 C60.000 U70.000 V50.000 W90.000 F74.074 +""", + """G90 +G20 +G54 +M6 T1 +G1 X0.3937 Y0.7874 Z1.1811 A40.0000 B50.0000 C60.0000 U2.7559 V3.1496 W3.5433 F2.9163 +G1 X0.3937 Y0.7874 Z1.1811 A40.0000 B50.0000 C60.0000 U2.7559 V1.9685 W3.5433 F2.9163 +""", + ), + # test using all possible axes, just W changing + ( + [ + Path.Command("G1 X10 Y20 Z30 A40 B50 C60 U70 V80 W90 F1.23456"), + Path.Command("G1 X10 Y20 Z30 A40 B50 C60 U70 V80 W60 F1.23456"), + ], + """G90 +G21 +G54 +M6 T1 +G1 X10.000 Y20.000 Z30.000 A40.000 B50.000 C60.000 U70.000 V80.000 W90.000 F74.074 +G1 X10.000 Y20.000 Z30.000 A40.000 B50.000 C60.000 U70.000 V80.000 W60.000 F74.074 +""", + """G90 +G20 +G54 +M6 T1 +G1 X0.3937 Y0.7874 Z1.1811 A40.0000 B50.0000 C60.0000 U2.7559 V3.1496 W3.5433 F2.9163 +G1 X0.3937 Y0.7874 Z1.1811 A40.0000 B50.0000 C60.0000 U2.7559 V3.1496 W2.3622 F2.9163 +""", + ), + # test using just XYZ, all same, then all changing + ( + [ + Path.Command("G1 X10 Y20 Z30 F1.23456"), + Path.Command("G1 X10 Y20 Z30 F1.23456"), + Path.Command("G1 X20 Y30 Z10 F1.23456"), + ], + """G90 +G21 +G54 +M6 T1 +G1 X10.000 Y20.000 Z30.000 F74.074 +G1 X10.000 Y20.000 Z30.000 F74.074 +G1 X20.000 Y30.000 Z10.000 F74.074 +""", + """G90 +G20 +G54 +M6 T1 +G1 X0.3937 Y0.7874 Z1.1811 F2.9163 +G1 X0.3937 Y0.7874 Z1.1811 F2.9163 +G1 X0.7874 Y1.1811 Z0.3937 F2.9163 +""", + ), + # test using just UVW, all same, then all changing + ( + [ + Path.Command("G1 U70 V80 W90 F1.23456"), + Path.Command("G1 U70 V80 W90 F1.23456"), + Path.Command("G1 U40 V50 W60 F1.23456"), + ], + """G90 +G21 +G54 +M6 T1 +G1 U70.000 V80.000 W90.000 F74.074 +G1 U70.000 V80.000 W90.000 F74.074 +G1 U40.000 V50.000 W60.000 F74.074 +""", + """G90 +G20 +G54 +M6 T1 +G1 U2.7559 V3.1496 W3.5433 F2.9163 +G1 U2.7559 V3.1496 W3.5433 F2.9163 +G1 U1.5748 V1.9685 W2.3622 F2.9163 +""", + ), + # test using just ABC, which should not convert the feed rate for --inches + ( + [ + Path.Command("G1 A40 B50 C60 F1.23456"), + Path.Command("G1 A40 B50 C60 F1.23456"), + Path.Command("G1 A70 B80 C90 F1.23456"), + ], + """G90 +G21 +G54 +M6 T1 +G1 A40.000 B50.000 C60.000 F74.074 +G1 A40.000 B50.000 C60.000 F74.074 +G1 A70.000 B80.000 C90.000 F74.074 +""", + """G90 +G20 +G54 +M6 T1 +G1 A40.0000 B50.0000 C60.0000 F74.0736 +G1 A40.0000 B50.0000 C60.0000 F74.0736 +G1 A70.0000 B80.0000 C90.0000 F74.0736 +""", + ), + # test using XYZ and ABC, all same, then all changing + ( + [ + Path.Command("G1 X10 Y20 Z30 A40 B50 C60 F1.23456"), + Path.Command("G1 X10 Y20 Z30 A40 B50 C60 F1.23456"), + Path.Command("G1 X20 Y30 Z10 A70 B80 C90 F1.23456"), + ], + """G90 +G21 +G54 +M6 T1 +G1 X10.000 Y20.000 Z30.000 A40.000 B50.000 C60.000 F74.074 +G1 X10.000 Y20.000 Z30.000 A40.000 B50.000 C60.000 F74.074 +G1 X20.000 Y30.000 Z10.000 A70.000 B80.000 C90.000 F74.074 +""", + """G90 +G20 +G54 +M6 T1 +G1 X0.3937 Y0.7874 Z1.1811 A40.0000 B50.0000 C60.0000 F2.9163 +G1 X0.3937 Y0.7874 Z1.1811 A40.0000 B50.0000 C60.0000 F74.0736 +G1 X0.7874 Y1.1811 Z0.3937 A70.0000 B80.0000 C90.0000 F2.9163 +""", + ), + # test using ABC and UVW, all same, then all changing + ( + [ + Path.Command("G1 A40 B50 C60 U70 V80 W90 F1.23456"), + Path.Command("G1 A40 B50 C60 U70 V80 W90 F1.23456"), + Path.Command("G1 A70 B80 C90 U40 V50 W60 F1.23456"), + ], + """G90 +G21 +G54 +M6 T1 +G1 A40.000 B50.000 C60.000 U70.000 V80.000 W90.000 F74.074 +G1 A40.000 B50.000 C60.000 U70.000 V80.000 W90.000 F74.074 +G1 A70.000 B80.000 C90.000 U40.000 V50.000 W60.000 F74.074 +""", + """G90 +G20 +G54 +M6 T1 +G1 A40.0000 B50.0000 C60.0000 U2.7559 V3.1496 W3.5433 F2.9163 +G1 A40.0000 B50.0000 C60.0000 U2.7559 V3.1496 W3.5433 F74.0736 +G1 A70.0000 B80.0000 C90.0000 U1.5748 V1.9685 W2.3622 F2.9163 +""", + ), + # test using X and A only, both same, then both changing + ( + [ + Path.Command("G1 X10 A40 F1.23456"), + Path.Command("G1 X10 A40 F1.23456"), + Path.Command("G1 X20 A70 F1.23456"), + ], + """G90 +G21 +G54 +M6 T1 +G1 X10.000 A40.000 F74.074 +G1 X10.000 A40.000 F74.074 +G1 X20.000 A70.000 F74.074 +""", + """G90 +G20 +G54 +M6 T1 +G1 X0.3937 A40.0000 F2.9163 +G1 X0.3937 A40.0000 F74.0736 +G1 X0.7874 A70.0000 F2.9163 +""", + ), + # test using A and U only, both same, then both changing + ( + [ + Path.Command("G1 A40 U70 F1.23456"), + Path.Command("G1 A40 U70 F1.23456"), + Path.Command("G1 A70 U40 F1.23456"), + ], + """G90 +G21 +G54 +M6 T1 +G1 A40.000 U70.000 F74.074 +G1 A40.000 U70.000 F74.074 +G1 A70.000 U40.000 F74.074 +""", + """G90 +G20 +G54 +M6 T1 +G1 A40.0000 U2.7559 F2.9163 +G1 A40.0000 U2.7559 F74.0736 +G1 A70.0000 U1.5748 F2.9163 +""", + ), + # test using A only, which should not convert the feed rate for --inches + ( + [ + Path.Command("G1 A40 F1.23456"), + Path.Command("G1 A40 F1.23456"), + Path.Command("G1 A70 F1.23456"), + ], + """G90 +G21 +G54 +M6 T1 +G1 A40.000 F74.074 +G1 A40.000 F74.074 +G1 A70.000 F74.074 +""", + """G90 +G20 +G54 +M6 T1 +G1 A40.0000 F74.0736 +G1 A40.0000 F74.0736 +G1 A70.0000 F74.0736 +""", + ), + ) + for input_value, metric_output_expected, inches_output_expected in axes_test_values: + with self.subTest("F feed speed test", input_value=input_value): + self.multi_compare(input_value, metric_output_expected, "") + self.multi_compare(input_value, inches_output_expected, "--inches") diff --git a/src/Mod/CAM/Path/Post/UtilsParse.py b/src/Mod/CAM/Path/Post/UtilsParse.py index bcd3963e16..f752eb49cf 100644 --- a/src/Mod/CAM/Path/Post/UtilsParse.py +++ b/src/Mod/CAM/Path/Post/UtilsParse.py @@ -27,6 +27,7 @@ # * * # *************************************************************************** +import math import re from typing import Any, Callable, Dict, List, Tuple, Union @@ -207,13 +208,19 @@ def default_axis_parameter( command: str, # pylint: disable=unused-argument param: str, param_value: PathParameter, + parameters: PathParameters, # pylint: disable=unused-argument current_location: PathParameters, ) -> str: """Process an axis parameter.""" + # + # used to compare two floating point numbers for "close-enough equality" + # + epsilon: float = 0.00001 + if ( not values["OUTPUT_DOUBLES"] and param in current_location - and current_location[param] == param_value + and math.fabs(current_location[param] - param_value) < epsilon ): return "" return format_for_axis(values, Units.Quantity(param_value, Units.Length)) @@ -224,6 +231,7 @@ def default_D_parameter( command: str, param: str, # pylint: disable=unused-argument param_value: PathParameter, + parameters: PathParameters, # pylint: disable=unused-argument current_location: PathParameters, # pylint: disable=unused-argument ) -> str: """Process the D parameter.""" @@ -242,13 +250,20 @@ def default_F_parameter( command: str, param: str, param_value: PathParameter, + parameters: PathParameters, current_location: PathParameters, ) -> str: """Process the F parameter.""" + # + # used to compare two floating point numbers for "close-enough equality" + # + epsilon: float = 0.00001 + found: bool + if ( not values["OUTPUT_DOUBLES"] and param in current_location - and current_location[param] == param_value + and math.fabs(current_location[param] - param_value) < epsilon ): return "" # Many posts don't use rapid speeds, but eventually @@ -260,6 +275,26 @@ def default_F_parameter( feed = Units.Quantity(param_value, Units.Velocity) if feed.getValueAs(values["UNIT_SPEED_FORMAT"]) <= 0.0: return "" + # if any of X, Y, Z, U, V, or W are in the parameters + # and any of their values is different than where the device currently should be + # then feed is in linear units + found = False + for key in ("X", "Y", "Z", "U", "V", "W"): + if key in parameters and math.fabs(current_location[key] - parameters[key]) > epsilon: + found = True + if found: + return format_for_feed(values, feed) + # else if any of A, B, or C are in the paramters, the feed is in degrees, + # which should not be converted when in --inches mode + found = False + for key in ("A", "B", "C"): + if key in parameters: + found = True + if found: + # converting from degrees per second to degrees per minute as well + return format(float(feed * 60.0), f'.{str(values["FEED_PRECISION"])}f') + # which leaves none of X, Y, Z, U, V, W, A, B, C, + # which should not be valid but return a converted value just in case return format_for_feed(values, feed) @@ -268,6 +303,7 @@ def default_int_parameter( command: str, # pylint: disable=unused-argument param: str, # pylint: disable=unused-argument param_value: PathParameter, + parameters: PathParameters, # pylint: disable=unused-argument current_location: PathParameters, # pylint: disable=unused-argument ) -> str: """Process a parameter that is treated like an integer.""" @@ -279,6 +315,7 @@ def default_length_parameter( command: str, # pylint: disable=unused-argument param: str, # pylint: disable=unused-argument param_value: PathParameter, + parameters: PathParameters, # pylint: disable=unused-argument current_location: PathParameters, # pylint: disable=unused-argument ) -> str: """Process a parameter that is treated like a length.""" @@ -290,6 +327,7 @@ def default_P_parameter( command: str, param: str, # pylint: disable=unused-argument param_value: PathParameter, + parameters: PathParameters, # pylint: disable=unused-argument current_location: PathParameters, # pylint: disable=unused-argument ) -> str: """Process the P parameter.""" @@ -308,6 +346,7 @@ def default_Q_parameter( command: str, param: str, # pylint: disable=unused-argument param_value: PathParameter, + parameters: PathParameters, # pylint: disable=unused-argument current_location: PathParameters, # pylint: disable=unused-argument ) -> str: """Process the Q parameter.""" @@ -323,13 +362,19 @@ def default_rotary_parameter( command: str, # pylint: disable=unused-argument param: str, param_value: PathParameter, + parameters: PathParameters, # pylint: disable=unused-argument current_location: PathParameters, ) -> str: """Process a rotarty parameter (such as A, B, and C).""" + # + # used to compare two floating point numbers for "close-enough equality" + # + epsilon: float = 0.00001 + if ( not values["OUTPUT_DOUBLES"] and param in current_location - and current_location[param] == param_value + and math.fabs(current_location[param] - param_value) < epsilon ): return "" # unlike other axis, rotary axis such as A, B, and C are always in degrees @@ -342,6 +387,7 @@ def default_S_parameter( command: str, # pylint: disable=unused-argument param: str, # pylint: disable=unused-argument param_value: PathParameter, + parameters: PathParameters, # pylint: disable=unused-argument current_location: PathParameters, # pylint: disable=unused-argument ) -> str: """Process the S parameter.""" @@ -656,7 +702,30 @@ def parse_a_path(values: Values, gcode: Gcode, pathobj) -> None: parameter: str parameter_value: str - current_location.update(Path.Command("G0", {"X": -1, "Y": -1, "Z": -1, "F": 0.0}).Parameters) + # Check to see if values["TOOL_BEFORE_CHANGE"] is set and value is true + # doing it here to reduce the number of times it is checked + swap_tool_change_order = False + if "TOOL_BEFORE_CHANGE" in values and values["TOOL_BEFORE_CHANGE"]: + swap_tool_change_order = True + current_location.update( + # the goal is to have initial values that aren't likely to match + # any "real" first parameter values + Path.Command( + "G0", + { + "X": 123456789.0, + "Y": 123456789.0, + "Z": 123456789.0, + "U": 123456789.0, + "V": 123456789.0, + "W": 123456789.0, + "A": 123456789.0, + "B": 123456789.0, + "C": 123456789.0, + "F": 123456789.0, + }, + ).Parameters + ) adaptive_op_variables = determine_adaptive_op(values, pathobj) for c in pathobj.Path.Commands: @@ -686,6 +755,7 @@ def parse_a_path(values: Values, gcode: Gcode, pathobj) -> None: command, parameter, c.Parameters[parameter], + c.Parameters, current_location, ) if parameter_value: