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 404c6e751b..66614d4ae0 100644 --- a/src/Mod/CAM/Path/Post/UtilsParse.py +++ b/src/Mod/CAM/Path/Post/UtilsParse.py @@ -28,6 +28,7 @@ # * * # *************************************************************************** +import math import re from typing import Any, Callable, Dict, List, Tuple, Union @@ -208,13 +209,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)) @@ -225,6 +232,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.""" @@ -243,13 +251,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 @@ -261,6 +276,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) @@ -269,6 +304,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.""" @@ -280,6 +316,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.""" @@ -291,6 +328,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.""" @@ -309,6 +347,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.""" @@ -324,13 +363,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 @@ -343,6 +388,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.""" @@ -662,7 +708,25 @@ def parse_a_path(values: Values, gcode: Gcode, pathobj) -> None: swap_tool_change_order = False if "TOOL_BEFORE_CHANGE" in values and values["TOOL_BEFORE_CHANGE"]: swap_tool_change_order = True - current_location.update(Path.Command("G0", {"X": -1, "Y": -1, "Z": -1, "F": 0.0}).Parameters) + 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: @@ -692,6 +756,7 @@ def parse_a_path(values: Values, gcode: Gcode, pathobj) -> None: command, parameter, c.Parameters[parameter], + c.Parameters, current_location, ) if parameter_value: