From 087d3934499b18619136b789d1da6a8dd82dd9ec Mon Sep 17 00:00:00 2001 From: Lawrence Woestman Date: Mon, 7 Apr 2025 10:31:38 -0700 Subject: [PATCH] CAM: added three command line arguments, with tests --- .../CAM/CAMTests/TestRefactoredTestPost.py | 448 ++++++++++++++++++ src/Mod/CAM/Path/Post/Command.py | 38 +- src/Mod/CAM/Path/Post/UtilsArguments.py | 66 ++- src/Mod/CAM/Path/Post/UtilsExport.py | 139 +++--- src/Mod/CAM/Path/Post/UtilsParse.py | 64 +-- .../CAM/Path/Post/scripts/snapmaker_post.py | 36 +- 6 files changed, 669 insertions(+), 122 deletions(-) diff --git a/src/Mod/CAM/CAMTests/TestRefactoredTestPost.py b/src/Mod/CAM/CAMTests/TestRefactoredTestPost.py index d58ad8a0a9..4cd48772ec 100644 --- a/src/Mod/CAM/CAMTests/TestRefactoredTestPost.py +++ b/src/Mod/CAM/CAMTests/TestRefactoredTestPost.py @@ -20,13 +20,19 @@ # * USA * # * * # *************************************************************************** +from os import linesep, path, remove +import tempfile +from unittest.mock import mock_open, patch import FreeCAD import Path import CAMTests.PathTestUtils as PathTestUtils +from Path.Post.Command import CommandPathPost from Path.Post.Processor import PostProcessorFactory +from Path.Post.Utils import FilenameGenerator +from PySide.QtCore import QT_TRANSLATE_NOOP # type: ignore Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) Path.Log.trackModule(Path.Log.thisModule()) @@ -109,6 +115,7 @@ class TestRefactoredTestPost(PathTestUtils.PathTestBase): def multi_compare(self, path, expected, args, debug=False): """Perform a test with multiple lines of gcode comparison.""" nl = "\n" + self.job.PostProcessorArgs = args # replace the original path (that came with the job and operation) with our path self.profile_op.Path = Path.Path(path) @@ -422,6 +429,436 @@ G54 ############################################################################# + def test00135(self) -> None: + """Test enabling and disabling coolant.""" + args: str + expected: str + gcode: str + nl = "\n" + save_CoolantMode = self.profile_op.CoolantMode + + c = Path.Command("G0 X10 Y20 Z30") + self.profile_op.Path = Path.Path([c]) + + # Test Flood coolant enabled + self.profile_op.CoolantMode = "Flood" + expected = """(Begin preamble) +G90 +G21 +(Begin operation) +G54 +(Finish operation) +(Begin operation) +(TC: Default Tool) +(Begin toolchange) +(M6 T1) +(Finish operation) +(Begin operation) +(Coolant On: Flood) +M8 +G0 X10.000 Y20.000 Z30.000 +(Finish operation) +(Coolant Off: Flood) +M9 +(Begin postamble) +""" + self.job.PostProcessorArgs = "--enable_coolant --comments" + gcode = self.post.export()[0][1] + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + # Test Mist coolant enabled + self.profile_op.CoolantMode = "Mist" + expected = """(Begin preamble) +G90 +G21 +(Begin operation) +G54 +(Finish operation) +(Begin operation) +(TC: Default Tool) +(Begin toolchange) +(M6 T1) +(Finish operation) +(Begin operation) +(Coolant On: Mist) +M7 +G0 X10.000 Y20.000 Z30.000 +(Finish operation) +(Coolant Off: Mist) +M9 +(Begin postamble) +""" + self.job.PostProcessorArgs = "--enable_coolant --comments" + gcode = self.post.export()[0][1] + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + # Test None coolant enabled with CoolantMode property + self.profile_op.CoolantMode = "None" + expected = """(Begin preamble) +G90 +G21 +(Begin operation) +G54 +(Finish operation) +(Begin operation) +(TC: Default Tool) +(Begin toolchange) +(M6 T1) +(Finish operation) +(Begin operation) +G0 X10.000 Y20.000 Z30.000 +(Finish operation) +(Begin postamble) +""" + self.job.PostProcessorArgs = "--enable_coolant --comments" + gcode = self.post.export()[0][1] + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + # Test Flood coolant disabled + self.profile_op.CoolantMode = "Flood" + expected = """(Begin preamble) +G90 +G21 +(Begin operation) +G54 +(Finish operation) +(Begin operation) +(TC: Default Tool) +(Begin toolchange) +(M6 T1) +(Finish operation) +(Begin operation) +G0 X10.000 Y20.000 Z30.000 +(Finish operation) +(Begin postamble) +""" + self.job.PostProcessorArgs = "--disable_coolant --comments" + gcode = self.post.export()[0][1] + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + # Test Mist coolant disabled + self.profile_op.CoolantMode = "Mist" + expected = """(Begin preamble) +G90 +G21 +(Begin operation) +G54 +(Finish operation) +(Begin operation) +(TC: Default Tool) +(Begin toolchange) +(M6 T1) +(Finish operation) +(Begin operation) +G0 X10.000 Y20.000 Z30.000 +(Finish operation) +(Begin postamble) +""" + self.job.PostProcessorArgs = "--disable_coolant --comments" + gcode = self.post.export()[0][1] + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + # Test None coolant disabled with CoolantMode property + self.profile_op.CoolantMode = "None" + expected = """(Begin preamble) +G90 +G21 +(Begin operation) +G54 +(Finish operation) +(Begin operation) +(TC: Default Tool) +(Begin toolchange) +(M6 T1) +(Finish operation) +(Begin operation) +G0 X10.000 Y20.000 Z30.000 +(Finish operation) +(Begin postamble) +""" + self.job.PostProcessorArgs = "--disable_coolant --comments" + gcode = self.post.export()[0][1] + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + # Test Flood coolant configured but no coolant argument (default) + self.profile_op.CoolantMode = "Flood" + expected = """(Begin preamble) +G90 +G21 +(Begin operation) +G54 +(Finish operation) +(Begin operation) +(TC: Default Tool) +(Begin toolchange) +(M6 T1) +(Finish operation) +(Begin operation) +G0 X10.000 Y20.000 Z30.000 +(Finish operation) +(Begin postamble) +""" + self.job.PostProcessorArgs = "--comments" + gcode = self.post.export()[0][1] + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + # Test coolant enabled without a CoolantMode property + + self.profile_op.removeProperty("CoolantMode") + + expected = """(Begin preamble) +G90 +G21 +(Begin operation) +G54 +(Finish operation) +(Begin operation) +(TC: Default Tool) +(Begin toolchange) +(M6 T1) +(Finish operation) +(Begin operation) +G0 X10.000 Y20.000 Z30.000 +(Finish operation) +(Begin postamble) +""" + self.job.PostProcessorArgs = "--enable_coolant --comments" + gcode = self.post.export()[0][1] + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + # Test coolant disabled without a CoolantMode property + expected = """(Begin preamble) +G90 +G21 +(Begin operation) +G54 +(Finish operation) +(Begin operation) +(TC: Default Tool) +(Begin toolchange) +(M6 T1) +(Finish operation) +(Begin operation) +G0 X10.000 Y20.000 Z30.000 +(Finish operation) +(Begin postamble) +""" + self.job.PostProcessorArgs = "--disable_coolant --comments" + gcode = self.post.export()[0][1] + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + # re-create the original CoolantMode property + self.profile_op.addProperty( + "App::PropertyEnumeration", + "CoolantMode", + "Path", + QT_TRANSLATE_NOOP("App::Property", "Coolant option for this operation"), + ) + self.profile_op.CoolantMode = ["None", "Flood", "Mist"] + self.profile_op.CoolantMode = save_CoolantMode + + ############################################################################# + + def test00137(self) -> None: + """Test enabling/disabling machine specific commands.""" + + # test with machine specific commands enabled + self.multi_compare( + "(MC_RUN_COMMAND: blah)", + """(Begin preamble) +G90 +G21 +(Begin operation) +G54 +(Finish operation) +(Begin operation) +(TC: Default Tool) +(Begin toolchange) +(M6 T1) +(Finish operation) +(Begin operation) +(MC_RUN_COMMAND: blah) +blah +(Finish operation) +(Begin postamble) +""", + "--enable_machine_specific_commands --comments", + ) + # test with machine specific commands disabled + self.multi_compare( + "(MC_RUN_COMMAND: blah)", + """(Begin preamble) +G90 +G21 +(Begin operation) +G54 +(Finish operation) +(Begin operation) +(TC: Default Tool) +(Begin toolchange) +(M6 T1) +(Finish operation) +(Begin operation) +(MC_RUN_COMMAND: blah) +(Finish operation) +(Begin postamble) +""", + "--disable_machine_specific_commands --comments", + ) + # test with machine specific commands default + self.multi_compare( + "(MC_RUN_COMMAND: blah)", + """(Begin preamble) +G90 +G21 +(Begin operation) +G54 +(Finish operation) +(Begin operation) +(TC: Default Tool) +(Begin toolchange) +(M6 T1) +(Finish operation) +(Begin operation) +(MC_RUN_COMMAND: blah) +(Finish operation) +(Begin postamble) +""", + "--comments", + ) + # test with odd characters and spaces in the machine specific command + self.multi_compare( + "(MC_RUN_COMMAND: These are odd characters:!@#$%^&*?/)", + """(Begin preamble) +G90 +G21 +(Begin operation) +G54 +(Finish operation) +(Begin operation) +(TC: Default Tool) +(Begin toolchange) +(M6 T1) +(Finish operation) +(Begin operation) +(MC_RUN_COMMAND: These are odd characters:!@#$%^&*?/) +These are odd characters:!@#$%^&*?/ +(Finish operation) +(Begin postamble) +""", + "--enable_machine_specific_commands --comments", + ) + + ############################################################################# + + def test00138(self) -> None: + """Test end of line characters.""" + + class MockWriter: + def __init__(self): + self.contents = "" + + def write(self, data): + self.contents = data + + writer = MockWriter() + opener = mock_open() + opener.return_value.write = writer.write + + output_file_pattern = path.join(tempfile.gettempdir(), "test_postprocessor_write.nc") + self.job.PostProcessorOutputFile = output_file_pattern + Path.Preferences.setOutputFileDefaults(output_file_pattern, "Append Unique ID on conflict") + policy = Path.Preferences.defaultOutputPolicy() + generator = FilenameGenerator(job=self.job) + generated_filename = generator.generate_filenames() + generator.set_subpartname("") + fname = next(generated_filename) + + self.profile_op.Path = Path.Path([]) + + # Test with whatever end-of-line characters the system running the test happens to use + expected = """G90 +G21 +G54 +""" + self.job.PostProcessorArgs = "" + gcode = self.post.export()[0][1] + self.assertEqual(gcode, expected) + # also test what is written to a mock file + with patch("builtins.open", opener) as m: + CommandPathPost._write_file(self, fname, gcode, policy) + if m.call_args.kwargs["newline"] is None: + mocked_output = writer.contents.replace("\n", linesep) + else: + mocked_output = writer.contents + expected = expected.replace("\n", linesep) + self.assertEqual(expected, mocked_output) + + # Test with a new line + expected = "\n\nG90\nG21\nG54\n" + self.job.PostProcessorArgs = "--end_of_line_characters='\n'" + gcode = self.post.export()[0][1] + self.assertEqual(gcode, expected) + # also test what is written to a mock file + with patch("builtins.open", opener) as m: + CommandPathPost._write_file(self, fname, gcode, policy) + if m.call_args.kwargs["newline"] is None: + mocked_output = writer.contents.replace("\n", linesep) + else: + mocked_output = writer.contents + expected = expected[2:] + self.assertEqual(expected, mocked_output) + + # Test with a carriage return followed by a new line + expected = "G90\r\nG21\r\nG54\r\n" + self.job.PostProcessorArgs = "--end_of_line_characters='\r\n'" + gcode = self.post.export()[0][1] + self.assertEqual(gcode, expected) + # also test what is written to a mock file + with patch("builtins.open", opener) as m: + CommandPathPost._write_file(self, fname, gcode, policy) + if m.call_args.kwargs["newline"] is None: + mocked_output = writer.contents.replace("\n", linesep) + else: + mocked_output = writer.contents + self.assertEqual(expected, mocked_output) + + # Test with a carriage return + expected = "G90\rG21\rG54\r" + self.job.PostProcessorArgs = "--end_of_line_characters='\r'" + gcode = self.post.export()[0][1] + self.assertEqual(gcode, expected) + # also test what is written to a mock file + with patch("builtins.open", opener) as m: + CommandPathPost._write_file(self, fname, gcode, policy) + if m.call_args.kwargs["newline"] is None: + mocked_output = writer.contents.replace("\n", linesep) + else: + mocked_output = writer.contents + self.assertEqual(expected, mocked_output) + + # Test writing a mock file with a zero-length string for gcode + expected = "" + gcode = "" + with patch("builtins.open", opener) as m: + CommandPathPost._write_file(self, fname, gcode, policy) + if m.call_args.kwargs["newline"] is None: + mocked_output = writer.contents.replace("\n", linesep) + else: + mocked_output = writer.contents + self.assertEqual(expected, mocked_output) + + ############################################################################# + def test00140(self): """Test feed-precision.""" nl = "\n" @@ -606,6 +1043,17 @@ G54 --no-comments Suppress comment output --comment_symbol COMMENT_SYMBOL The character used to start a comment, default is "(" + --enable_coolant Enable coolant + --disable_coolant Disable coolant (default) + --enable_machine_specific_commands + Enable machine specific commands of the form + (MC_RUN_COMMAND: blah) + --disable_machine_specific_commands + Disable machine specific commands (default) + --end_of_line_characters END_OF_LINE_CHARACTERS + The character(s) to use at the end of each line in the + output file, default is whatever the system uses, may + also use '\\n', '\\r', or '\\r\\n' --feed-precision FEED_PRECISION Number of digits of precision for feed rate, default is 3 diff --git a/src/Mod/CAM/Path/Post/Command.py b/src/Mod/CAM/Path/Post/Command.py index 2f5f123c91..fc9dfd66da 100644 --- a/src/Mod/CAM/Path/Post/Command.py +++ b/src/Mod/CAM/Path/Post/Command.py @@ -122,6 +122,34 @@ class CommandPathPost: return self.candidate is not None def _write_file(self, filename, gcode, policy): + # + # Up to this point the postprocessors have been using "\n" as the end-of-line + # characters in the gcode and using the process of writing out the file as a way + # to convert the "\n" into whatever end-of-line characters match the system + # running the postprocessor. This can be a problem if the controller which will + # run the gcode doesn't like the same end-of-line characters as the system that + # ran the postprocessor to generate the gcode. + # The refactored code base now allows for four possible types of end-of-line + # characters in the gcode. + # + if len(gcode) > 1 and gcode[0:2] == "\n\n": + # The gcode shouldn't normally start with "\n\n". + # This means that the gcode contains "\n" as the end-of-line characters and + # that the gcode should be written out exactly that way. + newline_handling = "" + gcode = gcode[2:] + elif "\r" in gcode: + # Write out the gcode with whatever end-of-line characters it already has, + # presumably either "\r" or "\r\n". + newline_handling = "" + else: + # The gcode is assumed to contain "\n" as the end-of-line characters (if + # there are any end-of-line characters in the gcode). This case also + # handles a zero-length gcode string. + # Write out the gcode but convert "\n" to whatever the system uses. + # This is also backwards compatible with the "previous" way of doing things. + newline_handling = None + if policy == "Open File Dialog": dlg = QtGui.QFileDialog() dlg.setFileMode(QtGui.QFileDialog.FileMode.AnyFile) @@ -131,7 +159,7 @@ class CommandPathPost: if dlg.exec_(): filename = dlg.selectedFiles()[0] Path.Log.debug(filename) - with open(filename, "w") as f: + with open(filename, "w", encoding="utf-8", newline=newline_handling) as f: f.write(gcode) else: return @@ -140,7 +168,7 @@ class CommandPathPost: while os.path.isfile(filename): base, ext = os.path.splitext(filename) filename = f"{base}-1{ext}" - with open(filename, "w") as f: + with open(filename, "w", encoding="utf-8", newline=newline_handling) as f: f.write(gcode) elif policy == "Open File Dialog on conflict": @@ -153,16 +181,16 @@ class CommandPathPost: if dlg.exec_(): filename = dlg.selectedFiles()[0] Path.Log.debug(filename) - with open(filename, "w") as f: + with open(filename, "w", encoding="utf-8", newline=newline_handling) as f: f.write(gcode) else: return else: - with open(filename, "w") as f: + with open(filename, "w", encoding="utf-8", newline=newline_handling) as f: f.write(gcode) else: # Overwrite - with open(filename, "w") as f: + with open(filename, "w", encoding="utf-8", newline=newline_handling) as f: f.write(gcode) FreeCAD.Console.PrintMessage(f"File written to {filename}\n") diff --git a/src/Mod/CAM/Path/Post/UtilsArguments.py b/src/Mod/CAM/Path/Post/UtilsArguments.py index 94314bc38e..924d1f4084 100644 --- a/src/Mod/CAM/Path/Post/UtilsArguments.py +++ b/src/Mod/CAM/Path/Post/UtilsArguments.py @@ -5,7 +5,7 @@ # * Copyright (c) 2015 Dan Falck * # * Copyright (c) 2018, 2019 Gauthier Briere * # * Copyright (c) 2019, 2020 Schildkroet * -# * Copyright (c) 2022 Larry Woestman * +# * Copyright (c) 2022-2025 Larry Woestman * # * * # * This file is part of the FreeCAD CAx development system. * # * * @@ -74,6 +74,8 @@ def init_argument_defaults(argument_defaults: Dict[str, bool]) -> None: argument_defaults["axis-modal"] = False argument_defaults["bcnc"] = False argument_defaults["comments"] = True + argument_defaults["enable_coolant"] = False + argument_defaults["enable_machine_specific_commands"] = False argument_defaults["header"] = True argument_defaults["line-numbers"] = False argument_defaults["metric_inches"] = True @@ -95,6 +97,9 @@ def init_arguments_visible(arguments_visible: Dict[str, bool]) -> None: arguments_visible["command_space"] = False arguments_visible["comments"] = True arguments_visible["comment_symbol"] = False + arguments_visible["enable_coolant"] = False + arguments_visible["enable_machine_specific_commands"] = False + arguments_visible["end_of_line_characters"] = False arguments_visible["feed-precision"] = True arguments_visible["header"] = True arguments_visible["line-numbers"] = True @@ -212,6 +217,35 @@ def init_shared_arguments( "--comment_symbol", help=help_message, ) + add_flag_type_arguments( + shared, + argument_defaults["enable_coolant"], + "--enable_coolant", + "--disable_coolant", + "Enable coolant", + "Disable coolant", + arguments_visible["enable_coolant"], + ) + add_flag_type_arguments( + shared, + argument_defaults["enable_machine_specific_commands"], + "--enable_machine_specific_commands", + "--disable_machine_specific_commands", + "Enable machine specific commands of the form (MC_RUN_COMMAND: blah)", + "Disable machine specific commands", + arguments_visible["enable_machine_specific_commands"], + ) + if arguments_visible["end_of_line_characters"]: + help_message = ( + "The character(s) to use at the end of each line in the output file, " + "default is whatever the system uses, may also use '\\n', '\\r', or '\\r\\n'" + ) + else: + help_message = argparse.SUPPRESS + shared.add_argument( + "--end_of_line_characters", + help=help_message, + ) if arguments_visible["feed-precision"]: help_message = ( f"Number of digits of precision for feed rate, " @@ -412,10 +446,11 @@ def init_shared_values(values: Values) -> None: # # By default the line ending characters of the output file(s) # are written to match the system that the postprocessor runs on. - # If you need to force the line ending characters to a specific - # value, set this variable to "\n" or "\r\n" instead. + # This is indicated by a value of "\n". If you need to force the line ending + # characters to a specific value, set this variable to "\n\n" (meaning "use \n"), + # "\r" or "\r\n" instead. # - values["END_OF_LINE_CHARACTERS"] = os.linesep + values["END_OF_LINE_CHARACTERS"] = "\n" # # The starting precision for feed is also set to 3 digits after the decimal point. # @@ -658,7 +693,6 @@ def process_shared_arguments( filename, "w", encoding="utf-8", - newline=values["END_OF_LINE_CHARACTERS"], ) as f: f.write(argument_text) return (False, argument_text) @@ -669,7 +703,6 @@ def process_shared_arguments( filename, "w", encoding="utf-8", - newline=values["END_OF_LINE_CHARACTERS"], ) as f: f.write(argument_text) return (False, argument_text) @@ -726,6 +759,23 @@ def process_shared_arguments( values["OUTPUT_COMMENTS"] = False if args.comment_symbol: values["COMMENT_SYMBOL"] = args.comment_symbol + if args.enable_coolant: + values["ENABLE_COOLANT"] = True + if args.disable_coolant: + values["ENABLE_COOLANT"] = False + if args.enable_machine_specific_commands: + values["ENABLE_MACHINE_SPECIFIC_COMMANDS"] = True + if args.disable_machine_specific_commands: + values["ENABLE_MACHINE_SPECIFIC_COMMANDS"] = False + if args.end_of_line_characters: + if args.end_of_line_characters == "\\n" or args.end_of_line_characters == "\n": + values["END_OF_LINE_CHARACTERS"] = "\n\n" + elif args.end_of_line_characters == "\\r" or args.end_of_line_characters == "\r": + values["END_OF_LINE_CHARACTERS"] = "\r" + elif args.end_of_line_characters == "\\r\\n" or args.end_of_line_characters == "\r\n": + values["END_OF_LINE_CHARACTERS"] = "\r\n" + else: + print("invalid end_of_line_characters, ignoring") if args.header: values["OUTPUT_HEADER"] = True if args.no_header: @@ -739,9 +789,9 @@ def process_shared_arguments( if args.no_modal: values["MODAL"] = False if args.postamble is not None: - values["POSTAMBLE"] = args.postamble + values["POSTAMBLE"] = args.postamble.replace("\\n", "\n") if args.preamble is not None: - values["PREAMBLE"] = args.preamble + values["PREAMBLE"] = args.preamble.replace("\\n", "\n") if args.return_to != "": values["RETURN_TO"] = [int(v) for v in args.return_to.split(",")] if len(values["RETURN_TO"]) != 3: diff --git a/src/Mod/CAM/Path/Post/UtilsExport.py b/src/Mod/CAM/Path/Post/UtilsExport.py index 71e929a010..e78f65fb47 100644 --- a/src/Mod/CAM/Path/Post/UtilsExport.py +++ b/src/Mod/CAM/Path/Post/UtilsExport.py @@ -5,7 +5,7 @@ # * Copyright (c) 2015 Dan Falck * # * Copyright (c) 2018, 2019 Gauthier Briere * # * Copyright (c) 2019, 2020 Schildkroet * -# * Copyright (c) 2022 Larry Woestman * +# * Copyright (c) 2022-2025 Larry Woestman * # * * # * This file is part of the FreeCAD CAx development system. * # * * @@ -54,104 +54,96 @@ def check_canned_cycles(values: Values) -> None: def output_coolant_off(values: Values, gcode: Gcode, coolant_mode: str) -> None: """Output the commands to turn coolant off if necessary.""" comment: str - nl: str = "\n" if values["ENABLE_COOLANT"] and coolant_mode != "None": if values["OUTPUT_COMMENTS"]: comment = PostUtilsParse.create_comment(values, f"Coolant Off: {coolant_mode}") - gcode.append(f"{PostUtilsParse.linenumber(values)}{comment}{nl}") - gcode.append(f"{PostUtilsParse.linenumber(values)}M9{nl}") + gcode.append(f"{PostUtilsParse.linenumber(values)}{comment}") + gcode.append(f"{PostUtilsParse.linenumber(values)}M9") def output_coolant_on(values: Values, gcode: Gcode, coolant_mode: str) -> None: """Output the commands to turn coolant on if necessary.""" comment: str - nl: str = "\n" if values["ENABLE_COOLANT"]: if values["OUTPUT_COMMENTS"] and coolant_mode != "None": comment = PostUtilsParse.create_comment(values, f"Coolant On: {coolant_mode}") - gcode.append(f"{PostUtilsParse.linenumber(values)}{comment}{nl}") + gcode.append(f"{PostUtilsParse.linenumber(values)}{comment}") if coolant_mode == "Flood": - gcode.append(f"{PostUtilsParse.linenumber(values)}M8{nl}") + gcode.append(f"{PostUtilsParse.linenumber(values)}M8") elif coolant_mode == "Mist": - gcode.append(f"{PostUtilsParse.linenumber(values)}M7{nl}") + gcode.append(f"{PostUtilsParse.linenumber(values)}M7") def output_end_bcnc(values: Values, gcode: Gcode) -> None: """Output the ending BCNC header.""" comment: str - nl: str = "\n" if values["OUTPUT_BCNC"]: comment = PostUtilsParse.create_comment(values, "Block-name: post_amble") - gcode.append(f"{PostUtilsParse.linenumber(values)}{comment}{nl}") + gcode.append(f"{PostUtilsParse.linenumber(values)}{comment}") comment = PostUtilsParse.create_comment(values, "Block-expand: 0") - gcode.append(f"{PostUtilsParse.linenumber(values)}{comment}{nl}") + gcode.append(f"{PostUtilsParse.linenumber(values)}{comment}") comment = PostUtilsParse.create_comment(values, "Block-enable: 1") - gcode.append(f"{PostUtilsParse.linenumber(values)}{comment}{nl}") + gcode.append(f"{PostUtilsParse.linenumber(values)}{comment}") def output_header(values: Values, gcode: Gcode) -> None: """Output the header.""" cam_file: str comment: str - nl: str = "\n" if not values["OUTPUT_HEADER"]: return comment = PostUtilsParse.create_comment(values, "Exported by FreeCAD") - gcode.append(f"{PostUtilsParse.linenumber(values)}{comment}{nl}") + gcode.append(f"{PostUtilsParse.linenumber(values)}{comment}") comment = PostUtilsParse.create_comment( values, f'Post Processor: {values["POSTPROCESSOR_FILE_NAME"]}' ) - gcode.append(f"{PostUtilsParse.linenumber(values)}{comment}{nl}") + gcode.append(f"{PostUtilsParse.linenumber(values)}{comment}") if FreeCAD.ActiveDocument: cam_file = os.path.basename(FreeCAD.ActiveDocument.FileName) else: cam_file = "" comment = PostUtilsParse.create_comment(values, f"Cam File: {cam_file}") - gcode.append(f"{PostUtilsParse.linenumber(values)}{comment}{nl}") + gcode.append(f"{PostUtilsParse.linenumber(values)}{comment}") comment = PostUtilsParse.create_comment(values, f"Output Time: {str(datetime.datetime.now())}") - gcode.append(f"{PostUtilsParse.linenumber(values)}{comment}{nl}") + gcode.append(f"{PostUtilsParse.linenumber(values)}{comment}") def output_motion_mode(values: Values, gcode: Gcode) -> None: """Verify if PREAMBLE or SAFETYBLOCK have changed MOTION_MODE.""" - nl: str = "\n" if "G90" in values["PREAMBLE"] or "G90" in values["SAFETYBLOCK"]: values["MOTION_MODE"] = "G90" elif "G91" in values["PREAMBLE"] or "G91" in values["SAFETYBLOCK"]: values["MOTION_MODE"] = "G91" else: - gcode.append(f'{PostUtilsParse.linenumber(values)}{values["MOTION_MODE"]}{nl}') + gcode.append(f'{PostUtilsParse.linenumber(values)}{values["MOTION_MODE"]}') def output_postamble_header(values: Values, gcode: Gcode) -> None: """Output the postamble header.""" comment: str = "" - nl: str = "\n" if values["OUTPUT_COMMENTS"]: comment = PostUtilsParse.create_comment(values, "Begin postamble") - gcode.append(f"{PostUtilsParse.linenumber(values)}{comment}{nl}") + gcode.append(f"{PostUtilsParse.linenumber(values)}{comment}") def output_postamble(values: Values, gcode: Gcode) -> None: """Output the postamble.""" line: str - nl: str = "\n" for line in values["POSTAMBLE"].splitlines(False): - gcode.append(f"{PostUtilsParse.linenumber(values)}{line}{nl}") + gcode.append(f"{PostUtilsParse.linenumber(values)}{line}") def output_postop(values: Values, gcode: Gcode, obj) -> None: """Output the post-operation information.""" comment: str line: str - nl: str = "\n" if values["OUTPUT_COMMENTS"]: if values["SHOW_OPERATION_LABELS"]: @@ -160,55 +152,52 @@ def output_postop(values: Values, gcode: Gcode, obj) -> None: ) else: comment = PostUtilsParse.create_comment(values, f'{values["FINISH_LABEL"]} operation') - gcode.append(f"{PostUtilsParse.linenumber(values)}{comment}{nl}") + gcode.append(f"{PostUtilsParse.linenumber(values)}{comment}") for line in values["POST_OPERATION"].splitlines(False): - gcode.append(f"{PostUtilsParse.linenumber(values)}{line}{nl}") + gcode.append(f"{PostUtilsParse.linenumber(values)}{line}") def output_preamble(values: Values, gcode: Gcode) -> None: """Output the preamble.""" comment: str line: str - nl: str = "\n" if values["OUTPUT_COMMENTS"]: comment = PostUtilsParse.create_comment(values, "Begin preamble") - gcode.append(f"{PostUtilsParse.linenumber(values)}{comment}{nl}") + gcode.append(f"{PostUtilsParse.linenumber(values)}{comment}") for line in values["PREAMBLE"].splitlines(False): - gcode.append(f"{PostUtilsParse.linenumber(values)}{line}{nl}") + gcode.append(f"{PostUtilsParse.linenumber(values)}{line}") def output_preop(values: Values, gcode: Gcode, obj) -> None: """Output the pre-operation information.""" comment: str line: str - nl: str = "\n" if values["OUTPUT_COMMENTS"]: if values["SHOW_OPERATION_LABELS"]: comment = PostUtilsParse.create_comment(values, f"Begin operation: {obj.Label}") else: comment = PostUtilsParse.create_comment(values, "Begin operation") - gcode.append(f"{PostUtilsParse.linenumber(values)}{comment}{nl}") + gcode.append(f"{PostUtilsParse.linenumber(values)}{comment}") if values["SHOW_MACHINE_UNITS"]: comment = PostUtilsParse.create_comment( values, f'Machine units: {values["UNIT_SPEED_FORMAT"]}' ) - gcode.append(f"{PostUtilsParse.linenumber(values)}{comment}{nl}") + gcode.append(f"{PostUtilsParse.linenumber(values)}{comment}") if values["OUTPUT_MACHINE_NAME"]: comment = PostUtilsParse.create_comment( values, f'Machine: {values["MACHINE_NAME"]}, {values["UNIT_SPEED_FORMAT"]}', ) - gcode.append(f"{PostUtilsParse.linenumber(values)}{comment}{nl}") + gcode.append(f"{PostUtilsParse.linenumber(values)}{comment}") for line in values["PRE_OPERATION"].splitlines(False): - gcode.append(f"{PostUtilsParse.linenumber(values)}{line}{nl}") + gcode.append(f"{PostUtilsParse.linenumber(values)}{line}") def output_return_to(values: Values, gcode: Gcode) -> None: """Output the RETURN_TO command.""" cmd: str - nl: str = "\n" num_x: str num_y: str num_z: str @@ -220,56 +209,51 @@ def output_return_to(values: Values, gcode: Gcode) -> None: cmd = PostUtilsParse.format_command_line( values, ["G0", f"X{num_x}", f"Y{num_y}", f"Z{num_z}"] ) - gcode.append(f"{PostUtilsParse.linenumber(values)}{cmd}{nl}") + gcode.append(f"{PostUtilsParse.linenumber(values)}{cmd}") def output_safetyblock(values: Values, gcode: Gcode) -> None: """Output the safety block.""" line: str - nl: str = "\n" for line in values["SAFETYBLOCK"].splitlines(False): - gcode.append(f"{PostUtilsParse.linenumber(values)}{line}{nl}") + gcode.append(f"{PostUtilsParse.linenumber(values)}{line}") def output_start_bcnc(values: Values, gcode: Gcode, obj) -> None: """Output the starting BCNC header.""" comment: str - nl: str = "\n" if values["OUTPUT_BCNC"]: comment = PostUtilsParse.create_comment(values, f"Block-name: {obj.Label}") - gcode.append(f"{PostUtilsParse.linenumber(values)}{comment}{nl}") + gcode.append(f"{PostUtilsParse.linenumber(values)}{comment}") comment = PostUtilsParse.create_comment(values, "Block-expand: 0") - gcode.append(f"{PostUtilsParse.linenumber(values)}{comment}{nl}") + gcode.append(f"{PostUtilsParse.linenumber(values)}{comment}") comment = PostUtilsParse.create_comment(values, "Block-enable: 1") - gcode.append(f"{PostUtilsParse.linenumber(values)}{comment}{nl}") + gcode.append(f"{PostUtilsParse.linenumber(values)}{comment}") def output_tool_list(values: Values, gcode: Gcode, objectslist) -> None: """Output a list of the tools used in the objects.""" comment: str - nl: str = "\n" if values["OUTPUT_COMMENTS"] and values["LIST_TOOLS_IN_PREAMBLE"]: for item in objectslist: if hasattr(item, "Proxy") and isinstance(item.Proxy, PathToolController.ToolController): comment = PostUtilsParse.create_comment(values, f"T{item.ToolNumber}={item.Name}") - gcode.append(f"{PostUtilsParse.linenumber(values)}{comment}{nl}") + gcode.append(f"{PostUtilsParse.linenumber(values)}{comment}") def output_tool_return(values: Values, gcode: Gcode) -> None: """Output the tool return block.""" line: str - nl: str = "\n" for line in values["TOOLRETURN"].splitlines(False): - gcode.append(f"{PostUtilsParse.linenumber(values)}{line}{nl}") + gcode.append(f"{PostUtilsParse.linenumber(values)}{line}") def output_units(values: Values, gcode: Gcode) -> None: """Verify if PREAMBLE or SAFETYBLOCK have changed UNITS.""" - nl: str = "\n" if "G21" in values["PREAMBLE"] or "G21" in values["SAFETYBLOCK"]: values["UNITS"] = "G21" @@ -280,7 +264,7 @@ def output_units(values: Values, gcode: Gcode) -> None: values["UNIT_FORMAT"] = "in" values["UNIT_SPEED_FORMAT"] = "in/min" else: - gcode.append(f'{PostUtilsParse.linenumber(values)}{values["UNITS"]}{nl}') + gcode.append(f'{PostUtilsParse.linenumber(values)}{values["UNITS"]}') def export_common(values: Values, objectslist, filename: str) -> str: @@ -288,8 +272,8 @@ def export_common(values: Values, objectslist, filename: str) -> str: coolant_mode: str dia: PostUtils.GCodeEditorDialog final: str + final_for_editor: str gcode: Gcode = [] - result: bool for obj in objectslist: if not hasattr(obj, "Path"): @@ -333,24 +317,63 @@ def export_common(values: Values, objectslist, filename: str) -> str: output_safetyblock(values, gcode) output_postamble(values, gcode) - final = "".join(gcode) + # add the appropriate end-of-line characters to the gcode, including after the last line + gcode.append("") + if values["END_OF_LINE_CHARACTERS"] == "\n\n": + # flag that we want to use "\n" as the end-of-line characters + # by putting "\n\n" at the front of the gcode (which shouldn't otherwise happen) + final = "\n\n" + "\n".join(gcode) + else: + # the other possibilities are: + # "\n" means "use the end-of-line characters that match the system" + # "\r" means "use \r" + # "\r\n" means "use \r\n" + final = values["END_OF_LINE_CHARACTERS"].join(gcode) if FreeCAD.GuiUp and values["SHOW_EDITOR"]: if len(final) > 100000: print("Skipping editor since output is greater than 100kb") else: dia = PostUtils.GCodeEditorDialog() - dia.editor.setText(final) - result = dia.exec_() - if result: - final = dia.editor.toPlainText() + # the editor expects lines to end in "\n", and returns lines ending in "\n" + if values["END_OF_LINE_CHARACTERS"] == "\n": + dia.editor.setText(final) + if dia.exec_(): + final = dia.editor.toPlainText() + else: + final_for_editor = "\n".join(gcode) + dia.editor.setText(final_for_editor) + if dia.exec_(): + final_for_editor = dia.editor.toPlainText() + # convert all "\n" to the appropriate end-of-line characters + if values["END_OF_LINE_CHARACTERS"] == "\n\n": + # flag that we want to use "\n" as the end-of-line characters + # by putting "\n\n" at the front of the gcode + # (which shouldn't otherwise happen) + final = "\n\n" + final_for_editor + else: + # the other possibilities are: + # "\r" means "use \r" + # "\r\n" means "use \r\n" + final = final_for_editor.replace("\n", values["END_OF_LINE_CHARACTERS"]) print("done postprocessing.") if not filename == "-": - with open( - filename, "w", encoding="utf-8", newline=values["END_OF_LINE_CHARACTERS"] - ) as gfile: - gfile.write(final) + if final[0:2] == "\n\n": + # write out the gcode using "\n" as the end-of-line characters + with open(filename, "w", encoding="utf-8", newline="") as gfile: + gfile.write(final[2:]) + elif "\r" in final: + with open(filename, "w", encoding="utf-8", newline="") as gfile: + # write out the gcode with whatever end-of-line characters it already has, + # presumably either "\r" or "\r\n" + gfile.write(final) + else: + with open(filename, "w", encoding="utf-8", newline=None) as gfile: + # The gcode has "\n" as the end-of-line characters, which means + # "write out the gcode with whatever end-of-line characters the system + # that is running the postprocessor uses". + gfile.write(final) return final diff --git a/src/Mod/CAM/Path/Post/UtilsParse.py b/src/Mod/CAM/Path/Post/UtilsParse.py index ec2808b706..49831fcee0 100644 --- a/src/Mod/CAM/Path/Post/UtilsParse.py +++ b/src/Mod/CAM/Path/Post/UtilsParse.py @@ -55,7 +55,6 @@ def check_for_an_adaptive_op( ) -> str: """Check to see if the current command is an adaptive op.""" adaptiveOp: bool - nl: str = "\n" opHorizRapid: float opVertRapid: float @@ -63,7 +62,7 @@ def check_for_an_adaptive_op( if values["OUTPUT_ADAPTIVE"] and adaptiveOp and command in values["RAPID_MOVES"]: if opHorizRapid and opVertRapid: return "G1" - command_line.append(f"(Tool Controller Rapid Values are unset){nl}") + command_line.append(f"(Tool Controller Rapid Values are unset)") return "" @@ -78,12 +77,11 @@ def check_for_drill_translate( ) -> bool: """Check for drill commands to translate.""" comment: str - nl: str = "\n" if values["TRANSLATE_DRILL_CYCLES"] and command in values["DRILL_CYCLES_TO_TRANSLATE"]: if values["OUTPUT_COMMENTS"]: # Comment the original command comment = create_comment(values, format_command_line(values, command_line)) - gcode.append(f"{linenumber(values)}{comment}{nl}") + gcode.append(f"{linenumber(values)}{comment}") # wrap this block to ensure that the value of values["MOTION_MODE"] # is restored in case of error try: @@ -100,7 +98,7 @@ def check_for_drill_translate( # drill_translate uses G90 mode internally, so need to # switch back to G91 mode if it was that way originally if values["MOTION_MODE"] == "G91": - gcode.append(f"{linenumber(values)}G91{nl}") + gcode.append(f"{linenumber(values)}G91") return True return False @@ -108,7 +106,6 @@ def check_for_drill_translate( def check_for_machine_specific_commands(values: Values, gcode: Gcode, command: str) -> None: """Check for comments containing machine-specific commands.""" m: object - nl: str = "\n" raw_command: str if values["ENABLE_MACHINE_SPECIFIC_COMMANDS"]: @@ -116,7 +113,7 @@ def check_for_machine_specific_commands(values: Values, gcode: Gcode, command: s if m: raw_command = m.group(1) # pass literally to the controller - gcode.append(f"{linenumber(values)}{raw_command}{nl}") + gcode.append(f"{linenumber(values)}{raw_command}") def check_for_spindle_wait( @@ -124,12 +121,11 @@ def check_for_spindle_wait( ) -> None: """Check for commands that might need a wait command after them.""" cmd: str - nl: str = "\n" if values["SPINDLE_WAIT"] > 0 and command in ("M3", "M03", "M4", "M04"): - gcode.append(f"{linenumber(values)}{format_command_line(values, command_line)}{nl}") + gcode.append(f"{linenumber(values)}{format_command_line(values, command_line)}") cmd = format_command_line(values, ["G4", f'P{values["SPINDLE_WAIT"]}']) - gcode.append(f"{linenumber(values)}{cmd}{nl}") + gcode.append(f"{linenumber(values)}{cmd}") def check_for_suppressed_commands( @@ -137,13 +133,12 @@ def check_for_suppressed_commands( ) -> bool: """Check for commands that will be suppressed.""" comment: str - nl: str = "\n" if command in values["SUPPRESS_COMMANDS"]: if values["OUTPUT_COMMENTS"]: # convert the command to a comment comment = create_comment(values, format_command_line(values, command_line)) - gcode.append(f"{linenumber(values)}{comment}{nl}") + gcode.append(f"{linenumber(values)}{comment}") # remove the command return True return False @@ -151,34 +146,32 @@ def check_for_suppressed_commands( def check_for_tlo(values: Values, gcode: Gcode, command: str, params: PathParameters) -> None: """Output a tool length command if USE_TLO is True.""" - nl: str = "\n" if command in ("M6", "M06") and values["USE_TLO"]: cmd = format_command_line(values, ["G43", f'H{str(int(params["T"]))}']) - gcode.append(f"{linenumber(values)}{cmd}{nl}") + gcode.append(f"{linenumber(values)}{cmd}") def check_for_tool_change( values: Values, gcode: Gcode, command: str, command_line: CommandLine ) -> bool: """Check for a tool change.""" - nl: str = "\n" if command in ("M6", "M06"): if values["OUTPUT_COMMENTS"]: comment = create_comment(values, "Begin toolchange") - gcode.append(f"{linenumber(values)}{comment}{nl}") + gcode.append(f"{linenumber(values)}{comment}") if values["OUTPUT_TOOL_CHANGE"]: if values["STOP_SPINDLE_FOR_TOOL_CHANGE"]: # stop the spindle - gcode.append(f"{linenumber(values)}M5{nl}") + gcode.append(f"{linenumber(values)}M5") for line in values["TOOL_CHANGE"].splitlines(False): - gcode.append(f"{linenumber(values)}{line}{nl}") + gcode.append(f"{linenumber(values)}{line}") return False if values["OUTPUT_COMMENTS"]: # convert the tool change to a comment comment = create_comment(values, format_command_line(values, command_line)) - gcode.append(f"{linenumber(values)}{comment}{nl}") + gcode.append(f"{linenumber(values)}{comment}") return True return False @@ -425,14 +418,13 @@ def drill_translate( drill_y: float drill_z: float motion_z: float - nl: str = "\n" retract_z: float F_feedrate: str G0_retract_z: str if values["MOTION_MODE"] == "G91": # force absolute coordinates during cycles - gcode.append(f"{linenumber(values)}G90{nl}") + gcode.append(f"{linenumber(values)}G90") drill_x = Units.Quantity(params["X"], Units.Length) drill_y = Units.Quantity(params["Y"], Units.Length) @@ -440,7 +432,7 @@ def drill_translate( retract_z = Units.Quantity(params["R"], Units.Length) if retract_z < drill_z: # R less than Z is error comment = create_comment(values, "Drill cycle error: R less than Z") - gcode.append(f"{linenumber(values)}{comment}{nl}") + gcode.append(f"{linenumber(values)}{comment}") return motion_z = Units.Quantity(motion_location["Z"], Units.Length) if values["MOTION_MODE"] == "G91": # relative movements @@ -452,9 +444,9 @@ def drill_translate( retract_z = motion_z cmd = format_command_line(values, ["G0", f"Z{format_for_axis(values, retract_z)}"]) - G0_retract_z = f"{cmd}{nl}" + G0_retract_z = f"{cmd}" cmd = format_for_feed(values, Units.Quantity(params["F"], Units.Velocity)) - F_feedrate = f'{values["COMMAND_SPACE"]}F{cmd}{nl}' + F_feedrate = f'{values["COMMAND_SPACE"]}F{cmd}' # preliminary movement(s) if motion_z < retract_z: @@ -467,7 +459,7 @@ def drill_translate( f"Y{format_for_axis(values, drill_y)}", ], ) - gcode.append(f"{linenumber(values)}{cmd}{nl}") + gcode.append(f"{linenumber(values)}{cmd}") if motion_z > retract_z: # NIST GCODE 3.5.16.1 Preliminary and In-Between Motion says G0 to retract_z # Here use G1 since retract height may be below surface ! @@ -588,7 +580,6 @@ def output_G73_G83_drill_moves( drill_step: float last_stop_z: float next_stop_z: float - nl: str = "\n" last_stop_z = retract_z drill_step = Units.Quantity(params["Q"], Units.Length) @@ -603,7 +594,7 @@ def output_G73_G83_drill_moves( values, ["G0", f"Z{format_for_axis(values, clearance_depth)}"], ) - gcode.append(f"{linenumber(values)}{cmd}{nl}") + gcode.append(f"{linenumber(values)}{cmd}") next_stop_z = last_stop_z - drill_step if next_stop_z > drill_z: cmd = format_command_line( @@ -620,7 +611,7 @@ def output_G73_G83_drill_moves( f"Z{format_for_axis(values, chip_breaker_height)}", ], ) - gcode.append(f"{linenumber(values)}{cmd}{nl}") + gcode.append(f"{linenumber(values)}{cmd}") elif command == "G83": # Rapid up to the retract height gcode.append(f"{linenumber(values)}{G0_retract_z}") @@ -643,26 +634,24 @@ def output_G81_G82_drill_moves( ) -> None: """Output the movement G code for G81 and G82.""" cmd: str - nl: str = "\n" cmd = format_command_line(values, ["G1", f"Z{format_for_axis(values, drill_z)}"]) gcode.append(f"{linenumber(values)}{cmd}{F_feedrate}") # pause where applicable if command == "G82": cmd = format_command_line(values, ["G4", f'P{str(params["P"])}']) - gcode.append(f"{linenumber(values)}{cmd}{nl}") + gcode.append(f"{linenumber(values)}{cmd}") gcode.append(f"{linenumber(values)}{G0_retract_z}") def parse_a_group(values: Values, gcode: Gcode, pathobj) -> None: """Parse a Group (compound, project, or simple path).""" comment: str - nl: str = "\n" if hasattr(pathobj, "Group"): # We have a compound or project. if values["OUTPUT_COMMENTS"]: comment = create_comment(values, f"Compound: {pathobj.Label}") - gcode.append(f"{linenumber(values)}{comment}{nl}") + gcode.append(f"{linenumber(values)}{comment}") for p in pathobj.Group: parse_a_group(values, gcode, p) else: # parsing simple path @@ -671,7 +660,7 @@ def parse_a_group(values: Values, gcode: Gcode, pathobj) -> None: return if values["OUTPUT_PATH_LABELS"] and values["OUTPUT_COMMENTS"]: comment = create_comment(values, f"Path: {pathobj.Label}") - gcode.append(f"{linenumber(values)}{comment}{nl}") + gcode.append(f"{linenumber(values)}{comment}") parse_a_path(values, gcode, pathobj) @@ -685,7 +674,6 @@ def parse_a_path(values: Values, gcode: Gcode, pathobj) -> None: drill_retract_mode: str = "G98" lastcommand: str = "" motion_location: PathParameters = {} # keep track of last motion location - nl: str = "\n" parameter: str parameter_value: str @@ -784,13 +772,13 @@ def parse_a_path(values: Values, gcode: Gcode, pathobj) -> None: command_line[1], command_line[0], ] # swap the order of the commands - # Add a line number to the front and a newline to the end of the command line + # Add a line number to the front of the command line gcode.append( - f"{linenumber(values)}{format_command_line(values, swapped_command_line)}{nl}" + f"{linenumber(values)}{format_command_line(values, swapped_command_line)}" ) else: - # Add a line number to the front and a newline to the end of the command line - gcode.append(f"{linenumber(values)}{format_command_line(values, command_line)}{nl}") + # Add a line number to the front of the command line + gcode.append(f"{linenumber(values)}{format_command_line(values, command_line)}") check_for_tlo(values, gcode, command, c.Parameters) check_for_machine_specific_commands(values, gcode, command) diff --git a/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py b/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py index 1881a012cf..d8a9a20307 100644 --- a/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py +++ b/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py @@ -674,20 +674,22 @@ class Snapmaker(Path.Post.Processor.PostProcessor): def output_header(self, gcode: List[str]): """custom method derived from Path.Post.UtilsExport.output_header""" cam_file: str + comment: str if not self.values["OUTPUT_HEADER"]: return def add_comment(text): com = Path.Post.UtilsParse.create_comment(self.values, text) - gcode.append( - f'{Path.Post.UtilsParse.linenumber(self.values)}{com}{self.values["END_OF_LINE_CHARACTERS"]}' - ) + gcode.append(f"{Path.Post.UtilsParse.linenumber(self.values)}{com}") add_comment("Header Start") add_comment("header_type: cnc") add_comment(f'machine: {self.values["MACHINE_NAME"]}') - add_comment(f'Post Processor: {self.values["POSTPROCESSOR_FILE_NAME"]}') + comment = Path.Post.UtilsParse.create_comment( + self.values, f'Post Processor: {self.values["POSTPROCESSOR_FILE_NAME"]}' + ) + gcode.append(f"{Path.Post.UtilsParse.linenumber(self.values)}{comment}") if FreeCAD.ActiveDocument: cam_file = os.path.basename(FreeCAD.ActiveDocument.FileName) else: @@ -803,20 +805,28 @@ class Snapmaker(Path.Post.Processor.PostProcessor): if self.values["BOUNDARIES_CHECK"]: self.check_boundaries(gcode) - final = "".join(gcode) + # add the appropriate end-of-line characters to the gcode, including after the last line + gcode.append("") + final = self.values["END_OF_LINE_CHARACTERS"].join(gcode) if FreeCAD.GuiUp and self.values["SHOW_EDITOR"]: # size limit removed as irrelevant on my computer - see if issues occur - dia = Path.Post.Utils.GCodeEditorDialog() - dia.editor.setText(final) - result = dia.exec_() - if result: - final = dia.editor.toPlainText() + dia = PostUtils.GCodeEditorDialog() + # the editor expects lines to end in "\n", and returns lines ending in "\n" + if self.values["END_OF_LINE_CHARACTERS"] == "\n": + dia.editor.setText(final) + if dia.exec_(): + final = dia.editor.toPlainText() + else: + final_for_editor = "\n".join(gcode) + dia.editor.setText(final_for_editor) + if dia.exec_(): + final_for_editor = dia.editor.toPlainText() + # convert all "\n" to the appropriate end-of-line characters + final = final_for_editor.replace("\n", self.values["END_OF_LINE_CHARACTERS"]) if not filename == "-": - with open( - filename, "w", encoding="utf-8", newline=self.values["END_OF_LINE_CHARACTERS"] - ) as gfile: + with open(filename, "w", encoding="utf-8", newline="") as gfile: gfile.write(final) return final