From bb172e4a6b0a0b9a92a42dd34d3dbff9d503c750 Mon Sep 17 00:00:00 2001 From: sliptonic Date: Sat, 15 Nov 2025 09:46:34 -0600 Subject: [PATCH] CAM: Renaming of postprocessors and cleanup (#24771) * Post Processor cleanup and rename remove unused files Basic implementation of generic post add blending mode support for linuxcnc add mocking for postprocessor tests Add tests for generic post linuxcnc test only tests linuxcnc specific functionality minor improvements add arc splitting to refactored post processors Refactor smoothie post move posts to legacy. Remove 'refactored' lint cleanup fixes #19417 * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- src/Mod/CAM/CAMTests/PostTestMocks.py | 155 ++++ .../CAM/CAMTests/TestCentroidLegacyPost.py | 326 +++++++ src/Mod/CAM/CAMTests/TestCentroidPost.py | 281 +++--- ...dTestDressupPost.py => TestDressupPost.py} | 6 +- ...oredLinuxCNCPost.py => TestGenericPost.py} | 65 +- src/Mod/CAM/CAMTests/TestGrblLegacyPost.py | 254 ++++++ src/Mod/CAM/CAMTests/TestGrblPost.py | 267 +++--- .../CAM/CAMTests/TestLinuxCNCLegacyPost.py | 420 +++++++++ src/Mod/CAM/CAMTests/TestLinuxCNCPost.py | 452 +++------- .../CAM/CAMTests/TestMach3Mach4LegacyPost.py | 288 ++++++ src/Mod/CAM/CAMTests/TestMach3Mach4Post.py | 277 +++--- ...toredMassoG3Post.py => TestMassoG3Post.py} | 5 +- src/Mod/CAM/CAMTests/TestPathPost.py | 6 +- ...redTestPostGCodes.py => TestPostGCodes.py} | 6 +- ...redTestPostMCodes.py => TestPostMCodes.py} | 6 +- .../CAMTests/TestRefactoredCentroidPost.py | 361 -------- .../CAM/CAMTests/TestRefactoredGrblPost.py | 353 -------- .../CAMTests/TestRefactoredMach3Mach4Post.py | 365 -------- ...tRefactoredTestPost.py => TestTestPost.py} | 12 +- src/Mod/CAM/CAMTests/boxtest1.fcstd | Bin 46372 -> 0 bytes src/Mod/CAM/CAMTests/drill_test1.FCStd | Bin 31979 -> 0 bytes src/Mod/CAM/CAMTests/test_centroid_00.ngc | 69 -- src/Mod/CAM/CAMTests/test_filenaming.fcstd | Bin 279228 -> 0 bytes src/Mod/CAM/CMakeLists.txt | 43 +- src/Mod/CAM/Path/Post/Utils.py | 37 +- src/Mod/CAM/Path/Post/UtilsArguments.py | 19 + src/Mod/CAM/Path/Post/UtilsParse.py | 8 +- .../Path/Post/scripts/centroid_legacy_post.py | 347 +++++++ .../CAM/Path/Post/scripts/centroid_post.py | 487 ++++------ .../CAM/Path/Post/scripts/comparams_post.py | 116 --- src/Mod/CAM/Path/Post/scripts/dumper_post.py | 98 -- src/Mod/CAM/Path/Post/scripts/example_post.py | 100 --- src/Mod/CAM/Path/Post/scripts/example_pre.py | 115 --- src/Mod/CAM/Path/Post/scripts/generic_post.py | 56 +- .../CAM/Path/Post/scripts/grbl_legacy_post.py | 717 +++++++++++++++ src/Mod/CAM/Path/Post/scripts/grbl_post.py | 844 ++++-------------- .../Path/Post/scripts/linuxcnc_legacy_post.py | 461 ++++++++++ .../CAM/Path/Post/scripts/linuxcnc_post.py | 648 ++++---------- .../Post/scripts/mach3_mach4_legacy_post.py | 481 ++++++++++ .../CAM/Path/Post/scripts/mach3_mach4_post.py | 563 +++--------- ...ored_masso_g3_post.py => masso_g3_post.py} | 48 +- .../Post/scripts/refactored_centroid_post.py | 196 ---- .../Path/Post/scripts/refactored_grbl_post.py | 190 ---- .../Post/scripts/refactored_linuxcnc_post.py | 129 --- .../scripts/refactored_mach3_mach4_post.py | 150 ---- .../CAM/Path/Post/scripts/smoothie_post.py | 644 +++++-------- .../{refactored_test_post.py => test_post.py} | 8 +- src/Mod/CAM/TestCAMApp.py | 25 +- 48 files changed, 5086 insertions(+), 5418 deletions(-) create mode 100644 src/Mod/CAM/CAMTests/PostTestMocks.py create mode 100644 src/Mod/CAM/CAMTests/TestCentroidLegacyPost.py rename src/Mod/CAM/CAMTests/{TestRefactoredTestDressupPost.py => TestDressupPost.py} (97%) rename src/Mod/CAM/CAMTests/{TestRefactoredLinuxCNCPost.py => TestGenericPost.py} (87%) create mode 100644 src/Mod/CAM/CAMTests/TestGrblLegacyPost.py create mode 100644 src/Mod/CAM/CAMTests/TestLinuxCNCLegacyPost.py create mode 100644 src/Mod/CAM/CAMTests/TestMach3Mach4LegacyPost.py rename src/Mod/CAM/CAMTests/{TestRefactoredMassoG3Post.py => TestMassoG3Post.py} (98%) rename src/Mod/CAM/CAMTests/{TestRefactoredTestPostGCodes.py => TestPostGCodes.py} (99%) rename src/Mod/CAM/CAMTests/{TestRefactoredTestPostMCodes.py => TestPostMCodes.py} (98%) delete mode 100644 src/Mod/CAM/CAMTests/TestRefactoredCentroidPost.py delete mode 100644 src/Mod/CAM/CAMTests/TestRefactoredGrblPost.py delete mode 100644 src/Mod/CAM/CAMTests/TestRefactoredMach3Mach4Post.py rename src/Mod/CAM/CAMTests/{TestRefactoredTestPost.py => TestTestPost.py} (99%) delete mode 100644 src/Mod/CAM/CAMTests/boxtest1.fcstd delete mode 100644 src/Mod/CAM/CAMTests/drill_test1.FCStd delete mode 100644 src/Mod/CAM/CAMTests/test_centroid_00.ngc delete mode 100644 src/Mod/CAM/CAMTests/test_filenaming.fcstd create mode 100644 src/Mod/CAM/Path/Post/scripts/centroid_legacy_post.py delete mode 100644 src/Mod/CAM/Path/Post/scripts/comparams_post.py delete mode 100644 src/Mod/CAM/Path/Post/scripts/dumper_post.py delete mode 100644 src/Mod/CAM/Path/Post/scripts/example_post.py delete mode 100644 src/Mod/CAM/Path/Post/scripts/example_pre.py create mode 100644 src/Mod/CAM/Path/Post/scripts/grbl_legacy_post.py create mode 100644 src/Mod/CAM/Path/Post/scripts/linuxcnc_legacy_post.py create mode 100644 src/Mod/CAM/Path/Post/scripts/mach3_mach4_legacy_post.py rename src/Mod/CAM/Path/Post/scripts/{refactored_masso_g3_post.py => masso_g3_post.py} (80%) delete mode 100644 src/Mod/CAM/Path/Post/scripts/refactored_centroid_post.py delete mode 100644 src/Mod/CAM/Path/Post/scripts/refactored_grbl_post.py delete mode 100644 src/Mod/CAM/Path/Post/scripts/refactored_linuxcnc_post.py delete mode 100644 src/Mod/CAM/Path/Post/scripts/refactored_mach3_mach4_post.py rename src/Mod/CAM/Path/Post/scripts/{refactored_test_post.py => test_post.py} (95%) diff --git a/src/Mod/CAM/CAMTests/PostTestMocks.py b/src/Mod/CAM/CAMTests/PostTestMocks.py new file mode 100644 index 0000000000..479d0e7080 --- /dev/null +++ b/src/Mod/CAM/CAMTests/PostTestMocks.py @@ -0,0 +1,155 @@ +# *************************************************************************** +# * Copyright (c) 2025 sliptonic * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +"""Mock objects for postprocessor testing. + +This module provides mock objects that simulate FreeCAD CAM job structure +without requiring disk I/O or loading actual FreeCAD documents. +""" + +import Path + + +class MockToolController: + """Mock ToolController for operations.""" + + def __init__( + self, + tool_number=1, + label="TC: Default Tool", + spindle_speed=1000, + spindle_dir="Forward", + ): + self.ToolNumber = tool_number + self.Label = label + self.SpindleSpeed = spindle_speed + self.SpindleDir = spindle_dir + self.Name = f"TC{tool_number}" + + # Create a simple path with tool change commands + self.Path = Path.Path() + self.Path.addCommands( + [Path.Command(f"M6 T{tool_number}"), Path.Command(f"M3 S{spindle_speed}")] + ) + + def InList(self): + return [] + + +class MockOperation: + """Mock Operation object for testing postprocessors.""" + + def __init__(self, name="Operation", label=None, tool_controller=None, active=True): + self.Name = name + self.Label = label or name + self.Active = active + self.ToolController = tool_controller + + # Create an empty path by default + self.Path = Path.Path() + + def InList(self): + """Mock InList - operations belong to a job.""" + return [] + + +class MockStock: + """Mock Stock object with BoundBox.""" + + def __init__(self, xmin=0.0, xmax=100.0, ymin=0.0, ymax=100.0, zmin=0.0, zmax=10.0): + self.Shape = type( + "obj", + (object,), + { + "BoundBox": type( + "obj", + (object,), + { + "XMin": xmin, + "XMax": xmax, + "YMin": ymin, + "YMax": ymax, + "ZMin": zmin, + "ZMax": zmax, + }, + )() + }, + )() + + +class MockSetupSheet: + """Mock SetupSheet object.""" + + def __init__(self, clearance_height=5.0, safe_height=3.0): + self.ClearanceHeightOffset = type("obj", (object,), {"Value": clearance_height})() + self.SafeHeightOffset = type("obj", (object,), {"Value": safe_height})() + + +class MockJob: + """Mock Job object for testing postprocessors.""" + + def __init__(self): + # Create mock Stock with BoundBox + self.Stock = MockStock() + + # Create mock SetupSheet + self.SetupSheet = MockSetupSheet() + + # Create mock Operations group + self.Operations = type("obj", (object,), {"Group": []})() + + # Create mock Tools group + self.Tools = type("obj", (object,), {"Group": []})() + + # Create mock Model group + self.Model = type("obj", (object,), {"Group": []})() + + # Basic properties + self.Label = "MockJob" + self.PostProcessor = "" + self.PostProcessorArgs = "" + self.PostProcessorOutputFile = "" + self.Fixtures = ["G54"] + self.OrderOutputBy = "Tool" + self.SplitOutput = False + + def InList(self): + """Mock InList for fixture setup.""" + return [] + + +def create_default_job_with_operation(): + """Create a mock job with a default tool controller and operation. + + This is a convenience function for common test scenarios. + Returns: (job, operation, tool_controller) + """ + job = MockJob() + + # Create default tool controller + tc = MockToolController(tool_number=1, label="TC: Default Tool", spindle_speed=1000) + job.Tools.Group = [tc] + + # Create default operation + op = MockOperation(name="Profile", label="Profile", tool_controller=tc) + job.Operations.Group = [op] + + return job, op, tc diff --git a/src/Mod/CAM/CAMTests/TestCentroidLegacyPost.py b/src/Mod/CAM/CAMTests/TestCentroidLegacyPost.py new file mode 100644 index 0000000000..1efedb82dd --- /dev/null +++ b/src/Mod/CAM/CAMTests/TestCentroidLegacyPost.py @@ -0,0 +1,326 @@ +# *************************************************************************** +# * Copyright (c) 2022 sliptonic * +# * Copyright (c) 2022 Larry Woestman * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +from importlib import reload + +import FreeCAD + +import Path +from CAMTests import PathTestUtils +from Path.Post.scripts import centroid_legacy_post as postprocessor + + +Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) +Path.Log.trackModule(Path.Log.thisModule()) + + +class TestCentroidLegacyPost(PathTestUtils.PathTestBase): + """Test suite for the Centroid legacy postprocessor. + + This class verifies the functionality of the centroid_legacy_post postprocessor, + which generates G-code for Centroid CNC controllers. Tests cover: + + - Output generation with various options (header, comments, editor suppression) + - Command formatting and coordinate precision control + - Line numbering + - Preamble and postamble generation + - Unit conversion (metric/imperial) + - Modal command suppression + - Axis modal (coordinate suppression for unchanged axes) + - Tool change command generation with tool length offset (TLO) + - Comment formatting and conversion + + The legacy postprocessor has limited configurability compared to newer + postprocessors, and some command-line options are tested to verify they + don't break the default behavior. + """ + + @classmethod + def setUpClass(cls): + """setUpClass()... + This method is called upon instantiation of this test class. Add code + and objects here that are needed for the duration of the test() methods + in this class. In other words, set up the 'global' test environment + here; use the `setUp()` method to set up a 'local' test environment. + This method does not have access to the class `self` reference, but it + is able to call static methods within this same class. + """ + + # Open existing FreeCAD document with test geometry + FreeCAD.newDocument("Unnamed") + + @classmethod + def tearDownClass(cls): + """tearDownClass()... + This method is called prior to destruction of this test class. Add + code and objects here that cleanup the test environment after the + test() methods in this class have been executed. This method does + not have access to the class `self` reference. This method + is able to call static methods within this same class. + """ + # Close geometry document without saving + FreeCAD.closeDocument(FreeCAD.ActiveDocument.Name) + + # Setup and tear down methods called before and after each unit test + def setUp(self): + """setUp()... + This method is called prior to each `test()` method. Add code and + objects here that are needed for multiple `test()` methods. + """ + self.doc = FreeCAD.ActiveDocument + self.con = FreeCAD.Console + self.docobj = FreeCAD.ActiveDocument.addObject("Path::Feature", "testpath") + reload( + postprocessor + ) # technical debt. This shouldn't be necessary but here to bypass a bug + + def tearDown(self): + """tearDown()... + This method is called after each test() method. Add cleanup instructions here. + Such cleanup instructions will likely undo those in the setUp() method. + """ + FreeCAD.ActiveDocument.removeObject("testpath") + + def test000(self): + """Test Output Generation. + Empty path. Produces only the preamble and postable. + """ + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + # Test generating with header + # Header contains a time stamp that messes up unit testing. + # Only test length of result. + args = "--no-show-editor" + gcode = postprocessor.export(postables, "-", args) + self.assertEqual(len(gcode.splitlines()), 16) + + # Test without header + expected = """G90 G80 G40 G49 +;begin preamble +G53 G00 G17 +G21 +;begin operation +;end operation: testpath +;begin postamble +M5 +M25 +G49 H0 +G90 G80 G40 G49 +M99 +""" + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "-", args) + self.assertEqual(gcode, expected) + + # test without comments + expected = """G90 G80 G40 G49 +G53 G00 G17 +G21 +M5 +M25 +G49 H0 +G90 G80 G40 G49 +M99 +""" + + args = "--no-header --no-comments --no-show-editor" + # args = ("--no-header --no-comments --no-show-editor --precision=2") + gcode = postprocessor.export(postables, "-", args) + self.assertEqual(gcode, expected) + + def test010(self): + """Test command Generation. + Test Precision + """ + c = Path.Command("G0 X10 Y20 Z30") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "-", args) + result = gcode.splitlines()[5] + expected = "G0 X10.0000 Y20.0000 Z30.0000" + self.assertEqual(result, expected) + + args = "--no-header --axis-precision=2 --no-show-editor" + gcode = postprocessor.export(postables, "-", args) + result = gcode.splitlines()[5] + expected = "G0 X10.00 Y20.00 Z30.00" + self.assertEqual(result, expected) + + def test020(self): + """ + Test Line Numbers + """ + c = Path.Command("G0 X10 Y20 Z30") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --line-numbers --no-show-editor" + gcode = postprocessor.export(postables, "-", args) + result = gcode.splitlines()[5] + expected = "N150 G0 X10.0000 Y20.0000 Z30.0000" + self.assertEqual(result, expected) + + def test030(self): + """ + Test Pre-amble + """ + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + # + # The original centroid postprocessor does not have a + # --preamble option. We end up with the default preamble. + # + args = "--no-header --no-comments --preamble='G18 G55' --no-show-editor" + gcode = postprocessor.export(postables, "-", args) + result = gcode.splitlines()[1] + self.assertEqual(result, "G53 G00 G17") + + def test040(self): + """ + Test Post-amble + """ + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + # + # The original centroid postprocessor does not have a + # --postamble option. We end up with the default postamble. + # + args = "--no-header --no-comments --postamble='G0 Z50\nM2' --no-show-editor" + gcode = postprocessor.export(postables, "-", args) + self.assertEqual(gcode.splitlines()[-1], "M99") + + def test050(self): + """ + Test inches + """ + + c = Path.Command("G0 X10 Y20 Z30") + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --inches --no-show-editor" + gcode = postprocessor.export(postables, "-", args) + self.assertEqual(gcode.splitlines()[3], "G20") + + result = gcode.splitlines()[5] + expected = "G0 X0.3937 Y0.7874 Z1.1811" + self.assertEqual(result, expected) + + args = "--no-header --inches --axis-precision=2 --no-show-editor" + gcode = postprocessor.export(postables, "-", args) + result = gcode.splitlines()[5] + expected = "G0 X0.39 Y0.79 Z1.18" + self.assertEqual(result, expected) + + def test060(self): + """ + Test test modal + Suppress the command name if the same as previous + """ + c = Path.Command("G0 X10 Y20 Z30") + c1 = Path.Command("G0 X10 Y30 Z30") + + self.docobj.Path = Path.Path([c, c1]) + postables = [self.docobj] + + # + # The original centroid postprocessor does not have a + # --modal option. We end up with the original gcode. + # + args = "--no-header --modal --no-show-editor" + gcode = postprocessor.export(postables, "-", args) + result = gcode.splitlines()[6] + expected = "G0 X10.0000 Y30.0000 Z30.0000" + self.assertEqual(result, expected) + + def test070(self): + """ + Test axis modal + Suppress the axis coordinate if the same as previous + """ + c = Path.Command("G0 X10 Y20 Z30") + c1 = Path.Command("G0 X10 Y30 Z30") + + self.docobj.Path = Path.Path([c, c1]) + postables = [self.docobj] + + # + # The original centroid postprocessor does not have an + # --axis-modal option. We end up with the original gcode. + # + args = "--no-header --axis-modal --no-show-editor" + gcode = postprocessor.export(postables, "-", args) + result = gcode.splitlines()[6] + expected = "G0 X10.0000 Y30.0000 Z30.0000" + self.assertEqual(result, expected) + + def test080(self): + """ + Test tool change + """ + c = Path.Command("M6 T2") + c2 = Path.Command("M3 S3000") + self.docobj.Path = Path.Path([c, c2]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "-", args) + self.assertEqual(gcode.splitlines()[5], "G43 H2") + self.assertEqual(gcode.splitlines()[6], "M6 T2") + self.assertEqual(gcode.splitlines()[7], "M3 S3000") + + # suppress TLO + # + # The original centroid postprocessor does not have an + # --no-tlo option. We end up with the original gcode. + # + args = "--no-header --no-tlo --no-show-editor" + gcode = postprocessor.export(postables, "-", args) + self.assertEqual(gcode.splitlines()[6], "M3 S3000") + + def test090(self): + """ + Test comment + """ + + c = Path.Command("(comment)") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "-", args) + result = gcode.splitlines()[5] + expected = ";comment" + self.assertEqual(result, expected) diff --git a/src/Mod/CAM/CAMTests/TestCentroidPost.py b/src/Mod/CAM/CAMTests/TestCentroidPost.py index 6a3ee7348b..0b0ef14e04 100644 --- a/src/Mod/CAM/CAMTests/TestCentroidPost.py +++ b/src/Mod/CAM/CAMTests/TestCentroidPost.py @@ -2,7 +2,7 @@ # *************************************************************************** # * Copyright (c) 2022 sliptonic * -# * Copyright (c) 2022 Larry Woestman * +# * Copyright (c) 2022 - 2025 Larry Woestman * # * * # * This program is free software; you can redistribute it and/or modify * # * it under the terms of the GNU Lesser General Public License (LGPL) * @@ -22,13 +22,11 @@ # * * # *************************************************************************** -from importlib import reload - import FreeCAD import Path import CAMTests.PathTestUtils as PathTestUtils -from Path.Post.scripts import centroid_post as postprocessor +from Path.Post.Processor import PostProcessorFactory Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) @@ -36,9 +34,12 @@ Path.Log.trackModule(Path.Log.thisModule()) class TestCentroidPost(PathTestUtils.PathTestBase): + """Test the centroid_post.py postprocessor.""" + @classmethod def setUpClass(cls): """setUpClass()... + This method is called upon instantiation of this test class. Add code and objects here that are needed for the duration of the test() methods in this class. In other words, set up the 'global' test environment @@ -47,64 +48,109 @@ class TestCentroidPost(PathTestUtils.PathTestBase): is able to call static methods within this same class. """ - # Open existing FreeCAD document with test geometry - FreeCAD.newDocument("Unnamed") + FreeCAD.ConfigSet("SuppressRecomputeRequiredDialog", "True") + cls.doc = FreeCAD.open(FreeCAD.getHomePath() + "/Mod/CAM/CAMTests/boxtest.fcstd") + cls.job = cls.doc.getObject("Job") + cls.post = PostProcessorFactory.get_post_processor(cls.job, "centroid") + # locate the operation named "Profile" + for op in cls.job.Operations.Group: + if op.Label == "Profile": + # remember the "Profile" operation + cls.profile_op = op + return @classmethod def tearDownClass(cls): """tearDownClass()... + This method is called prior to destruction of this test class. Add code and objects here that cleanup the test environment after the - test() methods in this class have been executed. This method does - not have access to the class `self` reference. This method - is able to call static methods within this same class. + test() methods in this class have been executed. This method does not + have access to the class `self` reference. This method is able to + call static methods within this same class. """ - # Close geometry document without saving - FreeCAD.closeDocument(FreeCAD.ActiveDocument.Name) + FreeCAD.closeDocument(cls.doc.Name) + FreeCAD.ConfigSet("SuppressRecomputeRequiredDialog", "") # Setup and tear down methods called before and after each unit test + def setUp(self): """setUp()... + This method is called prior to each `test()` method. Add code and objects here that are needed for multiple `test()` methods. """ - self.doc = FreeCAD.ActiveDocument - self.con = FreeCAD.Console - self.docobj = FreeCAD.ActiveDocument.addObject("Path::Feature", "testpath") - reload( - postprocessor - ) # technical debt. This shouldn't be necessary but here to bypass a bug + # allow a full length "diff" if an error occurs + self.maxDiff = None + # reinitialize the postprocessor data structures between tests + self.post.reinitialize() def tearDown(self): """tearDown()... + This method is called after each test() method. Add cleanup instructions here. Such cleanup instructions will likely undo those in the setUp() method. """ - FreeCAD.ActiveDocument.removeObject("testpath") + pass + + def single_compare(self, path, expected, args, debug=False): + """Perform a test with a single line 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) + # the gcode is in the first section for this particular job and operation + gcode = self.post.export()[0][1] + if debug: + print(f"--------{nl}{gcode}--------{nl}") + # there are 4 lines of "other stuff" before the line we are interested in + self.assertEqual(gcode.splitlines()[4], expected) + + 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) + # the gcode is in the first section for this particular job and operation + gcode = self.post.export()[0][1] + if debug: + print(f"--------{nl}{gcode}--------{nl}") + self.assertEqual(gcode, expected) def test000(self): """Test Output Generation. Empty path. Produces only the preamble and postable. """ + nl = "\n" - self.docobj.Path = Path.Path([]) - postables = [self.docobj] + self.profile_op.Path = Path.Path([]) # Test generating with header # Header contains a time stamp that messes up unit testing. # Only test length of result. - args = "--no-show-editor" - gcode = postprocessor.export(postables, "-", args) - self.assertTrue(len(gcode.splitlines()) == 16) + self.job.PostProcessorArgs = "--no-show-editor" + gcode = self.post.export()[0][1] + # print(f"--------{nl}{gcode}--------{nl}") + self.assertTrue(len(gcode.splitlines()) == 25) # Test without header expected = """G90 G80 G40 G49 -;begin preamble +;T1=TC__Default_Tool +;Begin preamble G53 G00 G17 G21 -;begin operation -;end operation: testpath -;begin postamble +;Begin operation +G54 +;End operation +;Begin operation +;TC: Default Tool +;Begin toolchange +M6 T1 +;End operation +;Begin operation +;End operation +;Begin postamble M5 M25 G49 H0 @@ -112,17 +158,17 @@ G90 G80 G40 G49 M99 """ - self.docobj.Path = Path.Path([]) - postables = [self.docobj] - - args = "--no-header --no-show-editor" - gcode = postprocessor.export(postables, "-", args) + self.job.PostProcessorArgs = "--no-header --no-show-editor" + gcode = self.post.export()[0][1] + # print(f"--------{nl}{gcode}--------{nl}") self.assertEqual(gcode, expected) # test without comments expected = """G90 G80 G40 G49 G53 G00 G17 G21 +G54 +M6 T1 M5 M25 G49 H0 @@ -130,29 +176,33 @@ G90 G80 G40 G49 M99 """ - args = "--no-header --no-comments --no-show-editor" # args = ("--no-header --no-comments --no-show-editor --precision=2") - gcode = postprocessor.export(postables, "-", args) + self.job.PostProcessorArgs = "--no-header --no-comments --no-show-editor" + gcode = self.post.export()[0][1] + # print(f"--------{nl}{gcode}--------{nl}") self.assertEqual(gcode, expected) def test010(self): """Test command Generation. Test Precision """ + nl = "\n" + c = Path.Command("G0 X10 Y20 Z30") - self.docobj.Path = Path.Path([c]) - postables = [self.docobj] + self.profile_op.Path = Path.Path([c]) - args = "--no-header --no-show-editor" - gcode = postprocessor.export(postables, "-", args) - result = gcode.splitlines()[5] + self.job.PostProcessorArgs = "--no-header --no-show-editor" + gcode = self.post.export()[0][1] + # print(f"--------{nl}{gcode}--------{nl}") + result = gcode.splitlines()[14] expected = "G0 X10.0000 Y20.0000 Z30.0000" self.assertEqual(result, expected) - args = "--no-header --axis-precision=2 --no-show-editor" - gcode = postprocessor.export(postables, "-", args) - result = gcode.splitlines()[5] + self.job.PostProcessorArgs = "--no-header --axis-precision=2 --no-show-editor" + gcode = self.post.export()[0][1] + # print(f"--------{nl}{gcode}--------{nl}") + result = gcode.splitlines()[14] expected = "G0 X10.00 Y20.00 Z30.00" self.assertEqual(result, expected) @@ -160,68 +210,75 @@ M99 """ Test Line Numbers """ + nl = "\n" + c = Path.Command("G0 X10 Y20 Z30") - self.docobj.Path = Path.Path([c]) - postables = [self.docobj] + self.profile_op.Path = Path.Path([c]) - args = "--no-header --line-numbers --no-show-editor" - gcode = postprocessor.export(postables, "-", args) - result = gcode.splitlines()[5] - expected = "N150 G0 X10.0000 Y20.0000 Z30.0000" + self.job.PostProcessorArgs = "--no-header --line-numbers --no-show-editor" + gcode = self.post.export()[0][1] + # print(f"--------{nl}{gcode}--------{nl}") + result = gcode.splitlines()[14] + expected = "N240 G0 X10.0000 Y20.0000 Z30.0000" self.assertEqual(result, expected) def test030(self): """ Test Pre-amble """ + nl = "\n" - self.docobj.Path = Path.Path([]) - postables = [self.docobj] + self.profile_op.Path = Path.Path([]) - # - # The original centroid postprocessor does not have a - # --preamble option. We end up with the default preamble. - # - args = "--no-header --no-comments --preamble='G18 G55' --no-show-editor" - gcode = postprocessor.export(postables, "-", args) + self.job.PostProcessorArgs = ( + "--no-header --no-comments --preamble='G18 G55' --no-show-editor" + ) + gcode = self.post.export()[0][1] + # print(f"--------{nl}{gcode}--------{nl}") result = gcode.splitlines()[1] - self.assertEqual(result, "G53 G00 G17") + self.assertEqual(result, "G18 G55") def test040(self): """ Test Post-amble """ - self.docobj.Path = Path.Path([]) - postables = [self.docobj] - # - # The original centroid postprocessor does not have a - # --postamble option. We end up with the default postamble. - # - args = "--no-header --no-comments --postamble='G0 Z50\nM2' --no-show-editor" - gcode = postprocessor.export(postables, "-", args) - self.assertEqual(gcode.splitlines()[-1], "M99") + nl = "\n" + + self.profile_op.Path = Path.Path([]) + + self.job.PostProcessorArgs = ( + "--no-header --no-comments --postamble='G0 Z50\nM2' --no-show-editor" + ) + gcode = self.post.export()[0][1] + # print(f"--------{nl}{gcode}--------{nl}") + result = gcode.splitlines()[-2] + self.assertEqual(result, "G0 Z50") + self.assertEqual(gcode.splitlines()[-1], "M2") def test050(self): """ Test inches """ + nl = "\n" c = Path.Command("G0 X10 Y20 Z30") - self.docobj.Path = Path.Path([c]) - postables = [self.docobj] - args = "--no-header --inches --no-show-editor" - gcode = postprocessor.export(postables, "-", args) - self.assertEqual(gcode.splitlines()[3], "G20") + self.profile_op.Path = Path.Path([c]) - result = gcode.splitlines()[5] + self.job.PostProcessorArgs = "--no-header --inches --no-show-editor" + gcode = self.post.export()[0][1] + # print(f"--------{nl}{gcode}--------{nl}") + self.assertEqual(gcode.splitlines()[4], "G20") + + result = gcode.splitlines()[14] expected = "G0 X0.3937 Y0.7874 Z1.1811" self.assertEqual(result, expected) - args = "--no-header --inches --axis-precision=2 --no-show-editor" - gcode = postprocessor.export(postables, "-", args) - result = gcode.splitlines()[5] + self.job.PostProcessorArgs = "--no-header --inches --axis-precision=2 --no-show-editor" + gcode = self.post.export()[0][1] + # print(f"--------{nl}{gcode}--------{nl}") + result = gcode.splitlines()[14] expected = "G0 X0.39 Y0.79 Z1.18" self.assertEqual(result, expected) @@ -230,20 +287,18 @@ M99 Test test modal Suppress the command name if the same as previous """ + nl = "\n" + c = Path.Command("G0 X10 Y20 Z30") c1 = Path.Command("G0 X10 Y30 Z30") - self.docobj.Path = Path.Path([c, c1]) - postables = [self.docobj] + self.profile_op.Path = Path.Path([c, c1]) - # - # The original centroid postprocessor does not have a - # --modal option. We end up with the original gcode. - # - args = "--no-header --modal --no-show-editor" - gcode = postprocessor.export(postables, "-", args) - result = gcode.splitlines()[6] - expected = "G0 X10.0000 Y30.0000 Z30.0000" + self.job.PostProcessorArgs = "--no-header --modal --no-show-editor" + gcode = self.post.export()[0][1] + # print(f"--------{nl}{gcode}--------{nl}") + result = gcode.splitlines()[15] + expected = "X10.0000 Y30.0000 Z30.0000" self.assertEqual(result, expected) def test070(self): @@ -251,58 +306,56 @@ M99 Test axis modal Suppress the axis coordinate if the same as previous """ + nl = "\n" + c = Path.Command("G0 X10 Y20 Z30") c1 = Path.Command("G0 X10 Y30 Z30") - self.docobj.Path = Path.Path([c, c1]) - postables = [self.docobj] + self.profile_op.Path = Path.Path([c, c1]) - # - # The original centroid postprocessor does not have an - # --axis-modal option. We end up with the original gcode. - # - args = "--no-header --axis-modal --no-show-editor" - gcode = postprocessor.export(postables, "-", args) - result = gcode.splitlines()[6] - expected = "G0 X10.0000 Y30.0000 Z30.0000" + self.job.PostProcessorArgs = "--no-header --axis-modal --no-show-editor" + gcode = self.post.export()[0][1] + # print(f"--------{nl}{gcode}--------{nl}") + result = gcode.splitlines()[15] + expected = "G0 Y30.0000" self.assertEqual(result, expected) def test080(self): """ Test tool change """ + nl = "\n" + c = Path.Command("M6 T2") c2 = Path.Command("M3 S3000") - self.docobj.Path = Path.Path([c, c2]) - postables = [self.docobj] - args = "--no-header --no-show-editor" - gcode = postprocessor.export(postables, "-", args) - self.assertEqual(gcode.splitlines()[5], "G43 H2") - self.assertEqual(gcode.splitlines()[6], "M6 T2") - self.assertEqual(gcode.splitlines()[7], "M3 S3000") + self.profile_op.Path = Path.Path([c, c2]) + + self.job.PostProcessorArgs = "--no-header --no-show-editor" + gcode = self.post.export()[0][1] + # print(f"--------{nl}{gcode}--------{nl}") + self.assertEqual(gcode.splitlines()[15], "M6 T2") + self.assertEqual(gcode.splitlines()[16], "M3 S3000") # suppress TLO - # - # The original centroid postprocessor does not have an - # --no-tlo option. We end up with the original gcode. - # - args = "--no-header --no-tlo --no-show-editor" - gcode = postprocessor.export(postables, "-", args) - self.assertEqual(gcode.splitlines()[6], "M3 S3000") + self.job.PostProcessorArgs = "--no-header --no-tlo --no-show-editor" + gcode = self.post.export()[0][1] + # print(f"--------{nl}{gcode}--------{nl}") + self.assertEqual(gcode.splitlines()[16], "M3 S3000") def test090(self): """ Test comment """ + nl = "\n" c = Path.Command("(comment)") - self.docobj.Path = Path.Path([c]) - postables = [self.docobj] + self.profile_op.Path = Path.Path([c]) - args = "--no-header --no-show-editor" - gcode = postprocessor.export(postables, "-", args) - result = gcode.splitlines()[5] + self.job.PostProcessorArgs = "--no-header --no-show-editor" + gcode = self.post.export()[0][1] + # print(f"--------{nl}{gcode}--------{nl}") + result = gcode.splitlines()[14] expected = ";comment" self.assertEqual(result, expected) diff --git a/src/Mod/CAM/CAMTests/TestRefactoredTestDressupPost.py b/src/Mod/CAM/CAMTests/TestDressupPost.py similarity index 97% rename from src/Mod/CAM/CAMTests/TestRefactoredTestDressupPost.py rename to src/Mod/CAM/CAMTests/TestDressupPost.py index b7268bb385..258bfd5d87 100644 --- a/src/Mod/CAM/CAMTests/TestRefactoredTestDressupPost.py +++ b/src/Mod/CAM/CAMTests/TestDressupPost.py @@ -34,8 +34,8 @@ Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) Path.Log.trackModule(Path.Log.thisModule()) -class TestRefactoredTestDressupPost(PathTestUtils.PathTestBase): - """Test the refactored_test_post.py postprocessor command line arguments.""" +class TestDressupPost(PathTestUtils.PathTestBase): + """Test the test_post.py postprocessor command line arguments.""" @classmethod def setUpClass(cls): @@ -51,7 +51,7 @@ class TestRefactoredTestDressupPost(PathTestUtils.PathTestBase): FreeCAD.ConfigSet("SuppressRecomputeRequiredDialog", "True") cls.doc = FreeCAD.open(FreeCAD.getHomePath() + "/Mod/CAM/CAMTests/dressuptest.FCStd") cls.job = cls.doc.getObject("Job") - cls.post = PostProcessorFactory.get_post_processor(cls.job, "refactored_test") + cls.post = PostProcessorFactory.get_post_processor(cls.job, "test") # there are 4 operations in dressuptest.FCStd # every operation uses a different ToolController diff --git a/src/Mod/CAM/CAMTests/TestRefactoredLinuxCNCPost.py b/src/Mod/CAM/CAMTests/TestGenericPost.py similarity index 87% rename from src/Mod/CAM/CAMTests/TestRefactoredLinuxCNCPost.py rename to src/Mod/CAM/CAMTests/TestGenericPost.py index 7657272b54..c2de12dbdc 100644 --- a/src/Mod/CAM/CAMTests/TestRefactoredLinuxCNCPost.py +++ b/src/Mod/CAM/CAMTests/TestGenericPost.py @@ -22,18 +22,12 @@ # * * # *************************************************************************** -# *************************************************************************** -# * Note: TestRefactoredMassoG3Post.py is a modified clone of this file * -# * any changes to this file should be applied to the other * -# * * -# * * -# *************************************************************************** - import FreeCAD import Path import CAMTests.PathTestUtils as PathTestUtils +import CAMTests.PostTestMocks as PostTestMocks from Path.Post.Processor import PostProcessorFactory @@ -41,8 +35,8 @@ Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) Path.Log.trackModule(Path.Log.thisModule()) -class TestRefactoredLinuxCNCPost(PathTestUtils.PathTestBase): - """Test the refactored_linuxcnc_post.py postprocessor.""" +class TestGenericPost(PathTestUtils.PathTestBase): + """Test the generic postprocessor.""" @classmethod def setUpClass(cls): @@ -56,16 +50,13 @@ class TestRefactoredLinuxCNCPost(PathTestUtils.PathTestBase): is able to call static methods within this same class. """ - FreeCAD.ConfigSet("SuppressRecomputeRequiredDialog", "True") - cls.doc = FreeCAD.open(FreeCAD.getHomePath() + "/Mod/CAM/CAMTests/boxtest.fcstd") - cls.job = cls.doc.getObject("Job") - cls.post = PostProcessorFactory.get_post_processor(cls.job, "refactored_linuxcnc") - # locate the operation named "Profile" - for op in cls.job.Operations.Group: - if op.Label == "Profile": - # remember the "Profile" operation - cls.profile_op = op - return + # Create mock job with default operation and tool controller + cls.job, cls.profile_op, cls.tool_controller = ( + PostTestMocks.create_default_job_with_operation() + ) + + # Create postprocessor using the mock job + cls.post = PostProcessorFactory.get_post_processor(cls.job, "generic") @classmethod def tearDownClass(cls): @@ -77,8 +68,8 @@ class TestRefactoredLinuxCNCPost(PathTestUtils.PathTestBase): have access to the class `self` reference. This method is able to call static methods within this same class. """ - FreeCAD.closeDocument(cls.doc.Name) - FreeCAD.ConfigSet("SuppressRecomputeRequiredDialog", "") + # No cleanup needed for mock objects + pass # Setup and tear down methods called before and after each unit test @@ -114,32 +105,29 @@ class TestRefactoredLinuxCNCPost(PathTestUtils.PathTestBase): # Only test length of result. self.job.PostProcessorArgs = "--no-show-editor" gcode = self.post.export()[0][1] - # print(f"--------{nl}{gcode}--------{nl}") - self.assertTrue(len(gcode.splitlines()) == 26) + # print(f"--------{nl}Actual line count: {len(gcode.splitlines())}{nl}{gcode}--------{nl}") + self.assertTrue(len(gcode.splitlines()) == 23) # Test without header expected = """(Begin preamble) -G17 G54 G40 G49 G80 G90 +G90 G21 -(Begin operation: Fixture) -(Machine units: mm/min) -G54 -(Finish operation: Fixture) (Begin operation: TC: Default Tool) (Machine units: mm/min) -(TC: Default Tool) (Begin toolchange) M5 M6 T1 G43 H1 +M3 S1000 (Finish operation: TC: Default Tool) +(Begin operation: Fixture) +(Machine units: mm/min) +G54 +(Finish operation: Fixture) (Begin operation: Profile) (Machine units: mm/min) (Finish operation: Profile) (Begin postamble) -M05 -G17 G54 G90 G80 G40 -M2 """ self.profile_op.Path = Path.Path([]) @@ -151,15 +139,13 @@ M2 self.assertEqual(gcode, expected) # test without comments - expected = """G17 G54 G40 G49 G80 G90 + expected = """G90 G21 -G54 M5 M6 T1 G43 H1 -M05 -G17 G54 G90 G80 G40 -M2 +M3 S1000 +G54 """ # args = ("--no-header --no-comments --no-show-editor --precision=2") @@ -222,8 +208,9 @@ M2 ) gcode = self.post.export()[0][1] # print(f"--------{nl}{gcode}--------{nl}") - result = gcode.splitlines()[0] - self.assertEqual(result, "G18 G55") + lines = gcode.splitlines() + # Preamble should be in the output + self.assertIn("G18 G55", gcode) def test040(self): """ diff --git a/src/Mod/CAM/CAMTests/TestGrblLegacyPost.py b/src/Mod/CAM/CAMTests/TestGrblLegacyPost.py new file mode 100644 index 0000000000..aae4d38de4 --- /dev/null +++ b/src/Mod/CAM/CAMTests/TestGrblLegacyPost.py @@ -0,0 +1,254 @@ +# *************************************************************************** +# * Copyright (c) 2022 sliptonic * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +import FreeCAD + +import Path +import CAMTests.PathTestUtils as PathTestUtils +from importlib import reload +from Path.Post.scripts import grbl_legacy_post as postprocessor + + +Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) +Path.Log.trackModule(Path.Log.thisModule()) + + +class TestGrblLegacyPost(PathTestUtils.PathTestBase): + @classmethod + def setUpClass(cls): + """setUpClass()... + This method is called upon instantiation of this test class. Add code + and objects here that are needed for the duration of the test() methods + in this class. In other words, set up the 'global' test environment + here; use the `setUp()` method to set up a 'local' test environment. + This method does not have access to the class `self` reference, but it + is able to call static methods within this same class. + """ + + # Open existing FreeCAD document with test geometry + FreeCAD.newDocument("Unnamed") + + @classmethod + def tearDownClass(cls): + """tearDownClass()... + This method is called prior to destruction of this test class. Add + code and objects here that cleanup the test environment after the + test() methods in this class have been executed. This method does + not have access to the class `self` reference. This method is able + to call static methods within this same class. + """ + # Close geometry document without saving + FreeCAD.closeDocument(FreeCAD.ActiveDocument.Name) + + # Setup and tear down methods called before and after each unit test + def setUp(self): + """setUp()... + This method is called prior to each `test()` method. Add code and + objects here that are needed for multiple `test()` methods. + """ + self.doc = FreeCAD.ActiveDocument + self.con = FreeCAD.Console + self.docobj = FreeCAD.ActiveDocument.addObject("Path::Feature", "testpath") + reload( + postprocessor + ) # technical debt. This shouldn't be necessary but here to bypass a bug + + def tearDown(self): + """tearDown()... + This method is called after each test() method. Add cleanup instructions here. + Such cleanup instructions will likely undo those in the setUp() method. + """ + FreeCAD.ActiveDocument.removeObject("testpath") + + def test000(self): + """Test Output Generation. + Empty path. Produces only the preamble and postable. + """ + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + # Test generating with header + # Header contains a time stamp that messes up unit testing. Only test + # length of result. + args = "--no-show-editor" + gcode = postprocessor.export(postables, "-", args) + self.assertEqual(len(gcode.splitlines()), 13) + + # Test without header + expected = """(Begin preamble) +G17 G90 +G21 +(Begin operation: testpath) +(Path: testpath) +(Finish operation: testpath) +(Begin postamble) +M5 +G17 G90 +M2 +""" + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + # args = ("--no-header --no-comments --no-show-editor --precision=2") + gcode = postprocessor.export(postables, "-", args) + self.assertEqual(gcode, expected) + + # test without comments + expected = """G17 G90 +G21 +M5 +G17 G90 +M2 +""" + + args = "--no-header --no-comments --no-show-editor" + # args = ("--no-header --no-comments --no-show-editor --precision=2") + gcode = postprocessor.export(postables, "-", args) + self.assertEqual(gcode, expected) + + def test010(self): + """Test command Generation. + Test Precision + Test imperial / inches + """ + c = Path.Command("G0 X10 Y20 Z30") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "-", args) + result = gcode.splitlines()[5] + expected = "G0 X10.000 Y20.000 Z30.000" + self.assertEqual(result, expected) + + args = "--no-header --precision=2 --no-show-editor" + gcode = postprocessor.export(postables, "-", args) + result = gcode.splitlines()[5] + expected = "G0 X10.00 Y20.00 Z30.00" + self.assertEqual(result, expected) + + def test020(self): + """ + Test Line Numbers + """ + c = Path.Command("G0 X10 Y20 Z30") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --line-numbers --no-show-editor" + gcode = postprocessor.export(postables, "-", args) + result = gcode.splitlines()[5] + expected = "N150 G0 X10.000 Y20.000 Z30.000" + self.assertEqual(result, expected) + + def test030(self): + """ + Test Pre-amble + """ + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + args = "--no-header --no-comments --preamble='G18 G55\n' --no-show-editor" + gcode = postprocessor.export(postables, "-", args) + result = gcode.splitlines()[0] + self.assertEqual(result, "G18 G55") + + def test040(self): + """ + Test Post-amble + """ + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + args = "--no-header --no-comments --postamble='G0 Z50\nM2' --no-show-editor" + gcode = postprocessor.export(postables, "-", args) + result = gcode.splitlines()[-2] + self.assertEqual(result, "G0 Z50") + self.assertEqual(gcode.splitlines()[-1], "M2") + + def test050(self): + """ + Test inches + """ + + c = Path.Command("G0 X10 Y20 Z30") + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --inches --no-show-editor" + gcode = postprocessor.export(postables, "-", args) + self.assertEqual(gcode.splitlines()[2], "G20") + + result = gcode.splitlines()[5] + expected = "G0 X0.3937 Y0.7874 Z1.1811" + self.assertEqual(result, expected) + + # Technical debt. The following test fails. Precision not working + # with imperial units. + + # args = ("--no-header --inches --precision=2") + # gcode = postprocessor.export(postables, "-", args) + # result = gcode.splitlines()[5] + # expected = "G0 X0.39 Y0.78 Z1.18 " + # self.assertEqual(result, expected) + + def test080(self): + """ + Test tool change + """ + c = Path.Command("M6 T2") + c2 = Path.Command("M3 S3000") + self.docobj.Path = Path.Path([c, c2]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "-", args) + self.assertEqual(gcode.splitlines()[6], "( M6 T2 )") + self.assertEqual(gcode.splitlines()[7], "M3 S3000") + + # suppress TLO + # + # The grbl postprocessor does not have a --no-tlo option. + # + # args = "--no-header --no-tlo --no-show-editor" + # gcode = postprocessor.export(postables, "-", args) + # self.assertEqual(gcode.splitlines()[7], "M3 S3000 ") + + def test090(self): + """ + Test comment + """ + + c = Path.Command("(comment)") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "-", args) + result = gcode.splitlines()[5] + expected = "(comment)" + self.assertEqual(result, expected) diff --git a/src/Mod/CAM/CAMTests/TestGrblPost.py b/src/Mod/CAM/CAMTests/TestGrblPost.py index 0a584624e6..d3e0bbaee5 100644 --- a/src/Mod/CAM/CAMTests/TestGrblPost.py +++ b/src/Mod/CAM/CAMTests/TestGrblPost.py @@ -2,6 +2,7 @@ # *************************************************************************** # * Copyright (c) 2022 sliptonic * +# * Copyright (c) 2022 - 2025 Larry Woestman * # * * # * This program is free software; you can redistribute it and/or modify * # * it under the terms of the GNU Lesser General Public License (LGPL) * @@ -25,8 +26,7 @@ import FreeCAD import Path import CAMTests.PathTestUtils as PathTestUtils -from importlib import reload -from Path.Post.scripts import grbl_post as postprocessor +from Path.Post.Processor import PostProcessorFactory Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) @@ -34,9 +34,12 @@ Path.Log.trackModule(Path.Log.thisModule()) class TestGrblPost(PathTestUtils.PathTestBase): + """Test the grbl_post.py postprocessor.""" + @classmethod def setUpClass(cls): """setUpClass()... + This method is called upon instantiation of this test class. Add code and objects here that are needed for the duration of the test() methods in this class. In other words, set up the 'global' test environment @@ -45,109 +48,134 @@ class TestGrblPost(PathTestUtils.PathTestBase): is able to call static methods within this same class. """ - # Open existing FreeCAD document with test geometry - FreeCAD.newDocument("Unnamed") + FreeCAD.ConfigSet("SuppressRecomputeRequiredDialog", "True") + cls.doc = FreeCAD.open(FreeCAD.getHomePath() + "/Mod/CAM/CAMTests/boxtest.fcstd") + cls.job = cls.doc.getObject("Job") + cls.post = PostProcessorFactory.get_post_processor(cls.job, "grbl") + # locate the operation named "Profile" + for op in cls.job.Operations.Group: + if op.Label == "Profile": + # remember the "Profile" operation + cls.profile_op = op + return @classmethod def tearDownClass(cls): """tearDownClass()... + This method is called prior to destruction of this test class. Add code and objects here that cleanup the test environment after the - test() methods in this class have been executed. This method does - not have access to the class `self` reference. This method is able - to call static methods within this same class. + test() methods in this class have been executed. This method does not + have access to the class `self` reference. This method + is able to call static methods within this same class. """ - # Close geometry document without saving - FreeCAD.closeDocument(FreeCAD.ActiveDocument.Name) + FreeCAD.closeDocument(cls.doc.Name) + FreeCAD.ConfigSet("SuppressRecomputeRequiredDialog", "") # Setup and tear down methods called before and after each unit test + def setUp(self): """setUp()... + This method is called prior to each `test()` method. Add code and objects here that are needed for multiple `test()` methods. """ - self.doc = FreeCAD.ActiveDocument - self.con = FreeCAD.Console - self.docobj = FreeCAD.ActiveDocument.addObject("Path::Feature", "testpath") - reload( - postprocessor - ) # technical debt. This shouldn't be necessary but here to bypass a bug + # allow a full length "diff" if an error occurs + self.maxDiff = None + # reinitialize the postprocessor data structures between tests + self.post.reinitialize() def tearDown(self): """tearDown()... + This method is called after each test() method. Add cleanup instructions here. Such cleanup instructions will likely undo those in the setUp() method. """ - FreeCAD.ActiveDocument.removeObject("testpath") + pass def test000(self): """Test Output Generation. Empty path. Produces only the preamble and postable. """ + nl = "\n" - self.docobj.Path = Path.Path([]) - postables = [self.docobj] + self.profile_op.Path = Path.Path([]) # Test generating with header - # Header contains a time stamp that messes up unit testing. Only test - # length of result. - args = "--no-show-editor" - gcode = postprocessor.export(postables, "-", args) - self.assertTrue(len(gcode.splitlines()) == 13) + # Header contains a time stamp that messes up unit testing. + # Only test length of result. + self.job.PostProcessorArgs = "--no-show-editor" + gcode = self.post.export()[0][1] + # print(f"--------{nl}{gcode}--------{nl}") + self.assertTrue(len(gcode.splitlines()) == 24) # Test without header expected = """(Begin preamble) G17 G90 G21 -(Begin operation: testpath) -(Path: testpath) -(Finish operation: testpath) +(Begin operation: Fixture) +(Path: Fixture) +G54 +(Finish operation: Fixture) +(Begin operation: TC: Default Tool) +(Path: TC: Default Tool) +(TC: Default Tool) +(Begin toolchange) +(M6 T1) +(Finish operation: TC: Default Tool) +(Begin operation: Profile) +(Path: Profile) +(Finish operation: Profile) (Begin postamble) M5 G17 G90 M2 """ - self.docobj.Path = Path.Path([]) - postables = [self.docobj] + self.profile_op.Path = Path.Path([]) - args = "--no-header --no-show-editor" # args = ("--no-header --no-comments --no-show-editor --precision=2") - gcode = postprocessor.export(postables, "-", args) + self.job.PostProcessorArgs = "--no-header --no-show-editor" + gcode = self.post.export()[0][1] + # print(f"--------{nl}{gcode}--------{nl}") self.assertEqual(gcode, expected) # test without comments expected = """G17 G90 G21 +G54 M5 G17 G90 M2 """ - args = "--no-header --no-comments --no-show-editor" # args = ("--no-header --no-comments --no-show-editor --precision=2") - gcode = postprocessor.export(postables, "-", args) + self.job.PostProcessorArgs = "--no-header --no-comments --no-show-editor" + gcode = self.post.export()[0][1] + # print(f"--------{nl}{gcode}--------{nl}") self.assertEqual(gcode, expected) def test010(self): """Test command Generation. Test Precision - Test imperial / inches """ + nl = "\n" + c = Path.Command("G0 X10 Y20 Z30") - self.docobj.Path = Path.Path([c]) - postables = [self.docobj] + self.profile_op.Path = Path.Path([c]) - args = "--no-header --no-show-editor" - gcode = postprocessor.export(postables, "-", args) - result = gcode.splitlines()[5] + self.job.PostProcessorArgs = "--no-header --no-show-editor" + gcode = self.post.export()[0][1] + # print(f"--------{nl}{gcode}--------{nl}") + result = gcode.splitlines()[15] expected = "G0 X10.000 Y20.000 Z30.000" self.assertEqual(result, expected) - args = "--no-header --precision=2 --no-show-editor" - gcode = postprocessor.export(postables, "-", args) - result = gcode.splitlines()[5] + self.job.PostProcessorArgs = "--no-header --precision=2 --no-show-editor" + gcode = self.post.export()[0][1] + # print(f"--------{nl}{gcode}--------{nl}") + result = gcode.splitlines()[15] expected = "G0 X10.00 Y20.00 Z30.00" self.assertEqual(result, expected) @@ -155,27 +183,32 @@ M2 """ Test Line Numbers """ + nl = "\n" + c = Path.Command("G0 X10 Y20 Z30") - self.docobj.Path = Path.Path([c]) - postables = [self.docobj] + self.profile_op.Path = Path.Path([c]) - args = "--no-header --line-numbers --no-show-editor" - gcode = postprocessor.export(postables, "-", args) - result = gcode.splitlines()[5] - expected = "N150 G0 X10.000 Y20.000 Z30.000" + self.job.PostProcessorArgs = "--no-header --line-numbers --no-show-editor" + gcode = self.post.export()[0][1] + # print(f"--------{nl}{gcode}--------{nl}") + result = gcode.splitlines()[15] + expected = "N250 G0 X10.000 Y20.000 Z30.000" self.assertEqual(result, expected) def test030(self): """ Test Pre-amble """ + nl = "\n" - self.docobj.Path = Path.Path([]) - postables = [self.docobj] + self.profile_op.Path = Path.Path([]) - args = "--no-header --no-comments --preamble='G18 G55\n' --no-show-editor" - gcode = postprocessor.export(postables, "-", args) + self.job.PostProcessorArgs = ( + "--no-header --no-comments --preamble='G18 G55' --no-show-editor" + ) + gcode = self.post.export()[0][1] + # print(f"--------{nl}{gcode}--------{nl}") result = gcode.splitlines()[0] self.assertEqual(result, "G18 G55") @@ -183,10 +216,15 @@ M2 """ Test Post-amble """ - self.docobj.Path = Path.Path([]) - postables = [self.docobj] - args = "--no-header --no-comments --postamble='G0 Z50\nM2' --no-show-editor" - gcode = postprocessor.export(postables, "-", args) + nl = "\n" + + self.profile_op.Path = Path.Path([]) + + self.job.PostProcessorArgs = ( + "--no-header --no-comments --postamble='G0 Z50\nM2' --no-show-editor" + ) + gcode = self.post.export()[0][1] + # print(f"--------{nl}{gcode}--------{nl}") result = gcode.splitlines()[-2] self.assertEqual(result, "G0 Z50") self.assertEqual(gcode.splitlines()[-1], "M2") @@ -195,102 +233,121 @@ M2 """ Test inches """ + nl = "\n" c = Path.Command("G0 X10 Y20 Z30") - self.docobj.Path = Path.Path([c]) - postables = [self.docobj] - args = "--no-header --inches --no-show-editor" - gcode = postprocessor.export(postables, "-", args) + self.profile_op.Path = Path.Path([c]) + + self.job.PostProcessorArgs = "--no-header --inches --no-show-editor" + gcode = self.post.export()[0][1] + # print(f"--------{nl}{gcode}--------{nl}") self.assertEqual(gcode.splitlines()[2], "G20") - result = gcode.splitlines()[5] + result = gcode.splitlines()[15] expected = "G0 X0.3937 Y0.7874 Z1.1811" self.assertEqual(result, expected) - # Technical debt. The following test fails. Precision not working - # with imperial units. - - # args = ("--no-header --inches --precision=2") - # gcode = postprocessor.export(postables, "-", args) - # result = gcode.splitlines()[5] - # expected = "G0 X0.39 Y0.78 Z1.18 " - # self.assertEqual(result, expected) + self.job.PostProcessorArgs = "--no-header --inches --precision=2 --no-show-editor" + gcode = self.post.export()[0][1] + # print(f"--------{nl}{gcode}--------{nl}") + result = gcode.splitlines()[15] + expected = "G0 X0.39 Y0.79 Z1.18" + self.assertEqual(result, expected) def test060(self): """ Test test modal Suppress the command name if the same as previous """ + nl = "\n" + c = Path.Command("G0 X10 Y20 Z30") c1 = Path.Command("G0 X10 Y30 Z30") - self.docobj.Path = Path.Path([c, c1]) - postables = [self.docobj] + self.profile_op.Path = Path.Path([c, c1]) - # - # The grbl postprocessor does not have a --modal option. - # - # args = "--no-header --modal --no-show-editor" - # gcode = postprocessor.export(postables, "-", args) - # result = gcode.splitlines()[6] - # expected = "X10.000 Y30.000 Z30.000 " - # self.assertEqual(result, expected) + self.job.PostProcessorArgs = "--no-header --modal --no-show-editor" + gcode = self.post.export()[0][1] + # print(f"--------{nl}{gcode}--------{nl}") + result = gcode.splitlines()[16] + expected = "X10.000 Y30.000 Z30.000" + self.assertEqual(result, expected) def test070(self): """ Test axis modal Suppress the axis coordinate if the same as previous """ + nl = "\n" + c = Path.Command("G0 X10 Y20 Z30") c1 = Path.Command("G0 X10 Y30 Z30") - self.docobj.Path = Path.Path([c, c1]) - postables = [self.docobj] + self.profile_op.Path = Path.Path([c, c1]) - # - # The grbl postprocessor does not have a --axis-modal option. - # - # args = "--no-header --axis-modal --no-show-editor" - # gcode = postprocessor.export(postables, "-", args) - # result = gcode.splitlines()[6] - # expected = "G0 Y30.000 " - # self.assertEqual(result, expected) + self.job.PostProcessorArgs = "--no-header --axis-modal --no-show-editor" + gcode = self.post.export()[0][1] + # print(f"--------{nl}{gcode}--------{nl}") + result = gcode.splitlines()[16] + expected = "G0 Y30.000" + self.assertEqual(result, expected) def test080(self): """ Test tool change """ + nl = "\n" + c = Path.Command("M6 T2") c2 = Path.Command("M3 S3000") - self.docobj.Path = Path.Path([c, c2]) - postables = [self.docobj] - args = "--no-header --no-show-editor" - gcode = postprocessor.export(postables, "-", args) - self.assertEqual(gcode.splitlines()[6], "( M6 T2 )") - self.assertEqual(gcode.splitlines()[7], "M3 S3000") + self.profile_op.Path = Path.Path([c, c2]) + + self.job.PostProcessorArgs = "--no-header --no-show-editor" + gcode = self.post.export()[0][1] + # print(f"--------{nl}{gcode}--------{nl}") + self.assertEqual(gcode.splitlines()[16], "(M6 T2)") + self.assertEqual(gcode.splitlines()[17], "M3 S3000") # suppress TLO - # - # The grbl postprocessor does not have a --no-tlo option. - # - # args = "--no-header --no-tlo --no-show-editor" - # gcode = postprocessor.export(postables, "-", args) - # self.assertEqual(gcode.splitlines()[7], "M3 S3000 ") + self.job.PostProcessorArgs = "--no-header --no-tlo --no-show-editor" + gcode = self.post.export()[0][1] + # print(f"--------{nl}{gcode}--------{nl}") + self.assertEqual(gcode.splitlines()[17], "M3 S3000") def test090(self): """ Test comment """ + nl = "\n" c = Path.Command("(comment)") - self.docobj.Path = Path.Path([c]) - postables = [self.docobj] + self.profile_op.Path = Path.Path([c]) - args = "--no-header --no-show-editor" - gcode = postprocessor.export(postables, "-", args) - result = gcode.splitlines()[5] + self.job.PostProcessorArgs = "--no-header --no-show-editor" + gcode = self.post.export()[0][1] + # print(f"--------{nl}{gcode}--------{nl}") + result = gcode.splitlines()[15] expected = "(comment)" self.assertEqual(result, expected) + + def test100(self): + """ + Test if coolant is enabled. + """ + nl = "\n" + + c = Path.Command("M7") + c1 = Path.Command("M8") + c2 = Path.Command("M9") + + self.profile_op.Path = Path.Path([c, c1, c2]) + + self.job.PostProcessorArgs = "--no-header --no-show-editor" + gcode = self.post.export()[0][1] + # print(f"--------{nl}{gcode}--------{nl}") + self.assertEqual(gcode.splitlines()[15], "M7") + self.assertEqual(gcode.splitlines()[16], "M8") + self.assertEqual(gcode.splitlines()[17], "M9") diff --git a/src/Mod/CAM/CAMTests/TestLinuxCNCLegacyPost.py b/src/Mod/CAM/CAMTests/TestLinuxCNCLegacyPost.py new file mode 100644 index 0000000000..63e0b7849a --- /dev/null +++ b/src/Mod/CAM/CAMTests/TestLinuxCNCLegacyPost.py @@ -0,0 +1,420 @@ +# *************************************************************************** +# * Copyright (c) 2022 sliptonic * +# * Copyright (c) 2023 Larry Woestman * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +import FreeCAD + +import Path +import CAMTests.PathTestUtils as PathTestUtils +from importlib import reload +from Path.Post.scripts import linuxcnc_legacy_post as postprocessor + +Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) +Path.Log.trackModule(Path.Log.thisModule()) + + +class TestLinuxCNCLegacyPost(PathTestUtils.PathTestBase): + @classmethod + def setUpClass(cls): + """setUpClass()... + This method is called upon instantiation of this test class. Add code + and objects here that are needed for the duration of the test() methods + in this class. In other words, set up the 'global' test environment + here; use the `setUp()` method to set up a 'local' test environment. + This method does not have access to the class `self` reference, but it + is able to call static methods within this same class. + """ + + # Open existing FreeCAD document with test geometry + FreeCAD.newDocument("Unnamed") + + @classmethod + def tearDownClass(cls): + """tearDownClass()... + This method is called prior to destruction of this test class. Add + code and objects here that cleanup the test environment after the + test() methods in this class have been executed. This method does + not have access to the class `self` reference. This method is able + to call static methods within this same class. + """ + # Close geometry document without saving + FreeCAD.closeDocument(FreeCAD.ActiveDocument.Name) + + # Setup and tear down methods called before and after each unit test + def setUp(self): + """setUp()... + This method is called prior to each `test()` method. Add code and + objects here that are needed for multiple `test()` methods. + """ + self.doc = FreeCAD.ActiveDocument + self.con = FreeCAD.Console + self.docobj = FreeCAD.ActiveDocument.addObject("Path::Feature", "testpath") + reload( + postprocessor + ) # technical debt. This shouldn't be necessary but here to bypass a bug + + def tearDown(self): + """tearDown()... + This method is called after each test() method. Add cleanup instructions here. + Such cleanup instructions will likely undo those in the setUp() method. + """ + FreeCAD.ActiveDocument.removeObject("testpath") + + def compare_sixth_line(self, path_string, expected, args, debug=False): + """Perform a test with a single comparison to the sixth line of the output.""" + nl = "\n" + if path_string: + self.docobj.Path = Path.Path([Path.Command(path_string)]) + else: + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + gcode = postprocessor.export(postables, "-", args) + if debug: + print(f"--------{nl}{gcode}--------{nl}") + self.assertEqual(gcode.splitlines()[5], expected) + + def test000(self): + """Test Output Generation. + Empty path. Produces only the preamble and postable. + """ + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + # Test generating with header + # Header contains a time stamp that messes up unit testing. + # Only test length of result. + args = "--no-show-editor" + gcode = postprocessor.export(postables, "-", args) + self.assertTrue(len(gcode.splitlines()) == 13) + + # Test without header + expected = """(begin preamble) +G17 G54 G40 G49 G80 G90 +G21 +(begin operation: testpath) +(machine units: mm/min) +(finish operation: testpath) +(begin postamble) +M05 +G17 G54 G90 G80 G40 +M2 +""" + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + # args = ("--no-header --no-comments --no-show-editor --precision=2") + gcode = postprocessor.export(postables, "-", args) + self.assertEqual(gcode, expected) + + # test without comments + expected = """G17 G54 G40 G49 G80 G90 +G21 +M05 +G17 G54 G90 G80 G40 +M2 +""" + + args = "--no-header --no-comments --no-show-editor" + # args = ("--no-header --no-comments --no-show-editor --precision=2") + gcode = postprocessor.export(postables, "-", args) + self.assertEqual(gcode, expected) + + def test010(self): + """Test command Generation. + Test Precision + Test imperial / inches + """ + self.compare_sixth_line( + "G0 X10 Y20 Z30", "G0 X10.000 Y20.000 Z30.000 ", "--no-header --no-show-editor" + ) + self.compare_sixth_line( + "G0 X10 Y20 Z30", + "G0 X10.00 Y20.00 Z30.00 ", + "--no-header --precision=2 --no-show-editor", + ) + + def test020(self): + """ + Test Line Numbers + """ + self.compare_sixth_line( + "G0 X10 Y20 Z30", + "N160 G0 X10.000 Y20.000 Z30.000 ", + "--no-header --line-numbers --no-show-editor", + ) + + def test030(self): + """ + Test Pre-amble + """ + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + args = "--no-header --no-comments --preamble='G18 G55' --no-show-editor" + gcode = postprocessor.export(postables, "-", args) + result = gcode.splitlines()[0] + self.assertEqual(result, "G18 G55") + + def test040(self): + """ + Test Post-amble + """ + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + args = "--no-header --no-comments --postamble='G0 Z50\nM2' --no-show-editor" + gcode = postprocessor.export(postables, "-", args) + result = gcode.splitlines()[-2] + self.assertEqual(result, "G0 Z50") + self.assertEqual(gcode.splitlines()[-1], "M2") + + def test050(self): + """ + Test inches + """ + + c = Path.Command("G0 X10 Y20 Z30") + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --inches --no-show-editor" + gcode = postprocessor.export(postables, "-", args) + self.assertEqual(gcode.splitlines()[2], "G20") + + result = gcode.splitlines()[5] + expected = "G0 X0.3937 Y0.7874 Z1.1811 " + self.assertEqual(result, expected) + + # Technical debt. The following test fails. Precision not working + # with imperial units. + + # args = ("--no-header --inches --precision=2") + # gcode = postprocessor.export(postables, "-", args) + # result = gcode.splitlines()[5] + # expected = "G0 X0.39 Y0.78 Z1.18 " + # self.assertEqual(result, expected) + + def test060(self): + """ + Test test modal + Suppress the command name if the same as previous + """ + c = Path.Command("G0 X10 Y20 Z30") + c1 = Path.Command("G0 X10 Y30 Z30") + + self.docobj.Path = Path.Path([c, c1]) + postables = [self.docobj] + + args = "--no-header --modal --no-show-editor" + gcode = postprocessor.export(postables, "-", args) + result = gcode.splitlines()[6] + expected = "X10.000 Y30.000 Z30.000 " + self.assertEqual(result, expected) + + def test070(self): + """ + Test axis modal + Suppress the axis coordinate if the same as previous + """ + c = Path.Command("G0 X10 Y20 Z30") + c1 = Path.Command("G0 X10 Y30 Z30") + + self.docobj.Path = Path.Path([c, c1]) + postables = [self.docobj] + + args = "--no-header --axis-modal --no-show-editor" + gcode = postprocessor.export(postables, "-", args) + result = gcode.splitlines()[6] + expected = "G0 Y30.000 " + self.assertEqual(result, expected) + + def test080(self): + """ + Test tool change + """ + c = Path.Command("M6 T2") + c2 = Path.Command("M3 S3000") + self.docobj.Path = Path.Path([c, c2]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "-", args) + self.assertEqual(gcode.splitlines()[5], "M5") + self.assertEqual(gcode.splitlines()[6], "M6 T2 ") + self.assertEqual(gcode.splitlines()[7], "G43 H2 ") + self.assertEqual(gcode.splitlines()[8], "M3 S3000 ") + + # suppress TLO + args = "--no-header --no-tlo --no-show-editor" + gcode = postprocessor.export(postables, "-", args) + self.assertEqual(gcode.splitlines()[7], "M3 S3000 ") + + def test090(self): + """ + Test comment + """ + self.compare_sixth_line("(comment)", "(comment) ", "--no-header --no-show-editor") + + def test100(self): + """Test A, B, & C axis output for values between 0 and 90 degrees""" + self.compare_sixth_line( + "G1 X10 Y20 Z30 A40 B50 C60", + "G1 X10.000 Y20.000 Z30.000 A40.000 B50.000 C60.000 ", + "--no-header --no-show-editor", + ) + self.compare_sixth_line( + "G1 X10 Y20 Z30 A40 B50 C60", + "G1 X0.3937 Y0.7874 Z1.1811 A40.0000 B50.0000 C60.0000 ", + "--no-header --inches --no-show-editor", + ) + + def test110(self): + """Test A, B, & C axis output for 89 degrees""" + self.compare_sixth_line( + "G1 X10 Y20 Z30 A89 B89 C89", + "G1 X10.000 Y20.000 Z30.000 A89.000 B89.000 C89.000 ", + "--no-header --no-show-editor", + ) + self.compare_sixth_line( + "G1 X10 Y20 Z30 A89 B89 C89", + "G1 X0.3937 Y0.7874 Z1.1811 A89.0000 B89.0000 C89.0000 ", + "--no-header --inches --no-show-editor", + ) + + def test120(self): + """Test A, B, & C axis output for 90 degrees""" + self.compare_sixth_line( + "G1 X10 Y20 Z30 A90 B90 C90", + "G1 X10.000 Y20.000 Z30.000 A90.000 B90.000 C90.000 ", + "--no-header --no-show-editor", + ) + self.compare_sixth_line( + "G1 X10 Y20 Z30 A90 B90 C90", + "G1 X0.3937 Y0.7874 Z1.1811 A90.0000 B90.0000 C90.0000 ", + "--no-header --inches --no-show-editor", + ) + + def test130(self): + """Test A, B, & C axis output for 91 degrees""" + self.compare_sixth_line( + "G1 X10 Y20 Z30 A91 B91 C91", + "G1 X10.000 Y20.000 Z30.000 A91.000 B91.000 C91.000 ", + "--no-header --no-show-editor", + ) + self.compare_sixth_line( + "G1 X10 Y20 Z30 A91 B91 C91", + "G1 X0.3937 Y0.7874 Z1.1811 A91.0000 B91.0000 C91.0000 ", + "--no-header --inches --no-show-editor", + ) + + def test140(self): + """Test A, B, & C axis output for values between 90 and 180 degrees""" + self.compare_sixth_line( + "G1 X10 Y20 Z30 A100 B110 C120", + "G1 X10.000 Y20.000 Z30.000 A100.000 B110.000 C120.000 ", + "--no-header --no-show-editor", + ) + self.compare_sixth_line( + "G1 X10 Y20 Z30 A100 B110 C120", + "G1 X0.3937 Y0.7874 Z1.1811 A100.0000 B110.0000 C120.0000 ", + "--no-header --inches --no-show-editor", + ) + + def test150(self): + """Test A, B, & C axis output for values between 180 and 360 degrees""" + self.compare_sixth_line( + "G1 X10 Y20 Z30 A240 B250 C260", + "G1 X10.000 Y20.000 Z30.000 A240.000 B250.000 C260.000 ", + "--no-header --no-show-editor", + ) + self.compare_sixth_line( + "G1 X10 Y20 Z30 A240 B250 C260", + "G1 X0.3937 Y0.7874 Z1.1811 A240.0000 B250.0000 C260.0000 ", + "--no-header --inches --no-show-editor", + ) + + def test160(self): + """Test A, B, & C axis output for values greater than 360 degrees""" + self.compare_sixth_line( + "G1 X10 Y20 Z30 A440 B450 C460", + "G1 X10.000 Y20.000 Z30.000 A440.000 B450.000 C460.000 ", + "--no-header --no-show-editor", + ) + self.compare_sixth_line( + "G1 X10 Y20 Z30 A440 B450 C460", + "G1 X0.3937 Y0.7874 Z1.1811 A440.0000 B450.0000 C460.0000 ", + "--no-header --inches --no-show-editor", + ) + + def test170(self): + """Test A, B, & C axis output for values between 0 and -90 degrees""" + self.compare_sixth_line( + "G1 X10 Y20 Z30 A-40 B-50 C-60", + "G1 X10.000 Y20.000 Z30.000 A-40.000 B-50.000 C-60.000 ", + "--no-header --no-show-editor", + ) + self.compare_sixth_line( + "G1 X10 Y20 Z30 A-40 B-50 C-60", + "G1 X0.3937 Y0.7874 Z1.1811 A-40.0000 B-50.0000 C-60.0000 ", + "--no-header --inches --no-show-editor", + ) + + def test180(self): + """Test A, B, & C axis output for values between -90 and -180 degrees""" + self.compare_sixth_line( + "G1 X10 Y20 Z30 A-100 B-110 C-120", + "G1 X10.000 Y20.000 Z30.000 A-100.000 B-110.000 C-120.000 ", + "--no-header --no-show-editor", + ) + self.compare_sixth_line( + "G1 X10 Y20 Z30 A-100 B-110 C-120", + "G1 X0.3937 Y0.7874 Z1.1811 A-100.0000 B-110.0000 C-120.0000 ", + "--no-header --inches --no-show-editor", + ) + + def test190(self): + """Test A, B, & C axis output for values between -180 and -360 degrees""" + self.compare_sixth_line( + "G1 X10 Y20 Z30 A-240 B-250 C-260", + "G1 X10.000 Y20.000 Z30.000 A-240.000 B-250.000 C-260.000 ", + "--no-header --no-show-editor", + ) + self.compare_sixth_line( + "G1 X10 Y20 Z30 A-240 B-250 C-260", + "G1 X0.3937 Y0.7874 Z1.1811 A-240.0000 B-250.0000 C-260.0000 ", + "--no-header --inches --no-show-editor", + ) + + def test200(self): + """Test A, B, & C axis output for values below -360 degrees""" + self.compare_sixth_line( + "G1 X10 Y20 Z30 A-440 B-450 C-460", + "G1 X10.000 Y20.000 Z30.000 A-440.000 B-450.000 C-460.000 ", + "--no-header --no-show-editor", + ) + self.compare_sixth_line( + "G1 X10 Y20 Z30 A-440 B-450 C-460", + "G1 X0.3937 Y0.7874 Z1.1811 A-440.0000 B-450.0000 C-460.0000 ", + "--no-header --inches --no-show-editor", + ) diff --git a/src/Mod/CAM/CAMTests/TestLinuxCNCPost.py b/src/Mod/CAM/CAMTests/TestLinuxCNCPost.py index a7014ff4a8..a33ee00998 100644 --- a/src/Mod/CAM/CAMTests/TestLinuxCNCPost.py +++ b/src/Mod/CAM/CAMTests/TestLinuxCNCPost.py @@ -2,7 +2,7 @@ # *************************************************************************** # * Copyright (c) 2022 sliptonic * -# * Copyright (c) 2023 Larry Woestman * +# * Copyright (c) 2022 - 2025 Larry Woestman * # * * # * This program is free software; you can redistribute it and/or modify * # * it under the terms of the GNU Lesser General Public License (LGPL) * @@ -26,17 +26,25 @@ import FreeCAD import Path import CAMTests.PathTestUtils as PathTestUtils -from importlib import reload -from Path.Post.scripts import linuxcnc_post as postprocessor +import CAMTests.PostTestMocks as PostTestMocks +from Path.Post.Processor import PostProcessorFactory + Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) Path.Log.trackModule(Path.Log.thisModule()) class TestLinuxCNCPost(PathTestUtils.PathTestBase): + """Test LinuxCNC-specific features of the inuxcnc_post.py postprocessor. + + This test suite focuses on LinuxCNC-specific functionality such as path blending modes. + Generic postprocessor functionality is tested in TestGenericPost. + """ + @classmethod def setUpClass(cls): """setUpClass()... + This method is called upon instantiation of this test class. Add code and objects here that are needed for the duration of the test() methods in this class. In other words, set up the 'global' test environment @@ -45,378 +53,138 @@ class TestLinuxCNCPost(PathTestUtils.PathTestBase): is able to call static methods within this same class. """ - # Open existing FreeCAD document with test geometry - FreeCAD.newDocument("Unnamed") + # Create mock job with default operation and tool controller + cls.job, cls.profile_op, cls.tool_controller = ( + PostTestMocks.create_default_job_with_operation() + ) + + # Create postprocessor using the mock job + cls.post = PostProcessorFactory.get_post_processor(cls.job, "linuxcnc") @classmethod def tearDownClass(cls): """tearDownClass()... + This method is called prior to destruction of this test class. Add code and objects here that cleanup the test environment after the - test() methods in this class have been executed. This method does - not have access to the class `self` reference. This method is able - to call static methods within this same class. + test() methods in this class have been executed. This method does not + have access to the class `self` reference. This method + is able to call static methods within this same class. """ - # Close geometry document without saving - FreeCAD.closeDocument(FreeCAD.ActiveDocument.Name) + # No cleanup needed for mock objects + pass # Setup and tear down methods called before and after each unit test + def setUp(self): """setUp()... + This method is called prior to each `test()` method. Add code and objects here that are needed for multiple `test()` methods. """ - self.doc = FreeCAD.ActiveDocument - self.con = FreeCAD.Console - self.docobj = FreeCAD.ActiveDocument.addObject("Path::Feature", "testpath") - reload( - postprocessor - ) # technical debt. This shouldn't be necessary but here to bypass a bug + # allow a full length "diff" if an error occurs + self.maxDiff = None + # reinitialize the postprocessor data structures between tests + self.post.reinitialize() def tearDown(self): """tearDown()... + This method is called after each test() method. Add cleanup instructions here. Such cleanup instructions will likely undo those in the setUp() method. """ - FreeCAD.ActiveDocument.removeObject("testpath") + pass - def compare_sixth_line(self, path_string, expected, args, debug=False): - """Perform a test with a single comparison to the sixth line of the output.""" - nl = "\n" - if path_string: - self.docobj.Path = Path.Path([Path.Command(path_string)]) - else: - self.docobj.Path = Path.Path([]) - postables = [self.docobj] - gcode = postprocessor.export(postables, "-", args) - if debug: - print(f"--------{nl}{gcode}--------{nl}") - self.assertEqual(gcode.splitlines()[5], expected) - - def test000(self): - """Test Output Generation. - Empty path. Produces only the preamble and postable. - """ - - self.docobj.Path = Path.Path([]) - postables = [self.docobj] - - # Test generating with header - # Header contains a time stamp that messes up unit testing. - # Only test length of result. - args = "--no-show-editor" - gcode = postprocessor.export(postables, "-", args) - self.assertTrue(len(gcode.splitlines()) == 13) - - # Test without header - expected = """(begin preamble) -G17 G54 G40 G49 G80 G90 -G21 -(begin operation: testpath) -(machine units: mm/min) -(finish operation: testpath) -(begin postamble) -M05 -G17 G54 G90 G80 G40 -M2 -""" - - self.docobj.Path = Path.Path([]) - postables = [self.docobj] - - args = "--no-header --no-show-editor" - # args = ("--no-header --no-comments --no-show-editor --precision=2") - gcode = postprocessor.export(postables, "-", args) - self.assertEqual(gcode, expected) - - # test without comments - expected = """G17 G54 G40 G49 G80 G90 -G21 -M05 -G17 G54 G90 G80 G40 -M2 -""" - - args = "--no-header --no-comments --no-show-editor" - # args = ("--no-header --no-comments --no-show-editor --precision=2") - gcode = postprocessor.export(postables, "-", args) - self.assertEqual(gcode, expected) - - def test010(self): - """Test command Generation. - Test Precision - Test imperial / inches - """ - self.compare_sixth_line( - "G0 X10 Y20 Z30", "G0 X10.000 Y20.000 Z30.000 ", "--no-header --no-show-editor" + def test_blend_mode_exact_path(self): + """Test EXACT_PATH blend mode outputs G61.""" + self.profile_op.Path = Path.Path([]) + self.job.PostProcessorArgs = ( + "--no-header --no-comments --blend-mode EXACT_PATH --no-show-editor" ) - self.compare_sixth_line( - "G0 X10 Y20 Z30", - "G0 X10.00 Y20.00 Z30.00 ", - "--no-header --precision=2 --no-show-editor", + gcode = self.post.export()[0][1] + + # G61 should be in the preamble + self.assertIn("G61", gcode) + # Should not have G64 + self.assertNotIn("G64", gcode) + # Should not have G61.1 + self.assertNotIn("G61.1", gcode) + + def test_blend_mode_exact_stop(self): + """Test EXACT_STOP blend mode outputs G61.1.""" + self.profile_op.Path = Path.Path([]) + self.job.PostProcessorArgs = ( + "--no-header --no-comments --blend-mode EXACT_STOP --no-show-editor" ) + gcode = self.post.export()[0][1] - def test020(self): - """ - Test Line Numbers - """ - self.compare_sixth_line( - "G0 X10 Y20 Z30", - "N160 G0 X10.000 Y20.000 Z30.000 ", - "--no-header --line-numbers --no-show-editor", + # G61.1 should be in the preamble + self.assertIn("G61.1", gcode) + # Should not have G64 + self.assertNotIn("G64", gcode) + + def test_blend_mode_blend_default(self): + """Test BLEND mode with default tolerance (0) outputs G64.""" + self.profile_op.Path = Path.Path([]) + self.job.PostProcessorArgs = "--no-header --no-comments --blend-mode BLEND --no-show-editor" + gcode = self.post.export()[0][1] + + # G64 should be in the preamble (without P parameter) + lines = gcode.splitlines() + has_g64 = any("G64" in line and "P" not in line for line in lines) + self.assertTrue(has_g64, "Expected G64 without P parameter") + + def test_blend_mode_blend_with_tolerance(self): + """Test BLEND mode with tolerance outputs G64 P.""" + self.profile_op.Path = Path.Path([]) + self.job.PostProcessorArgs = ( + "--no-header --no-comments --blend-mode BLEND --blend-tolerance 0.05 --no-show-editor" ) + gcode = self.post.export()[0][1] - def test030(self): - """ - Test Pre-amble - """ + # G64 P0.05 should be in the preamble + self.assertIn("G64 P0.0500", gcode) - self.docobj.Path = Path.Path([]) - postables = [self.docobj] - - args = "--no-header --no-comments --preamble='G18 G55' --no-show-editor" - gcode = postprocessor.export(postables, "-", args) - result = gcode.splitlines()[0] - self.assertEqual(result, "G18 G55") - - def test040(self): - """ - Test Post-amble - """ - self.docobj.Path = Path.Path([]) - postables = [self.docobj] - args = "--no-header --no-comments --postamble='G0 Z50\nM2' --no-show-editor" - gcode = postprocessor.export(postables, "-", args) - result = gcode.splitlines()[-2] - self.assertEqual(result, "G0 Z50") - self.assertEqual(gcode.splitlines()[-1], "M2") - - def test050(self): - """ - Test inches - """ - - c = Path.Command("G0 X10 Y20 Z30") - self.docobj.Path = Path.Path([c]) - postables = [self.docobj] - - args = "--no-header --inches --no-show-editor" - gcode = postprocessor.export(postables, "-", args) - self.assertEqual(gcode.splitlines()[2], "G20") - - result = gcode.splitlines()[5] - expected = "G0 X0.3937 Y0.7874 Z1.1811 " - self.assertEqual(result, expected) - - # Technical debt. The following test fails. Precision not working - # with imperial units. - - # args = ("--no-header --inches --precision=2") - # gcode = postprocessor.export(postables, "-", args) - # result = gcode.splitlines()[5] - # expected = "G0 X0.39 Y0.78 Z1.18 " - # self.assertEqual(result, expected) - - def test060(self): - """ - Test test modal - Suppress the command name if the same as previous - """ - c = Path.Command("G0 X10 Y20 Z30") - c1 = Path.Command("G0 X10 Y30 Z30") - - self.docobj.Path = Path.Path([c, c1]) - postables = [self.docobj] - - args = "--no-header --modal --no-show-editor" - gcode = postprocessor.export(postables, "-", args) - result = gcode.splitlines()[6] - expected = "X10.000 Y30.000 Z30.000 " - self.assertEqual(result, expected) - - def test070(self): - """ - Test axis modal - Suppress the axis coordinate if the same as previous - """ - c = Path.Command("G0 X10 Y20 Z30") - c1 = Path.Command("G0 X10 Y30 Z30") - - self.docobj.Path = Path.Path([c, c1]) - postables = [self.docobj] - - args = "--no-header --axis-modal --no-show-editor" - gcode = postprocessor.export(postables, "-", args) - result = gcode.splitlines()[6] - expected = "G0 Y30.000 " - self.assertEqual(result, expected) - - def test080(self): - """ - Test tool change - """ - c = Path.Command("M6 T2") - c2 = Path.Command("M3 S3000") - self.docobj.Path = Path.Path([c, c2]) - postables = [self.docobj] - - args = "--no-header --no-show-editor" - gcode = postprocessor.export(postables, "-", args) - self.assertEqual(gcode.splitlines()[5], "M5") - self.assertEqual(gcode.splitlines()[6], "M6 T2 ") - self.assertEqual(gcode.splitlines()[7], "G43 H2 ") - self.assertEqual(gcode.splitlines()[8], "M3 S3000 ") - - # suppress TLO - args = "--no-header --no-tlo --no-show-editor" - gcode = postprocessor.export(postables, "-", args) - self.assertEqual(gcode.splitlines()[7], "M3 S3000 ") - - def test090(self): - """ - Test comment - """ - self.compare_sixth_line("(comment)", "(comment) ", "--no-header --no-show-editor") - - def test100(self): - """Test A, B, & C axis output for values between 0 and 90 degrees""" - self.compare_sixth_line( - "G1 X10 Y20 Z30 A40 B50 C60", - "G1 X10.000 Y20.000 Z30.000 A40.000 B50.000 C60.000 ", - "--no-header --no-show-editor", - ) - self.compare_sixth_line( - "G1 X10 Y20 Z30 A40 B50 C60", - "G1 X0.3937 Y0.7874 Z1.1811 A40.0000 B50.0000 C60.0000 ", - "--no-header --inches --no-show-editor", + def test_blend_mode_blend_with_custom_tolerance(self): + """Test BLEND mode with custom tolerance value.""" + self.profile_op.Path = Path.Path([]) + self.job.PostProcessorArgs = ( + "--no-header --no-comments --blend-mode BLEND --blend-tolerance 0.02 --no-show-editor" ) + gcode = self.post.export()[0][1] - def test110(self): - """Test A, B, & C axis output for 89 degrees""" - self.compare_sixth_line( - "G1 X10 Y20 Z30 A89 B89 C89", - "G1 X10.000 Y20.000 Z30.000 A89.000 B89.000 C89.000 ", - "--no-header --no-show-editor", - ) - self.compare_sixth_line( - "G1 X10 Y20 Z30 A89 B89 C89", - "G1 X0.3937 Y0.7874 Z1.1811 A89.0000 B89.0000 C89.0000 ", - "--no-header --inches --no-show-editor", - ) + # G64 P0.02 should be in the preamble + self.assertIn("G64 P0.0200", gcode) - def test120(self): - """Test A, B, & C axis output for 90 degrees""" - self.compare_sixth_line( - "G1 X10 Y20 Z30 A90 B90 C90", - "G1 X10.000 Y20.000 Z30.000 A90.000 B90.000 C90.000 ", - "--no-header --no-show-editor", - ) - self.compare_sixth_line( - "G1 X10 Y20 Z30 A90 B90 C90", - "G1 X0.3937 Y0.7874 Z1.1811 A90.0000 B90.0000 C90.0000 ", - "--no-header --inches --no-show-editor", + def test_blend_mode_in_preamble_position(self): + """Test that blend mode command appears in correct position in preamble.""" + self.profile_op.Path = Path.Path([]) + self.job.PostProcessorArgs = ( + "--no-header --no-comments --blend-mode BLEND --blend-tolerance 0.1 --no-show-editor" ) + gcode = self.post.export()[0][1] + lines = gcode.splitlines() - def test130(self): - """Test A, B, & C axis output for 91 degrees""" - self.compare_sixth_line( - "G1 X10 Y20 Z30 A91 B91 C91", - "G1 X10.000 Y20.000 Z30.000 A91.000 B91.000 C91.000 ", - "--no-header --no-show-editor", - ) - self.compare_sixth_line( - "G1 X10 Y20 Z30 A91 B91 C91", - "G1 X0.3937 Y0.7874 Z1.1811 A91.0000 B91.0000 C91.0000 ", - "--no-header --inches --no-show-editor", - ) + # Find G64 P line + g64_line_idx = None + for i, line in enumerate(lines): + if "G64 P" in line: + g64_line_idx = i + break - def test140(self): - """Test A, B, & C axis output for values between 90 and 180 degrees""" - self.compare_sixth_line( - "G1 X10 Y20 Z30 A100 B110 C120", - "G1 X10.000 Y20.000 Z30.000 A100.000 B110.000 C120.000 ", - "--no-header --no-show-editor", - ) - self.compare_sixth_line( - "G1 X10 Y20 Z30 A100 B110 C120", - "G1 X0.3937 Y0.7874 Z1.1811 A100.0000 B110.0000 C120.0000 ", - "--no-header --inches --no-show-editor", - ) + self.assertIsNotNone(g64_line_idx, "G64 P command not found") + # Should be early in output (within first few lines of preamble) + self.assertLess(g64_line_idx, 5, "G64 command should be in preamble") - def test150(self): - """Test A, B, & C axis output for values between 180 and 360 degrees""" - self.compare_sixth_line( - "G1 X10 Y20 Z30 A240 B250 C260", - "G1 X10.000 Y20.000 Z30.000 A240.000 B250.000 C260.000 ", - "--no-header --no-show-editor", - ) - self.compare_sixth_line( - "G1 X10 Y20 Z30 A240 B250 C260", - "G1 X0.3937 Y0.7874 Z1.1811 A240.0000 B250.0000 C260.0000 ", - "--no-header --inches --no-show-editor", + def test_blend_tolerance_zero_equals_no_tolerance(self): + """Test that blend tolerance of 0 outputs G64 without P parameter.""" + self.profile_op.Path = Path.Path([]) + self.job.PostProcessorArgs = ( + "--no-header --no-comments --blend-mode BLEND --blend-tolerance 0 --no-show-editor" ) + gcode = self.post.export()[0][1] - def test160(self): - """Test A, B, & C axis output for values greater than 360 degrees""" - self.compare_sixth_line( - "G1 X10 Y20 Z30 A440 B450 C460", - "G1 X10.000 Y20.000 Z30.000 A440.000 B450.000 C460.000 ", - "--no-header --no-show-editor", - ) - self.compare_sixth_line( - "G1 X10 Y20 Z30 A440 B450 C460", - "G1 X0.3937 Y0.7874 Z1.1811 A440.0000 B450.0000 C460.0000 ", - "--no-header --inches --no-show-editor", - ) - - def test170(self): - """Test A, B, & C axis output for values between 0 and -90 degrees""" - self.compare_sixth_line( - "G1 X10 Y20 Z30 A-40 B-50 C-60", - "G1 X10.000 Y20.000 Z30.000 A-40.000 B-50.000 C-60.000 ", - "--no-header --no-show-editor", - ) - self.compare_sixth_line( - "G1 X10 Y20 Z30 A-40 B-50 C-60", - "G1 X0.3937 Y0.7874 Z1.1811 A-40.0000 B-50.0000 C-60.0000 ", - "--no-header --inches --no-show-editor", - ) - - def test180(self): - """Test A, B, & C axis output for values between -90 and -180 degrees""" - self.compare_sixth_line( - "G1 X10 Y20 Z30 A-100 B-110 C-120", - "G1 X10.000 Y20.000 Z30.000 A-100.000 B-110.000 C-120.000 ", - "--no-header --no-show-editor", - ) - self.compare_sixth_line( - "G1 X10 Y20 Z30 A-100 B-110 C-120", - "G1 X0.3937 Y0.7874 Z1.1811 A-100.0000 B-110.0000 C-120.0000 ", - "--no-header --inches --no-show-editor", - ) - - def test190(self): - """Test A, B, & C axis output for values between -180 and -360 degrees""" - self.compare_sixth_line( - "G1 X10 Y20 Z30 A-240 B-250 C-260", - "G1 X10.000 Y20.000 Z30.000 A-240.000 B-250.000 C-260.000 ", - "--no-header --no-show-editor", - ) - self.compare_sixth_line( - "G1 X10 Y20 Z30 A-240 B-250 C-260", - "G1 X0.3937 Y0.7874 Z1.1811 A-240.0000 B-250.0000 C-260.0000 ", - "--no-header --inches --no-show-editor", - ) - - def test200(self): - """Test A, B, & C axis output for values below -360 degrees""" - self.compare_sixth_line( - "G1 X10 Y20 Z30 A-440 B-450 C-460", - "G1 X10.000 Y20.000 Z30.000 A-440.000 B-450.000 C-460.000 ", - "--no-header --no-show-editor", - ) - self.compare_sixth_line( - "G1 X10 Y20 Z30 A-440 B-450 C-460", - "G1 X0.3937 Y0.7874 Z1.1811 A-440.0000 B-450.0000 C-460.0000 ", - "--no-header --inches --no-show-editor", - ) + # Should have G64 without P + lines = gcode.splitlines() + has_g64_without_p = any("G64" in line and "P" not in line for line in lines) + self.assertTrue(has_g64_without_p, "Expected G64 without P parameter when tolerance is 0") diff --git a/src/Mod/CAM/CAMTests/TestMach3Mach4LegacyPost.py b/src/Mod/CAM/CAMTests/TestMach3Mach4LegacyPost.py new file mode 100644 index 0000000000..2f6ab13129 --- /dev/null +++ b/src/Mod/CAM/CAMTests/TestMach3Mach4LegacyPost.py @@ -0,0 +1,288 @@ +# *************************************************************************** +# * Copyright (c) 2022 sliptonic * +# * Copyright (c) 2022 Larry Woestman * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +from importlib import reload + +import FreeCAD + +import Path +import CAMTests.PathTestUtils as PathTestUtils +from Path.Post.scripts import mach3_mach4_legacy_post as postprocessor + + +Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) +Path.Log.trackModule(Path.Log.thisModule()) + + +class TestMach3Mach4LegacyPost(PathTestUtils.PathTestBase): + @classmethod + def setUpClass(cls): + """setUpClass()... + This method is called upon instantiation of this test class. Add code + and objects here that are needed for the duration of the test() methods + in this class. In other words, set up the 'global' test environment + here; use the `setUp()` method to set up a 'local' test environment. + This method does not have access to the class `self` reference, but it + is able to call static methods within this same class. + """ + + # Open existing FreeCAD document with test geometry + FreeCAD.newDocument("Unnamed") + + @classmethod + def tearDownClass(cls): + """tearDownClass()... + This method is called prior to destruction of this test class. Add + code and objects here that cleanup the test environment after the + test() methods in this class have been executed. This method does + not have access to the class `self` reference. This method is able + to call static methods within this same class. + """ + # Close geometry document without saving + FreeCAD.closeDocument(FreeCAD.ActiveDocument.Name) + + # Setup and tear down methods called before and after each unit test + def setUp(self): + """setUp()... + This method is called prior to each `test()` method. Add code and + objects here that are needed for multiple `test()` methods. + """ + self.doc = FreeCAD.ActiveDocument + self.con = FreeCAD.Console + self.docobj = FreeCAD.ActiveDocument.addObject("Path::Feature", "testpath") + reload( + postprocessor + ) # technical debt. This shouldn't be necessary but here to bypass a bug + + def tearDown(self): + """tearDown()... + This method is called after each test() method. Add cleanup instructions here. + Such cleanup instructions will likely undo those in the setUp() method. + """ + FreeCAD.ActiveDocument.removeObject("testpath") + + def test000(self): + """Test Output Generation. + Empty path. Produces only the preamble and postable. + """ + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + # Test generating with header + # Header contains a time stamp that messes up unit testing. + # Only test length of result. + args = "--no-show-editor" + gcode = postprocessor.export(postables, "-", args) + self.assertTrue(len(gcode.splitlines()) == 13) + + # Test without header + expected = """(begin preamble) +G17 G54 G40 G49 G80 G90 +G21 +(begin operation: testpath) +(machine: mach3_4, mm/min) +(finish operation: testpath) +(begin postamble) +M05 +G17 G54 G90 G80 G40 +M2 +""" + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + # args = ("--no-header --no-comments --no-show-editor --precision=2") + gcode = postprocessor.export(postables, "-", args) + self.assertEqual(gcode, expected) + + # test without comments + expected = """G17 G54 G40 G49 G80 G90 +G21 +M05 +G17 G54 G90 G80 G40 +M2 +""" + + args = "--no-header --no-comments --no-show-editor" + # args = ("--no-header --no-comments --no-show-editor --precision=2") + gcode = postprocessor.export(postables, "-", args) + self.assertEqual(gcode, expected) + + def test010(self): + """Test command Generation. + Test Precision + """ + c = Path.Command("G0 X10 Y20 Z30") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "-", args) + result = gcode.splitlines()[5] + expected = "G0 X10.000 Y20.000 Z30.000" + self.assertEqual(result, expected) + + args = "--no-header --precision=2 --no-show-editor" + gcode = postprocessor.export(postables, "-", args) + result = gcode.splitlines()[5] + expected = "G0 X10.00 Y20.00 Z30.00" + self.assertEqual(result, expected) + + def test020(self): + """ + Test Line Numbers + """ + c = Path.Command("G0 X10 Y20 Z30") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --line-numbers --no-show-editor" + gcode = postprocessor.export(postables, "-", args) + result = gcode.splitlines()[5] + expected = "N160 G0 X10.000 Y20.000 Z30.000" + self.assertEqual(result, expected) + + def test030(self): + """ + Test Pre-amble + """ + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + args = "--no-header --no-comments --preamble='G18 G55' --no-show-editor" + gcode = postprocessor.export(postables, "-", args) + result = gcode.splitlines()[0] + self.assertEqual(result, "G18 G55") + + def test040(self): + """ + Test Post-amble + """ + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + args = "--no-header --no-comments --postamble='G0 Z50\nM2' --no-show-editor" + gcode = postprocessor.export(postables, "-", args) + result = gcode.splitlines()[-2] + self.assertEqual(result, "G0 Z50") + self.assertEqual(gcode.splitlines()[-1], "M2") + + def test050(self): + """ + Test inches + """ + + c = Path.Command("G0 X10 Y20 Z30") + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --inches --no-show-editor" + gcode = postprocessor.export(postables, "-", args) + self.assertEqual(gcode.splitlines()[2], "G20") + + result = gcode.splitlines()[5] + expected = "G0 X0.3937 Y0.7874 Z1.1811" + self.assertEqual(result, expected) + + # Technical debt. The following test fails. Precision not working + # with imperial units. + + # args = ("--no-header --inches --precision=2 --no-show-editor") + # gcode = postprocessor.export(postables, "-", args) + # result = gcode.splitlines()[5] + # expected = "G0 X0.39 Y0.79 Z1.18" + # self.assertEqual(result, expected) + + def test060(self): + """ + Test test modal + Suppress the command name if the same as previous + """ + c = Path.Command("G0 X10 Y20 Z30") + c1 = Path.Command("G0 X10 Y30 Z30") + + self.docobj.Path = Path.Path([c, c1]) + postables = [self.docobj] + + args = "--no-header --modal --no-show-editor" + gcode = postprocessor.export(postables, "-", args) + result = gcode.splitlines()[6] + expected = "X10.000 Y30.000 Z30.000" + self.assertEqual(result, expected) + + def test070(self): + """ + Test axis modal + Suppress the axis coordinate if the same as previous + """ + c = Path.Command("G0 X10 Y20 Z30") + c1 = Path.Command("G0 X10 Y30 Z30") + + self.docobj.Path = Path.Path([c, c1]) + postables = [self.docobj] + + args = "--no-header --axis-modal --no-show-editor" + gcode = postprocessor.export(postables, "-", args) + result = gcode.splitlines()[6] + expected = "G0 Y30.000" + self.assertEqual(result, expected) + + def test080(self): + """ + Test tool change + """ + c = Path.Command("M6 T2") + c2 = Path.Command("M3 S3000") + self.docobj.Path = Path.Path([c, c2]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "-", args) + self.assertEqual(gcode.splitlines()[5], "M5") + self.assertEqual(gcode.splitlines()[6], "M6 T2 ") + self.assertEqual(gcode.splitlines()[7], "G43 H2") + self.assertEqual(gcode.splitlines()[8], "M3 S3000") + + # suppress TLO + args = "--no-header --no-tlo --no-show-editor" + gcode = postprocessor.export(postables, "-", args) + self.assertEqual(gcode.splitlines()[7], "M3 S3000") + + def test090(self): + """ + Test comment + """ + + c = Path.Command("(comment)") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "-", args) + result = gcode.splitlines()[5] + expected = "(comment)" + self.assertEqual(result, expected) diff --git a/src/Mod/CAM/CAMTests/TestMach3Mach4Post.py b/src/Mod/CAM/CAMTests/TestMach3Mach4Post.py index 5bf7f4c614..7f4432aa3a 100644 --- a/src/Mod/CAM/CAMTests/TestMach3Mach4Post.py +++ b/src/Mod/CAM/CAMTests/TestMach3Mach4Post.py @@ -2,7 +2,7 @@ # *************************************************************************** # * Copyright (c) 2022 sliptonic * -# * Copyright (c) 2022 Larry Woestman * +# * Copyright (c) 2022 - 2025 Larry Woestman * # * * # * This program is free software; you can redistribute it and/or modify * # * it under the terms of the GNU Lesser General Public License (LGPL) * @@ -22,13 +22,11 @@ # * * # *************************************************************************** -from importlib import reload - import FreeCAD import Path import CAMTests.PathTestUtils as PathTestUtils -from Path.Post.scripts import mach3_mach4_post as postprocessor +from Path.Post.Processor import PostProcessorFactory Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) @@ -36,9 +34,12 @@ Path.Log.trackModule(Path.Log.thisModule()) class TestMach3Mach4Post(PathTestUtils.PathTestBase): + """Test the mach3_mach4_post.py postprocessor.""" + @classmethod - def setUpClass(cls): + def setUpClass(cls) -> None: """setUpClass()... + This method is called upon instantiation of this test class. Add code and objects here that are needed for the duration of the test() methods in this class. In other words, set up the 'global' test environment @@ -47,108 +48,162 @@ class TestMach3Mach4Post(PathTestUtils.PathTestBase): is able to call static methods within this same class. """ - # Open existing FreeCAD document with test geometry - FreeCAD.newDocument("Unnamed") + FreeCAD.ConfigSet("SuppressRecomputeRequiredDialog", "True") + cls.doc = FreeCAD.open(FreeCAD.getHomePath() + "/Mod/CAM/CAMTests/boxtest.fcstd") + cls.job = cls.doc.getObject("Job") + cls.post = PostProcessorFactory.get_post_processor(cls.job, "mach3_mach4") + # locate the operation named "Profile" + for op in cls.job.Operations.Group: + if op.Label == "Profile": + # remember the "Profile" operation + cls.profile_op = op + return @classmethod - def tearDownClass(cls): + def tearDownClass(cls) -> None: """tearDownClass()... + This method is called prior to destruction of this test class. Add code and objects here that cleanup the test environment after the - test() methods in this class have been executed. This method does - not have access to the class `self` reference. This method is able - to call static methods within this same class. + test() methods in this class have been executed. This method does not + have access to the class `self` reference. This method is able to + call static methods within this same class. """ - # Close geometry document without saving - FreeCAD.closeDocument(FreeCAD.ActiveDocument.Name) + FreeCAD.closeDocument(cls.doc.Name) + FreeCAD.ConfigSet("SuppressRecomputeRequiredDialog", "") # Setup and tear down methods called before and after each unit test - def setUp(self): + + def setUp(self) -> None: """setUp()... + This method is called prior to each `test()` method. Add code and objects here that are needed for multiple `test()` methods. """ - self.doc = FreeCAD.ActiveDocument - self.con = FreeCAD.Console - self.docobj = FreeCAD.ActiveDocument.addObject("Path::Feature", "testpath") - reload( - postprocessor - ) # technical debt. This shouldn't be necessary but here to bypass a bug + # allow a full length "diff" if an error occurs + self.maxDiff = None + # reinitialize the postprocessor data structures between tests + self.post.reinitialize() - def tearDown(self): + def tearDown(self) -> None: """tearDown()... + This method is called after each test() method. Add cleanup instructions here. Such cleanup instructions will likely undo those in the setUp() method. """ - FreeCAD.ActiveDocument.removeObject("testpath") + pass - def test000(self): + def single_compare(self, path, expected, args, debug=False): + """Perform a test with a single line 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) + # the gcode is in the first section for this particular job and operation + gcode = self.post.export()[0][1] + if debug: + print(f"--------{nl}{gcode}--------{nl}") + # there are 4 lines of "other stuff" before the line we are interested in + self.assertEqual(gcode.splitlines()[4], expected) + + 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) + # the gcode is in the first section for this particular job and operation + gcode = self.post.export()[0][1] + if debug: + print(f"--------{nl}{gcode}--------{nl}") + self.assertEqual(gcode, expected) + + def test000(self) -> None: """Test Output Generation. Empty path. Produces only the preamble and postable. """ + nl = "\n" - self.docobj.Path = Path.Path([]) - postables = [self.docobj] + self.profile_op.Path = Path.Path([]) # Test generating with header # Header contains a time stamp that messes up unit testing. # Only test length of result. - args = "--no-show-editor" - gcode = postprocessor.export(postables, "-", args) - self.assertTrue(len(gcode.splitlines()) == 13) + self.job.PostProcessorArgs = "--no-show-editor" + gcode = self.post.export()[0][1] + # print(f"--------{nl}{gcode}--------{nl}") + self.assertTrue(len(gcode.splitlines()) == 26) # Test without header - expected = """(begin preamble) + expected = """(Begin preamble) G17 G54 G40 G49 G80 G90 G21 -(begin operation: testpath) -(machine: mach3_4, mm/min) -(finish operation: testpath) -(begin postamble) +(Begin operation: Fixture) +(Machine: mach3_4, mm/min) +G54 +(Finish operation: Fixture) +(Begin operation: TC: Default Tool) +(Machine: mach3_4, mm/min) +(TC: Default Tool) +(Begin toolchange) +M5 +M6 T1 +G43 H1 +(Finish operation: TC: Default Tool) +(Begin operation: Profile) +(Machine: mach3_4, mm/min) +(Finish operation: Profile) +(Begin postamble) M05 G17 G54 G90 G80 G40 M2 """ - self.docobj.Path = Path.Path([]) - postables = [self.docobj] - - args = "--no-header --no-show-editor" # args = ("--no-header --no-comments --no-show-editor --precision=2") - gcode = postprocessor.export(postables, "-", args) + self.job.PostProcessorArgs = "--no-header --no-show-editor" + gcode = self.post.export()[0][1] + # print(f"--------{nl}{gcode}--------{nl}") self.assertEqual(gcode, expected) # test without comments expected = """G17 G54 G40 G49 G80 G90 G21 +G54 +M5 +M6 T1 +G43 H1 M05 G17 G54 G90 G80 G40 M2 """ - args = "--no-header --no-comments --no-show-editor" # args = ("--no-header --no-comments --no-show-editor --precision=2") - gcode = postprocessor.export(postables, "-", args) + self.job.PostProcessorArgs = "--no-header --no-comments --no-show-editor" + gcode = self.post.export()[0][1] + # print(f"--------{nl}{gcode}--------{nl}") self.assertEqual(gcode, expected) def test010(self): """Test command Generation. Test Precision """ + nl = "\n" + c = Path.Command("G0 X10 Y20 Z30") - self.docobj.Path = Path.Path([c]) - postables = [self.docobj] + self.profile_op.Path = Path.Path([c]) - args = "--no-header --no-show-editor" - gcode = postprocessor.export(postables, "-", args) - result = gcode.splitlines()[5] + self.job.PostProcessorArgs = "--no-header --no-show-editor" + gcode = self.post.export()[0][1] + # print(f"--------{nl}{gcode}--------{nl}") + result = gcode.splitlines()[17] expected = "G0 X10.000 Y20.000 Z30.000" self.assertEqual(result, expected) - args = "--no-header --precision=2 --no-show-editor" - gcode = postprocessor.export(postables, "-", args) - result = gcode.splitlines()[5] + self.job.PostProcessorArgs = "--no-header --precision=2 --no-show-editor" + gcode = self.post.export()[0][1] + # print(f"--------{nl}{gcode}--------{nl}") + result = gcode.splitlines()[17] expected = "G0 X10.00 Y20.00 Z30.00" self.assertEqual(result, expected) @@ -156,27 +211,32 @@ M2 """ Test Line Numbers """ + nl = "\n" + c = Path.Command("G0 X10 Y20 Z30") - self.docobj.Path = Path.Path([c]) - postables = [self.docobj] + self.profile_op.Path = Path.Path([c]) - args = "--no-header --line-numbers --no-show-editor" - gcode = postprocessor.export(postables, "-", args) - result = gcode.splitlines()[5] - expected = "N160 G0 X10.000 Y20.000 Z30.000" + self.job.PostProcessorArgs = "--no-header --line-numbers --no-show-editor" + gcode = self.post.export()[0][1] + # print(f"--------{nl}{gcode}--------{nl}") + result = gcode.splitlines()[17] + expected = "N270 G0 X10.000 Y20.000 Z30.000" self.assertEqual(result, expected) def test030(self): """ Test Pre-amble """ + nl = "\n" - self.docobj.Path = Path.Path([]) - postables = [self.docobj] + self.profile_op.Path = Path.Path([]) - args = "--no-header --no-comments --preamble='G18 G55' --no-show-editor" - gcode = postprocessor.export(postables, "-", args) + self.job.PostProcessorArgs = ( + "--no-header --no-comments --preamble='G18 G55' --no-show-editor" + ) + gcode = self.post.export()[0][1] + # print(f"--------{nl}{gcode}--------{nl}") result = gcode.splitlines()[0] self.assertEqual(result, "G18 G55") @@ -184,10 +244,15 @@ M2 """ Test Post-amble """ - self.docobj.Path = Path.Path([]) - postables = [self.docobj] - args = "--no-header --no-comments --postamble='G0 Z50\nM2' --no-show-editor" - gcode = postprocessor.export(postables, "-", args) + nl = "\n" + + self.profile_op.Path = Path.Path([]) + + self.job.PostProcessorArgs = ( + "--no-header --no-comments --postamble='G0 Z50\nM2' --no-show-editor" + ) + gcode = self.post.export()[0][1] + # print(f"--------{nl}{gcode}--------{nl}") result = gcode.splitlines()[-2] self.assertEqual(result, "G0 Z50") self.assertEqual(gcode.splitlines()[-1], "M2") @@ -196,42 +261,44 @@ M2 """ Test inches """ + nl = "\n" c = Path.Command("G0 X10 Y20 Z30") - self.docobj.Path = Path.Path([c]) - postables = [self.docobj] - args = "--no-header --inches --no-show-editor" - gcode = postprocessor.export(postables, "-", args) + self.profile_op.Path = Path.Path([c]) + + self.job.PostProcessorArgs = "--no-header --inches --no-show-editor" + gcode = self.post.export()[0][1] + # print(f"--------{nl}{gcode}--------{nl}") self.assertEqual(gcode.splitlines()[2], "G20") - result = gcode.splitlines()[5] + result = gcode.splitlines()[17] expected = "G0 X0.3937 Y0.7874 Z1.1811" self.assertEqual(result, expected) - # Technical debt. The following test fails. Precision not working - # with imperial units. - - # args = ("--no-header --inches --precision=2 --no-show-editor") - # gcode = postprocessor.export(postables, "-", args) - # result = gcode.splitlines()[5] - # expected = "G0 X0.39 Y0.79 Z1.18" - # self.assertEqual(result, expected) + self.job.PostProcessorArgs = "--no-header --inches --precision=2 --no-show-editor" + gcode = self.post.export()[0][1] + # print(f"--------{nl}{gcode}--------{nl}") + result = gcode.splitlines()[17] + expected = "G0 X0.39 Y0.79 Z1.18" + self.assertEqual(result, expected) def test060(self): """ Test test modal Suppress the command name if the same as previous """ + nl = "\n" + c = Path.Command("G0 X10 Y20 Z30") c1 = Path.Command("G0 X10 Y30 Z30") - self.docobj.Path = Path.Path([c, c1]) - postables = [self.docobj] + self.profile_op.Path = Path.Path([c, c1]) - args = "--no-header --modal --no-show-editor" - gcode = postprocessor.export(postables, "-", args) - result = gcode.splitlines()[6] + self.job.PostProcessorArgs = "--no-header --modal --no-show-editor" + gcode = self.post.export()[0][1] + # print(f"--------{nl}{gcode}--------{nl}") + result = gcode.splitlines()[18] expected = "X10.000 Y30.000 Z30.000" self.assertEqual(result, expected) @@ -240,15 +307,17 @@ M2 Test axis modal Suppress the axis coordinate if the same as previous """ + nl = "\n" + c = Path.Command("G0 X10 Y20 Z30") c1 = Path.Command("G0 X10 Y30 Z30") - self.docobj.Path = Path.Path([c, c1]) - postables = [self.docobj] + self.profile_op.Path = Path.Path([c, c1]) - args = "--no-header --axis-modal --no-show-editor" - gcode = postprocessor.export(postables, "-", args) - result = gcode.splitlines()[6] + self.job.PostProcessorArgs = "--no-header --axis-modal --no-show-editor" + gcode = self.post.export()[0][1] + # print(f"--------{nl}{gcode}--------{nl}") + result = gcode.splitlines()[18] expected = "G0 Y30.000" self.assertEqual(result, expected) @@ -256,35 +325,41 @@ M2 """ Test tool change """ + nl = "\n" + c = Path.Command("M6 T2") c2 = Path.Command("M3 S3000") - self.docobj.Path = Path.Path([c, c2]) - postables = [self.docobj] - args = "--no-header --no-show-editor" - gcode = postprocessor.export(postables, "-", args) - self.assertEqual(gcode.splitlines()[5], "M5") - self.assertEqual(gcode.splitlines()[6], "M6 T2 ") - self.assertEqual(gcode.splitlines()[7], "G43 H2") - self.assertEqual(gcode.splitlines()[8], "M3 S3000") + self.profile_op.Path = Path.Path([c, c2]) + + self.job.PostProcessorArgs = "--no-header --no-show-editor" + gcode = self.post.export()[0][1] + # print(f"--------{nl}{gcode}--------{nl}") + split_gcode = gcode.splitlines() + self.assertEqual(split_gcode[18], "M5") + self.assertEqual(split_gcode[19], "M6 T2") + self.assertEqual(split_gcode[20], "G43 H2") + self.assertEqual(split_gcode[21], "M3 S3000") # suppress TLO - args = "--no-header --no-tlo --no-show-editor" - gcode = postprocessor.export(postables, "-", args) - self.assertEqual(gcode.splitlines()[7], "M3 S3000") + self.job.PostProcessorArgs = "--no-header --no-tlo --no-show-editor" + gcode = self.post.export()[0][1] + # print(f"--------{nl}{gcode}--------{nl}") + self.assertEqual(gcode.splitlines()[19], "M3 S3000") def test090(self): """ Test comment """ + nl = "\n" c = Path.Command("(comment)") - self.docobj.Path = Path.Path([c]) - postables = [self.docobj] + self.profile_op.Path = Path.Path([c]) - args = "--no-header --no-show-editor" - gcode = postprocessor.export(postables, "-", args) - result = gcode.splitlines()[5] + self.job.PostProcessorArgs = "--no-header --no-show-editor" + gcode = self.post.export()[0][1] + # print(f"--------{nl}{gcode}--------{nl}") + result = gcode.splitlines()[17] expected = "(comment)" self.assertEqual(result, expected) diff --git a/src/Mod/CAM/CAMTests/TestRefactoredMassoG3Post.py b/src/Mod/CAM/CAMTests/TestMassoG3Post.py similarity index 98% rename from src/Mod/CAM/CAMTests/TestRefactoredMassoG3Post.py rename to src/Mod/CAM/CAMTests/TestMassoG3Post.py index 90e32c2de9..a7b93f1987 100644 --- a/src/Mod/CAM/CAMTests/TestRefactoredMassoG3Post.py +++ b/src/Mod/CAM/CAMTests/TestMassoG3Post.py @@ -43,7 +43,7 @@ Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) Path.Log.trackModule(Path.Log.thisModule()) -class TestRefactoredMassoG3Post(PathTestUtils.PathTestBase): +class TestMassoG3Post(PathTestUtils.PathTestBase): @classmethod def setUpClass(cls): """setUpClass()... @@ -58,7 +58,7 @@ class TestRefactoredMassoG3Post(PathTestUtils.PathTestBase): FreeCAD.ConfigSet("SuppressRecomputeRequiredDialog", "True") cls.doc = FreeCAD.open(FreeCAD.getHomePath() + "/Mod/CAM/CAMTests/boxtest.fcstd") cls.job = cls.doc.getObject("Job") - cls.post = PostProcessorFactory.get_post_processor(cls.job, "refactored_masso_g3") + cls.post = PostProcessorFactory.get_post_processor(cls.job, "masso_g3") # locate the operation named "Profile" for op in cls.job.Operations.Group: if op.Label == "Profile": @@ -109,7 +109,6 @@ class TestRefactoredMassoG3Post(PathTestUtils.PathTestBase): # Only test length of result. self.job.PostProcessorArgs = "--no-show-editor" gcode = self.post.export()[0][1] - # print(f"--------{nl}{gcode}--------{nl}") self.assertTrue(len(gcode.splitlines()) == 26) # Test without header diff --git a/src/Mod/CAM/CAMTests/TestPathPost.py b/src/Mod/CAM/CAMTests/TestPathPost.py index 4fde303f77..3955e3d334 100644 --- a/src/Mod/CAM/CAMTests/TestPathPost.py +++ b/src/Mod/CAM/CAMTests/TestPathPost.py @@ -470,14 +470,14 @@ class TestPostProcessorFactory(unittest.TestCase): def test030(self): # test wrapping of old school postprocessor scripts - post = PostProcessorFactory.get_post_processor(self.job, "linuxcnc") + post = PostProcessorFactory.get_post_processor(self.job, "linuxcnc_legacy") self.assertTrue(post is not None) self.assertTrue(hasattr(post, "_buildPostList")) def test040(self): """Test that the __name__ of the postprocessor is correct.""" - post = PostProcessorFactory.get_post_processor(self.job, "linuxcnc") - self.assertEqual(post.script_module.__name__, "linuxcnc_post") + post = PostProcessorFactory.get_post_processor(self.job, "linuxcnc_legacy") + self.assertEqual(post.script_module.__name__, "linuxcnc_legacy_post") class TestPathPostUtils(unittest.TestCase): diff --git a/src/Mod/CAM/CAMTests/TestRefactoredTestPostGCodes.py b/src/Mod/CAM/CAMTests/TestPostGCodes.py similarity index 99% rename from src/Mod/CAM/CAMTests/TestRefactoredTestPostGCodes.py rename to src/Mod/CAM/CAMTests/TestPostGCodes.py index 7848dfadf3..d6ad008dd1 100644 --- a/src/Mod/CAM/CAMTests/TestRefactoredTestPostGCodes.py +++ b/src/Mod/CAM/CAMTests/TestPostGCodes.py @@ -33,8 +33,8 @@ Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) Path.Log.trackModule(Path.Log.thisModule()) -class TestRefactoredTestPostGCodes(PathTestUtils.PathTestBase): - """Test the refactored_test_post.py postprocessor G codes.""" +class TestPostGCodes(PathTestUtils.PathTestBase): + """Test the test_post.py postprocessor G codes.""" @classmethod def setUpClass(cls): @@ -50,7 +50,7 @@ class TestRefactoredTestPostGCodes(PathTestUtils.PathTestBase): FreeCAD.ConfigSet("SuppressRecomputeRequiredDialog", "True") cls.doc = FreeCAD.open(FreeCAD.getHomePath() + "/Mod/CAM/CAMTests/boxtest.fcstd") cls.job = cls.doc.getObject("Job") - cls.post = PostProcessorFactory.get_post_processor(cls.job, "refactored_test") + cls.post = PostProcessorFactory.get_post_processor(cls.job, "test") # locate the operation named "Profile" for op in cls.job.Operations.Group: if op.Label == "Profile": diff --git a/src/Mod/CAM/CAMTests/TestRefactoredTestPostMCodes.py b/src/Mod/CAM/CAMTests/TestPostMCodes.py similarity index 98% rename from src/Mod/CAM/CAMTests/TestRefactoredTestPostMCodes.py rename to src/Mod/CAM/CAMTests/TestPostMCodes.py index c430987c3d..ef30401006 100644 --- a/src/Mod/CAM/CAMTests/TestRefactoredTestPostMCodes.py +++ b/src/Mod/CAM/CAMTests/TestPostMCodes.py @@ -33,8 +33,8 @@ Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) Path.Log.trackModule(Path.Log.thisModule()) -class TestRefactoredTestPostMCodes(PathTestUtils.PathTestBase): - """Test the refactored_test_post.py postprocessor.""" +class TestPostMCodes(PathTestUtils.PathTestBase): + """Test the test_post.py postprocessor M codes.""" @classmethod def setUpClass(cls): @@ -50,7 +50,7 @@ class TestRefactoredTestPostMCodes(PathTestUtils.PathTestBase): FreeCAD.ConfigSet("SuppressRecomputeRequiredDialog", "True") cls.doc = FreeCAD.open(FreeCAD.getHomePath() + "/Mod/CAM/CAMTests/boxtest.fcstd") cls.job = cls.doc.getObject("Job") - cls.post = PostProcessorFactory.get_post_processor(cls.job, "refactored_test") + cls.post = PostProcessorFactory.get_post_processor(cls.job, "test") # locate the operation named "Profile" for op in cls.job.Operations.Group: if op.Label == "Profile": diff --git a/src/Mod/CAM/CAMTests/TestRefactoredCentroidPost.py b/src/Mod/CAM/CAMTests/TestRefactoredCentroidPost.py deleted file mode 100644 index 4abd170178..0000000000 --- a/src/Mod/CAM/CAMTests/TestRefactoredCentroidPost.py +++ /dev/null @@ -1,361 +0,0 @@ -# SPDX-License-Identifier: LGPL-2.1-or-later - -# *************************************************************************** -# * Copyright (c) 2022 sliptonic * -# * Copyright (c) 2022 - 2025 Larry Woestman * -# * * -# * This program is free software; you can redistribute it and/or modify * -# * it under the terms of the GNU Lesser General Public License (LGPL) * -# * as published by the Free Software Foundation; either version 2 of * -# * the License, or (at your option) any later version. * -# * for detail see the LICENCE text file. * -# * * -# * This program is distributed in the hope that it will be useful, * -# * but WITHOUT ANY WARRANTY; without even the implied warranty of * -# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -# * GNU Library General Public License for more details. * -# * * -# * You should have received a copy of the GNU Library General Public * -# * License along with this program; if not, write to the Free Software * -# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * -# * USA * -# * * -# *************************************************************************** - -import FreeCAD - -import Path -import CAMTests.PathTestUtils as PathTestUtils -from Path.Post.Processor import PostProcessorFactory - - -Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) -Path.Log.trackModule(Path.Log.thisModule()) - - -class TestRefactoredCentroidPost(PathTestUtils.PathTestBase): - """Test the refactored_centroid_post.py postprocessor.""" - - @classmethod - def setUpClass(cls): - """setUpClass()... - - This method is called upon instantiation of this test class. Add code - and objects here that are needed for the duration of the test() methods - in this class. In other words, set up the 'global' test environment - here; use the `setUp()` method to set up a 'local' test environment. - This method does not have access to the class `self` reference, but it - is able to call static methods within this same class. - """ - - FreeCAD.ConfigSet("SuppressRecomputeRequiredDialog", "True") - cls.doc = FreeCAD.open(FreeCAD.getHomePath() + "/Mod/CAM/CAMTests/boxtest.fcstd") - cls.job = cls.doc.getObject("Job") - cls.post = PostProcessorFactory.get_post_processor(cls.job, "refactored_centroid") - # locate the operation named "Profile" - for op in cls.job.Operations.Group: - if op.Label == "Profile": - # remember the "Profile" operation - cls.profile_op = op - return - - @classmethod - def tearDownClass(cls): - """tearDownClass()... - - This method is called prior to destruction of this test class. Add - code and objects here that cleanup the test environment after the - test() methods in this class have been executed. This method does not - have access to the class `self` reference. This method is able to - call static methods within this same class. - """ - FreeCAD.closeDocument(cls.doc.Name) - FreeCAD.ConfigSet("SuppressRecomputeRequiredDialog", "") - - # Setup and tear down methods called before and after each unit test - - def setUp(self): - """setUp()... - - This method is called prior to each `test()` method. Add code and - objects here that are needed for multiple `test()` methods. - """ - # allow a full length "diff" if an error occurs - self.maxDiff = None - # reinitialize the postprocessor data structures between tests - self.post.reinitialize() - - def tearDown(self): - """tearDown()... - - This method is called after each test() method. Add cleanup instructions here. - Such cleanup instructions will likely undo those in the setUp() method. - """ - pass - - def single_compare(self, path, expected, args, debug=False): - """Perform a test with a single line 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) - # the gcode is in the first section for this particular job and operation - gcode = self.post.export()[0][1] - if debug: - print(f"--------{nl}{gcode}--------{nl}") - # there are 4 lines of "other stuff" before the line we are interested in - self.assertEqual(gcode.splitlines()[4], expected) - - 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) - # the gcode is in the first section for this particular job and operation - gcode = self.post.export()[0][1] - if debug: - print(f"--------{nl}{gcode}--------{nl}") - self.assertEqual(gcode, expected) - - def test000(self): - """Test Output Generation. - Empty path. Produces only the preamble and postable. - """ - nl = "\n" - - self.profile_op.Path = Path.Path([]) - - # Test generating with header - # Header contains a time stamp that messes up unit testing. - # Only test length of result. - self.job.PostProcessorArgs = "--no-show-editor" - gcode = self.post.export()[0][1] - # print(f"--------{nl}{gcode}--------{nl}") - self.assertTrue(len(gcode.splitlines()) == 25) - - # Test without header - expected = """G90 G80 G40 G49 -;T1=TC__Default_Tool -;Begin preamble -G53 G00 G17 -G21 -;Begin operation -G54 -;End operation -;Begin operation -;TC: Default Tool -;Begin toolchange -M6 T1 -;End operation -;Begin operation -;End operation -;Begin postamble -M5 -M25 -G49 H0 -G90 G80 G40 G49 -M99 -""" - - self.job.PostProcessorArgs = "--no-header --no-show-editor" - gcode = self.post.export()[0][1] - # print(f"--------{nl}{gcode}--------{nl}") - self.assertEqual(gcode, expected) - - # test without comments - expected = """G90 G80 G40 G49 -G53 G00 G17 -G21 -G54 -M6 T1 -M5 -M25 -G49 H0 -G90 G80 G40 G49 -M99 -""" - - # args = ("--no-header --no-comments --no-show-editor --precision=2") - self.job.PostProcessorArgs = "--no-header --no-comments --no-show-editor" - gcode = self.post.export()[0][1] - # print(f"--------{nl}{gcode}--------{nl}") - self.assertEqual(gcode, expected) - - def test010(self): - """Test command Generation. - Test Precision - """ - nl = "\n" - - c = Path.Command("G0 X10 Y20 Z30") - - self.profile_op.Path = Path.Path([c]) - - self.job.PostProcessorArgs = "--no-header --no-show-editor" - gcode = self.post.export()[0][1] - # print(f"--------{nl}{gcode}--------{nl}") - result = gcode.splitlines()[14] - expected = "G0 X10.0000 Y20.0000 Z30.0000" - self.assertEqual(result, expected) - - self.job.PostProcessorArgs = "--no-header --axis-precision=2 --no-show-editor" - gcode = self.post.export()[0][1] - # print(f"--------{nl}{gcode}--------{nl}") - result = gcode.splitlines()[14] - expected = "G0 X10.00 Y20.00 Z30.00" - self.assertEqual(result, expected) - - def test020(self): - """ - Test Line Numbers - """ - nl = "\n" - - c = Path.Command("G0 X10 Y20 Z30") - - self.profile_op.Path = Path.Path([c]) - - self.job.PostProcessorArgs = "--no-header --line-numbers --no-show-editor" - gcode = self.post.export()[0][1] - # print(f"--------{nl}{gcode}--------{nl}") - result = gcode.splitlines()[14] - expected = "N240 G0 X10.0000 Y20.0000 Z30.0000" - self.assertEqual(result, expected) - - def test030(self): - """ - Test Pre-amble - """ - nl = "\n" - - self.profile_op.Path = Path.Path([]) - - self.job.PostProcessorArgs = ( - "--no-header --no-comments --preamble='G18 G55' --no-show-editor" - ) - gcode = self.post.export()[0][1] - # print(f"--------{nl}{gcode}--------{nl}") - result = gcode.splitlines()[1] - self.assertEqual(result, "G18 G55") - - def test040(self): - """ - Test Post-amble - """ - nl = "\n" - - self.profile_op.Path = Path.Path([]) - - self.job.PostProcessorArgs = ( - "--no-header --no-comments --postamble='G0 Z50\nM2' --no-show-editor" - ) - gcode = self.post.export()[0][1] - # print(f"--------{nl}{gcode}--------{nl}") - result = gcode.splitlines()[-2] - self.assertEqual(result, "G0 Z50") - self.assertEqual(gcode.splitlines()[-1], "M2") - - def test050(self): - """ - Test inches - """ - nl = "\n" - - c = Path.Command("G0 X10 Y20 Z30") - - self.profile_op.Path = Path.Path([c]) - - self.job.PostProcessorArgs = "--no-header --inches --no-show-editor" - gcode = self.post.export()[0][1] - # print(f"--------{nl}{gcode}--------{nl}") - self.assertEqual(gcode.splitlines()[4], "G20") - - result = gcode.splitlines()[14] - expected = "G0 X0.3937 Y0.7874 Z1.1811" - self.assertEqual(result, expected) - - self.job.PostProcessorArgs = "--no-header --inches --axis-precision=2 --no-show-editor" - gcode = self.post.export()[0][1] - # print(f"--------{nl}{gcode}--------{nl}") - result = gcode.splitlines()[14] - expected = "G0 X0.39 Y0.79 Z1.18" - self.assertEqual(result, expected) - - def test060(self): - """ - Test test modal - Suppress the command name if the same as previous - """ - nl = "\n" - - c = Path.Command("G0 X10 Y20 Z30") - c1 = Path.Command("G0 X10 Y30 Z30") - - self.profile_op.Path = Path.Path([c, c1]) - - self.job.PostProcessorArgs = "--no-header --modal --no-show-editor" - gcode = self.post.export()[0][1] - # print(f"--------{nl}{gcode}--------{nl}") - result = gcode.splitlines()[15] - expected = "X10.0000 Y30.0000 Z30.0000" - self.assertEqual(result, expected) - - def test070(self): - """ - Test axis modal - Suppress the axis coordinate if the same as previous - """ - nl = "\n" - - c = Path.Command("G0 X10 Y20 Z30") - c1 = Path.Command("G0 X10 Y30 Z30") - - self.profile_op.Path = Path.Path([c, c1]) - - self.job.PostProcessorArgs = "--no-header --axis-modal --no-show-editor" - gcode = self.post.export()[0][1] - # print(f"--------{nl}{gcode}--------{nl}") - result = gcode.splitlines()[15] - expected = "G0 Y30.0000" - self.assertEqual(result, expected) - - def test080(self): - """ - Test tool change - """ - nl = "\n" - - c = Path.Command("M6 T2") - c2 = Path.Command("M3 S3000") - - self.profile_op.Path = Path.Path([c, c2]) - - self.job.PostProcessorArgs = "--no-header --no-show-editor" - gcode = self.post.export()[0][1] - # print(f"--------{nl}{gcode}--------{nl}") - self.assertEqual(gcode.splitlines()[15], "M6 T2") - self.assertEqual(gcode.splitlines()[16], "M3 S3000") - - # suppress TLO - self.job.PostProcessorArgs = "--no-header --no-tlo --no-show-editor" - gcode = self.post.export()[0][1] - # print(f"--------{nl}{gcode}--------{nl}") - self.assertEqual(gcode.splitlines()[16], "M3 S3000") - - def test090(self): - """ - Test comment - """ - nl = "\n" - - c = Path.Command("(comment)") - - self.profile_op.Path = Path.Path([c]) - - self.job.PostProcessorArgs = "--no-header --no-show-editor" - gcode = self.post.export()[0][1] - # print(f"--------{nl}{gcode}--------{nl}") - result = gcode.splitlines()[14] - expected = ";comment" - self.assertEqual(result, expected) diff --git a/src/Mod/CAM/CAMTests/TestRefactoredGrblPost.py b/src/Mod/CAM/CAMTests/TestRefactoredGrblPost.py deleted file mode 100644 index c4dbd697e8..0000000000 --- a/src/Mod/CAM/CAMTests/TestRefactoredGrblPost.py +++ /dev/null @@ -1,353 +0,0 @@ -# SPDX-License-Identifier: LGPL-2.1-or-later - -# *************************************************************************** -# * Copyright (c) 2022 sliptonic * -# * Copyright (c) 2022 - 2025 Larry Woestman * -# * * -# * This program is free software; you can redistribute it and/or modify * -# * it under the terms of the GNU Lesser General Public License (LGPL) * -# * as published by the Free Software Foundation; either version 2 of * -# * the License, or (at your option) any later version. * -# * for detail see the LICENCE text file. * -# * * -# * This program is distributed in the hope that it will be useful, * -# * but WITHOUT ANY WARRANTY; without even the implied warranty of * -# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -# * GNU Library General Public License for more details. * -# * * -# * You should have received a copy of the GNU Library General Public * -# * License along with this program; if not, write to the Free Software * -# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * -# * USA * -# * * -# *************************************************************************** - -import FreeCAD - -import Path -import CAMTests.PathTestUtils as PathTestUtils -from Path.Post.Processor import PostProcessorFactory - - -Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) -Path.Log.trackModule(Path.Log.thisModule()) - - -class TestRefactoredGrblPost(PathTestUtils.PathTestBase): - """Test the refactored_grbl_post.py postprocessor.""" - - @classmethod - def setUpClass(cls): - """setUpClass()... - - This method is called upon instantiation of this test class. Add code - and objects here that are needed for the duration of the test() methods - in this class. In other words, set up the 'global' test environment - here; use the `setUp()` method to set up a 'local' test environment. - This method does not have access to the class `self` reference, but it - is able to call static methods within this same class. - """ - - FreeCAD.ConfigSet("SuppressRecomputeRequiredDialog", "True") - cls.doc = FreeCAD.open(FreeCAD.getHomePath() + "/Mod/CAM/CAMTests/boxtest.fcstd") - cls.job = cls.doc.getObject("Job") - cls.post = PostProcessorFactory.get_post_processor(cls.job, "refactored_grbl") - # locate the operation named "Profile" - for op in cls.job.Operations.Group: - if op.Label == "Profile": - # remember the "Profile" operation - cls.profile_op = op - return - - @classmethod - def tearDownClass(cls): - """tearDownClass()... - - This method is called prior to destruction of this test class. Add - code and objects here that cleanup the test environment after the - test() methods in this class have been executed. This method does not - have access to the class `self` reference. This method - is able to call static methods within this same class. - """ - FreeCAD.closeDocument(cls.doc.Name) - FreeCAD.ConfigSet("SuppressRecomputeRequiredDialog", "") - - # Setup and tear down methods called before and after each unit test - - def setUp(self): - """setUp()... - - This method is called prior to each `test()` method. Add code and - objects here that are needed for multiple `test()` methods. - """ - # allow a full length "diff" if an error occurs - self.maxDiff = None - # reinitialize the postprocessor data structures between tests - self.post.reinitialize() - - def tearDown(self): - """tearDown()... - - This method is called after each test() method. Add cleanup instructions here. - Such cleanup instructions will likely undo those in the setUp() method. - """ - pass - - def test000(self): - """Test Output Generation. - Empty path. Produces only the preamble and postable. - """ - nl = "\n" - - self.profile_op.Path = Path.Path([]) - - # Test generating with header - # Header contains a time stamp that messes up unit testing. - # Only test length of result. - self.job.PostProcessorArgs = "--no-show-editor" - gcode = self.post.export()[0][1] - # print(f"--------{nl}{gcode}--------{nl}") - self.assertTrue(len(gcode.splitlines()) == 24) - - # Test without header - expected = """(Begin preamble) -G17 G90 -G21 -(Begin operation: Fixture) -(Path: Fixture) -G54 -(Finish operation: Fixture) -(Begin operation: TC: Default Tool) -(Path: TC: Default Tool) -(TC: Default Tool) -(Begin toolchange) -(M6 T1) -(Finish operation: TC: Default Tool) -(Begin operation: Profile) -(Path: Profile) -(Finish operation: Profile) -(Begin postamble) -M5 -G17 G90 -M2 -""" - - self.profile_op.Path = Path.Path([]) - - # args = ("--no-header --no-comments --no-show-editor --precision=2") - self.job.PostProcessorArgs = "--no-header --no-show-editor" - gcode = self.post.export()[0][1] - # print(f"--------{nl}{gcode}--------{nl}") - self.assertEqual(gcode, expected) - - # test without comments - expected = """G17 G90 -G21 -G54 -M5 -G17 G90 -M2 -""" - - # args = ("--no-header --no-comments --no-show-editor --precision=2") - self.job.PostProcessorArgs = "--no-header --no-comments --no-show-editor" - gcode = self.post.export()[0][1] - # print(f"--------{nl}{gcode}--------{nl}") - self.assertEqual(gcode, expected) - - def test010(self): - """Test command Generation. - Test Precision - """ - nl = "\n" - - c = Path.Command("G0 X10 Y20 Z30") - - self.profile_op.Path = Path.Path([c]) - - self.job.PostProcessorArgs = "--no-header --no-show-editor" - gcode = self.post.export()[0][1] - # print(f"--------{nl}{gcode}--------{nl}") - result = gcode.splitlines()[15] - expected = "G0 X10.000 Y20.000 Z30.000" - self.assertEqual(result, expected) - - self.job.PostProcessorArgs = "--no-header --precision=2 --no-show-editor" - gcode = self.post.export()[0][1] - # print(f"--------{nl}{gcode}--------{nl}") - result = gcode.splitlines()[15] - expected = "G0 X10.00 Y20.00 Z30.00" - self.assertEqual(result, expected) - - def test020(self): - """ - Test Line Numbers - """ - nl = "\n" - - c = Path.Command("G0 X10 Y20 Z30") - - self.profile_op.Path = Path.Path([c]) - - self.job.PostProcessorArgs = "--no-header --line-numbers --no-show-editor" - gcode = self.post.export()[0][1] - # print(f"--------{nl}{gcode}--------{nl}") - result = gcode.splitlines()[15] - expected = "N250 G0 X10.000 Y20.000 Z30.000" - self.assertEqual(result, expected) - - def test030(self): - """ - Test Pre-amble - """ - nl = "\n" - - self.profile_op.Path = Path.Path([]) - - self.job.PostProcessorArgs = ( - "--no-header --no-comments --preamble='G18 G55' --no-show-editor" - ) - gcode = self.post.export()[0][1] - # print(f"--------{nl}{gcode}--------{nl}") - result = gcode.splitlines()[0] - self.assertEqual(result, "G18 G55") - - def test040(self): - """ - Test Post-amble - """ - nl = "\n" - - self.profile_op.Path = Path.Path([]) - - self.job.PostProcessorArgs = ( - "--no-header --no-comments --postamble='G0 Z50\nM2' --no-show-editor" - ) - gcode = self.post.export()[0][1] - # print(f"--------{nl}{gcode}--------{nl}") - result = gcode.splitlines()[-2] - self.assertEqual(result, "G0 Z50") - self.assertEqual(gcode.splitlines()[-1], "M2") - - def test050(self): - """ - Test inches - """ - nl = "\n" - - c = Path.Command("G0 X10 Y20 Z30") - - self.profile_op.Path = Path.Path([c]) - - self.job.PostProcessorArgs = "--no-header --inches --no-show-editor" - gcode = self.post.export()[0][1] - # print(f"--------{nl}{gcode}--------{nl}") - self.assertEqual(gcode.splitlines()[2], "G20") - - result = gcode.splitlines()[15] - expected = "G0 X0.3937 Y0.7874 Z1.1811" - self.assertEqual(result, expected) - - self.job.PostProcessorArgs = "--no-header --inches --precision=2 --no-show-editor" - gcode = self.post.export()[0][1] - # print(f"--------{nl}{gcode}--------{nl}") - result = gcode.splitlines()[15] - expected = "G0 X0.39 Y0.79 Z1.18" - self.assertEqual(result, expected) - - def test060(self): - """ - Test test modal - Suppress the command name if the same as previous - """ - nl = "\n" - - c = Path.Command("G0 X10 Y20 Z30") - c1 = Path.Command("G0 X10 Y30 Z30") - - self.profile_op.Path = Path.Path([c, c1]) - - self.job.PostProcessorArgs = "--no-header --modal --no-show-editor" - gcode = self.post.export()[0][1] - # print(f"--------{nl}{gcode}--------{nl}") - result = gcode.splitlines()[16] - expected = "X10.000 Y30.000 Z30.000" - self.assertEqual(result, expected) - - def test070(self): - """ - Test axis modal - Suppress the axis coordinate if the same as previous - """ - nl = "\n" - - c = Path.Command("G0 X10 Y20 Z30") - c1 = Path.Command("G0 X10 Y30 Z30") - - self.profile_op.Path = Path.Path([c, c1]) - - self.job.PostProcessorArgs = "--no-header --axis-modal --no-show-editor" - gcode = self.post.export()[0][1] - # print(f"--------{nl}{gcode}--------{nl}") - result = gcode.splitlines()[16] - expected = "G0 Y30.000" - self.assertEqual(result, expected) - - def test080(self): - """ - Test tool change - """ - nl = "\n" - - c = Path.Command("M6 T2") - c2 = Path.Command("M3 S3000") - - self.profile_op.Path = Path.Path([c, c2]) - - self.job.PostProcessorArgs = "--no-header --no-show-editor" - gcode = self.post.export()[0][1] - # print(f"--------{nl}{gcode}--------{nl}") - self.assertEqual(gcode.splitlines()[16], "(M6 T2)") - self.assertEqual(gcode.splitlines()[17], "M3 S3000") - - # suppress TLO - self.job.PostProcessorArgs = "--no-header --no-tlo --no-show-editor" - gcode = self.post.export()[0][1] - # print(f"--------{nl}{gcode}--------{nl}") - self.assertEqual(gcode.splitlines()[17], "M3 S3000") - - def test090(self): - """ - Test comment - """ - nl = "\n" - - c = Path.Command("(comment)") - - self.profile_op.Path = Path.Path([c]) - - self.job.PostProcessorArgs = "--no-header --no-show-editor" - gcode = self.post.export()[0][1] - # print(f"--------{nl}{gcode}--------{nl}") - result = gcode.splitlines()[15] - expected = "(comment)" - self.assertEqual(result, expected) - - def test100(self): - """ - Test if coolant is enabled. - """ - nl = "\n" - - c = Path.Command("M7") - c1 = Path.Command("M8") - c2 = Path.Command("M9") - - self.profile_op.Path = Path.Path([c, c1, c2]) - - self.job.PostProcessorArgs = "--no-header --no-show-editor" - gcode = self.post.export()[0][1] - # print(f"--------{nl}{gcode}--------{nl}") - self.assertEqual(gcode.splitlines()[15], "M7") - self.assertEqual(gcode.splitlines()[16], "M8") - self.assertEqual(gcode.splitlines()[17], "M9") diff --git a/src/Mod/CAM/CAMTests/TestRefactoredMach3Mach4Post.py b/src/Mod/CAM/CAMTests/TestRefactoredMach3Mach4Post.py deleted file mode 100644 index 2dfe0505ff..0000000000 --- a/src/Mod/CAM/CAMTests/TestRefactoredMach3Mach4Post.py +++ /dev/null @@ -1,365 +0,0 @@ -# SPDX-License-Identifier: LGPL-2.1-or-later - -# *************************************************************************** -# * Copyright (c) 2022 sliptonic * -# * Copyright (c) 2022 - 2025 Larry Woestman * -# * * -# * This program is free software; you can redistribute it and/or modify * -# * it under the terms of the GNU Lesser General Public License (LGPL) * -# * as published by the Free Software Foundation; either version 2 of * -# * the License, or (at your option) any later version. * -# * for detail see the LICENCE text file. * -# * * -# * This program is distributed in the hope that it will be useful, * -# * but WITHOUT ANY WARRANTY; without even the implied warranty of * -# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -# * GNU Library General Public License for more details. * -# * * -# * You should have received a copy of the GNU Library General Public * -# * License along with this program; if not, write to the Free Software * -# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * -# * USA * -# * * -# *************************************************************************** - -import FreeCAD - -import Path -import CAMTests.PathTestUtils as PathTestUtils -from Path.Post.Processor import PostProcessorFactory - - -Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) -Path.Log.trackModule(Path.Log.thisModule()) - - -class TestRefactoredMach3Mach4Post(PathTestUtils.PathTestBase): - """Test the refactored_mach3_mach4_post.py postprocessor.""" - - @classmethod - def setUpClass(cls) -> None: - """setUpClass()... - - This method is called upon instantiation of this test class. Add code - and objects here that are needed for the duration of the test() methods - in this class. In other words, set up the 'global' test environment - here; use the `setUp()` method to set up a 'local' test environment. - This method does not have access to the class `self` reference, but it - is able to call static methods within this same class. - """ - - FreeCAD.ConfigSet("SuppressRecomputeRequiredDialog", "True") - cls.doc = FreeCAD.open(FreeCAD.getHomePath() + "/Mod/CAM/CAMTests/boxtest.fcstd") - cls.job = cls.doc.getObject("Job") - cls.post = PostProcessorFactory.get_post_processor(cls.job, "refactored_mach3_mach4") - # locate the operation named "Profile" - for op in cls.job.Operations.Group: - if op.Label == "Profile": - # remember the "Profile" operation - cls.profile_op = op - return - - @classmethod - def tearDownClass(cls) -> None: - """tearDownClass()... - - This method is called prior to destruction of this test class. Add - code and objects here that cleanup the test environment after the - test() methods in this class have been executed. This method does not - have access to the class `self` reference. This method is able to - call static methods within this same class. - """ - FreeCAD.closeDocument(cls.doc.Name) - FreeCAD.ConfigSet("SuppressRecomputeRequiredDialog", "") - - # Setup and tear down methods called before and after each unit test - - def setUp(self) -> None: - """setUp()... - - This method is called prior to each `test()` method. Add code and - objects here that are needed for multiple `test()` methods. - """ - # allow a full length "diff" if an error occurs - self.maxDiff = None - # reinitialize the postprocessor data structures between tests - self.post.reinitialize() - - def tearDown(self) -> None: - """tearDown()... - - This method is called after each test() method. Add cleanup instructions here. - Such cleanup instructions will likely undo those in the setUp() method. - """ - pass - - def single_compare(self, path, expected, args, debug=False): - """Perform a test with a single line 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) - # the gcode is in the first section for this particular job and operation - gcode = self.post.export()[0][1] - if debug: - print(f"--------{nl}{gcode}--------{nl}") - # there are 4 lines of "other stuff" before the line we are interested in - self.assertEqual(gcode.splitlines()[4], expected) - - 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) - # the gcode is in the first section for this particular job and operation - gcode = self.post.export()[0][1] - if debug: - print(f"--------{nl}{gcode}--------{nl}") - self.assertEqual(gcode, expected) - - def test000(self) -> None: - """Test Output Generation. - Empty path. Produces only the preamble and postable. - """ - nl = "\n" - - self.profile_op.Path = Path.Path([]) - - # Test generating with header - # Header contains a time stamp that messes up unit testing. - # Only test length of result. - self.job.PostProcessorArgs = "--no-show-editor" - gcode = self.post.export()[0][1] - # print(f"--------{nl}{gcode}--------{nl}") - self.assertTrue(len(gcode.splitlines()) == 26) - - # Test without header - expected = """(Begin preamble) -G17 G54 G40 G49 G80 G90 -G21 -(Begin operation: Fixture) -(Machine: mach3_4, mm/min) -G54 -(Finish operation: Fixture) -(Begin operation: TC: Default Tool) -(Machine: mach3_4, mm/min) -(TC: Default Tool) -(Begin toolchange) -M5 -M6 T1 -G43 H1 -(Finish operation: TC: Default Tool) -(Begin operation: Profile) -(Machine: mach3_4, mm/min) -(Finish operation: Profile) -(Begin postamble) -M05 -G17 G54 G90 G80 G40 -M2 -""" - - # args = ("--no-header --no-comments --no-show-editor --precision=2") - self.job.PostProcessorArgs = "--no-header --no-show-editor" - gcode = self.post.export()[0][1] - # print(f"--------{nl}{gcode}--------{nl}") - self.assertEqual(gcode, expected) - - # test without comments - expected = """G17 G54 G40 G49 G80 G90 -G21 -G54 -M5 -M6 T1 -G43 H1 -M05 -G17 G54 G90 G80 G40 -M2 -""" - - # args = ("--no-header --no-comments --no-show-editor --precision=2") - self.job.PostProcessorArgs = "--no-header --no-comments --no-show-editor" - gcode = self.post.export()[0][1] - # print(f"--------{nl}{gcode}--------{nl}") - self.assertEqual(gcode, expected) - - def test010(self): - """Test command Generation. - Test Precision - """ - nl = "\n" - - c = Path.Command("G0 X10 Y20 Z30") - - self.profile_op.Path = Path.Path([c]) - - self.job.PostProcessorArgs = "--no-header --no-show-editor" - gcode = self.post.export()[0][1] - # print(f"--------{nl}{gcode}--------{nl}") - result = gcode.splitlines()[17] - expected = "G0 X10.000 Y20.000 Z30.000" - self.assertEqual(result, expected) - - self.job.PostProcessorArgs = "--no-header --precision=2 --no-show-editor" - gcode = self.post.export()[0][1] - # print(f"--------{nl}{gcode}--------{nl}") - result = gcode.splitlines()[17] - expected = "G0 X10.00 Y20.00 Z30.00" - self.assertEqual(result, expected) - - def test020(self): - """ - Test Line Numbers - """ - nl = "\n" - - c = Path.Command("G0 X10 Y20 Z30") - - self.profile_op.Path = Path.Path([c]) - - self.job.PostProcessorArgs = "--no-header --line-numbers --no-show-editor" - gcode = self.post.export()[0][1] - # print(f"--------{nl}{gcode}--------{nl}") - result = gcode.splitlines()[17] - expected = "N270 G0 X10.000 Y20.000 Z30.000" - self.assertEqual(result, expected) - - def test030(self): - """ - Test Pre-amble - """ - nl = "\n" - - self.profile_op.Path = Path.Path([]) - - self.job.PostProcessorArgs = ( - "--no-header --no-comments --preamble='G18 G55' --no-show-editor" - ) - gcode = self.post.export()[0][1] - # print(f"--------{nl}{gcode}--------{nl}") - result = gcode.splitlines()[0] - self.assertEqual(result, "G18 G55") - - def test040(self): - """ - Test Post-amble - """ - nl = "\n" - - self.profile_op.Path = Path.Path([]) - - self.job.PostProcessorArgs = ( - "--no-header --no-comments --postamble='G0 Z50\nM2' --no-show-editor" - ) - gcode = self.post.export()[0][1] - # print(f"--------{nl}{gcode}--------{nl}") - result = gcode.splitlines()[-2] - self.assertEqual(result, "G0 Z50") - self.assertEqual(gcode.splitlines()[-1], "M2") - - def test050(self): - """ - Test inches - """ - nl = "\n" - - c = Path.Command("G0 X10 Y20 Z30") - - self.profile_op.Path = Path.Path([c]) - - self.job.PostProcessorArgs = "--no-header --inches --no-show-editor" - gcode = self.post.export()[0][1] - # print(f"--------{nl}{gcode}--------{nl}") - self.assertEqual(gcode.splitlines()[2], "G20") - - result = gcode.splitlines()[17] - expected = "G0 X0.3937 Y0.7874 Z1.1811" - self.assertEqual(result, expected) - - self.job.PostProcessorArgs = "--no-header --inches --precision=2 --no-show-editor" - gcode = self.post.export()[0][1] - # print(f"--------{nl}{gcode}--------{nl}") - result = gcode.splitlines()[17] - expected = "G0 X0.39 Y0.79 Z1.18" - self.assertEqual(result, expected) - - def test060(self): - """ - Test test modal - Suppress the command name if the same as previous - """ - nl = "\n" - - c = Path.Command("G0 X10 Y20 Z30") - c1 = Path.Command("G0 X10 Y30 Z30") - - self.profile_op.Path = Path.Path([c, c1]) - - self.job.PostProcessorArgs = "--no-header --modal --no-show-editor" - gcode = self.post.export()[0][1] - # print(f"--------{nl}{gcode}--------{nl}") - result = gcode.splitlines()[18] - expected = "X10.000 Y30.000 Z30.000" - self.assertEqual(result, expected) - - def test070(self): - """ - Test axis modal - Suppress the axis coordinate if the same as previous - """ - nl = "\n" - - c = Path.Command("G0 X10 Y20 Z30") - c1 = Path.Command("G0 X10 Y30 Z30") - - self.profile_op.Path = Path.Path([c, c1]) - - self.job.PostProcessorArgs = "--no-header --axis-modal --no-show-editor" - gcode = self.post.export()[0][1] - # print(f"--------{nl}{gcode}--------{nl}") - result = gcode.splitlines()[18] - expected = "G0 Y30.000" - self.assertEqual(result, expected) - - def test080(self): - """ - Test tool change - """ - nl = "\n" - - c = Path.Command("M6 T2") - c2 = Path.Command("M3 S3000") - - self.profile_op.Path = Path.Path([c, c2]) - - self.job.PostProcessorArgs = "--no-header --no-show-editor" - gcode = self.post.export()[0][1] - # print(f"--------{nl}{gcode}--------{nl}") - split_gcode = gcode.splitlines() - self.assertEqual(split_gcode[18], "M5") - self.assertEqual(split_gcode[19], "M6 T2") - self.assertEqual(split_gcode[20], "G43 H2") - self.assertEqual(split_gcode[21], "M3 S3000") - - # suppress TLO - self.job.PostProcessorArgs = "--no-header --no-tlo --no-show-editor" - gcode = self.post.export()[0][1] - # print(f"--------{nl}{gcode}--------{nl}") - self.assertEqual(gcode.splitlines()[19], "M3 S3000") - - def test090(self): - """ - Test comment - """ - nl = "\n" - - c = Path.Command("(comment)") - - self.profile_op.Path = Path.Path([c]) - - self.job.PostProcessorArgs = "--no-header --no-show-editor" - gcode = self.post.export()[0][1] - # print(f"--------{nl}{gcode}--------{nl}") - result = gcode.splitlines()[17] - expected = "(comment)" - self.assertEqual(result, expected) diff --git a/src/Mod/CAM/CAMTests/TestRefactoredTestPost.py b/src/Mod/CAM/CAMTests/TestTestPost.py similarity index 99% rename from src/Mod/CAM/CAMTests/TestRefactoredTestPost.py rename to src/Mod/CAM/CAMTests/TestTestPost.py index 76aad6155e..b6590c3110 100644 --- a/src/Mod/CAM/CAMTests/TestRefactoredTestPost.py +++ b/src/Mod/CAM/CAMTests/TestTestPost.py @@ -39,8 +39,8 @@ Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) Path.Log.trackModule(Path.Log.thisModule()) -class TestRefactoredTestPost(PathTestUtils.PathTestBase): - """Test the refactored_test_post.py postprocessor command line arguments.""" +class TestTestPost(PathTestUtils.PathTestBase): + """Test the test_post.py postprocessor command line arguments.""" @classmethod def setUpClass(cls): @@ -56,7 +56,7 @@ class TestRefactoredTestPost(PathTestUtils.PathTestBase): FreeCAD.ConfigSet("SuppressRecomputeRequiredDialog", "True") cls.doc = FreeCAD.open(FreeCAD.getHomePath() + "/Mod/CAM/CAMTests/boxtest.fcstd") cls.job = cls.doc.getObject("Job") - cls.post = PostProcessorFactory.get_post_processor(cls.job, "refactored_test") + cls.post = PostProcessorFactory.get_post_processor(cls.job, "test") # locate the operation named "Profile" for op in cls.job.Operations.Group: if op.Label == "Profile": @@ -944,7 +944,7 @@ G54 # print(f"--------{nl}{gcode}--------{nl}") split_gcode = gcode.splitlines() self.assertEqual(split_gcode[0], "(Exported by FreeCAD)") - self.assertEqual(split_gcode[1], "(Post Processor: refactored_test_post)") + self.assertEqual(split_gcode[1], "(Post Processor: test_post)") self.assertEqual(split_gcode[2], "(Cam File: boxtest.fcstd)") # The header contains a time stamp that messes up unit testing. # Only test the length of the line that contains the time. @@ -992,7 +992,7 @@ G54 split_gcode = gcode.splitlines() # print(f"--------{nl}{gcode}--------{nl}") self.assertEqual(split_gcode[0], "(Exported by FreeCAD)") - self.assertEqual(split_gcode[1], "(Post Processor: refactored_test_post)") + self.assertEqual(split_gcode[1], "(Post Processor: test_post)") self.assertEqual(split_gcode[2], "(Cam File: boxtest.fcstd)") # The header contains a time stamp that messes up unit testing. # Only test the length of the line that contains the time. @@ -1278,6 +1278,8 @@ G0 Z8.000 e.g. --return-to=0,0,0 (default is do not move) --show-editor Pop up editor before writing output (default) --no-show-editor Don't pop up editor before writing output + --split-arcs Convert G2/G3 arc commands to discrete G1 line segments + --no-split-arcs Output G2/G3 arc commands as-is (default) --tlo Output tool length offset (G43) following tool changes (default) --no-tlo Suppress tool length offset (G43) following tool diff --git a/src/Mod/CAM/CAMTests/boxtest1.fcstd b/src/Mod/CAM/CAMTests/boxtest1.fcstd deleted file mode 100644 index d150419beffd3796df29b63790d57fca017f9e84..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 46372 zcmaIdQ*b6g+a}=HwlmShwr$(a8%%85wl%SB+qP}n+WCG|?ZMWbbanMnA9i2QeJepYzkR zwbjFM5`Xd}H8(rz6x8QeeydhOA^aH zhH$qAmAy~;`T6;~z9aHCfLhMi?L>BSXOR68?~+4Z(G9mP@v&rCujH$Gh9^08ktu5;Cb@S5X>ggwmLtLC+e+QuwaC(?`{ zt2f(?-lcS}<#i(_kw@n4X5s8{joDqEKb(P%TP~}0jK-(VarN9~LmZNv`{NU|k+%D# zWPN!1weO^|%;x3xcK7c_m5tQ%;bd&ePp?p{=U&k7hVrgT*UC^YtStI#w-DrU@4Id* z9qhQb7ul^{L+_hN9`-vi`jt;HnkVMY74wXB@sW{{?B}|A23F853ums5s%M*$hwcUU zWouqbeD8AZ*BWt4+!yxiUffLQp7x!i@yohq{JjQq-WJS>M;M+ zimR;^4X!&K{E#}roA`WRBmc2�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

VEZnRhMFf>_&``bP z>-?Dr?^nt9CK-D9=h3k?Ar*4u86Wuq`fwF#$*(mJ$@XS~^G0eQigVQNY7}#_O7-U{ z;6g=8wG&AN7{!rW#*5^`e0Do-%RNXfaBSA6!d6fZZkh2uE z(T{4%CV61I_=8IV=ROc%ruXxJ1pX`jNsrD_9~|UY$Nham_{((3{C7t9cSiXCL`G;B z*~8@uoSP>9I5)k0`HGJKGr}weq?UW-ANA_7=&Wx|!xSgEk3K4kPp8T*N|Fj^42tAD z-KSItLex;|=YFxE`rMA)=KSgV0#R<=#op1O6kSl9o_z7@R?8iE8%^7w_N{JQU)4iI zTj|Y#9<>L?&;tm-wE)7c6>!CyVAn{poMsPq1OuF#;t3qfr0=Zh?a8B)l%kxLEuG?k;5$ljFhT$iGT{ z#P)l8=R2P*@ER#V^$47N)u%&t;K%dgJ3ecr`}i3AYkC}t)4R8iBvtWysD`OA+_~Z_ z>*uQrEvT|X(Xav1@7Nq|61Ae@E58Cx1nspyFr`b79e;xV=2rAc*BMSqm|H=wpqMeVteKSS(!On?v>>{Aaqpf-5j{j^EM+T-5o zJsr9m3KCA+yA$0MWAY4ZlXe|i7RK{HSR%!4Q%K&RNwhFKwPZaD<7QtpDmx-F1Y$Cu zZ*LpTvWbo&GyDW$n-ucry<-lLl-{Gr+iJHo3GY^B0{mhyxC_oeZ`mPk5#Hd0E$h$) zKj><5g2wVm&Zva&{dh4bAAxlC)2SblXH9>Nx~p^`IkxROnP zL;#=!rlX-Xs!a|QR9jO$ddnl52U;i)I~Niu*#U|sdkf*&5R`;@BIbrZ!v_cRy5;DM7E3n=Mf*#x(}cSLBXN z*X|-0yLzO zs;l)6Qo`KvyTnzmoecJKI7k9y15I#STBjJ1XxVXROS2_$@$1!LOaN)T%C%BBSK(*` z`iAk0@gs`3E?UsKtl~yR26TbYX{Jx0zs^L=#RxYtKsPnV`9liH^5UjPKsVh0y6ITi zsdb!r#E}~z^=|65MCQjaPh_{Qb*i$(4aJ&N>awMK9q zvQLF%Y<@^{u+BJ8*sao`PsZ9jw#)V%Cs7da_z{!(_QROZfC@#V`8-4!j7LwCcI@_I zUWd!sL;6XafN7^!ms?EK<98ur!8XXR$ZXOgNlD+VWzn2tMHBWFf-%UaT1QD^0FlEA zN^hJCv!qO_L8dT6Fv!(ul9MzyvkD~|#3!G1WS?&gw*%3{F76; z{C&|K%*TW}%M@kWO~(?8=*{wX;_o>kns*duRgsSX1;en>7Uo8_CBY>7G!`pzTWh|s z+6Hby>8eX~=x{b@(U$MSw^7j)MC|U%o0Jytz55qf?v~Aw2dR z_}jW3kPH*{7TRVqh`1b1zT^6Y&nG4Cg=+CeVCj0-@@_vd8)=W`?8pozP8G8Ro2Dy~ zG3?sS8g>Q0DhojM5hmtj|ENSMcoY(c7iGV`uyg(6lB3x7qb4lbH&&)!;XT?PLtx;H zx?wKEIm;FNsp>4q<&fuV6WlJ#BFiR?&hbO}%Q@~ZkGJ%*PLcrZWw(x_AoXe6 z+~@idt)^>ayt+qRn96?l8ffc0dJT?Hcnbdj~Kv5^U%M%r-;za$gn)nBVe& z!m#!Dfyv)K{UHl%_w#Kb2j+-$z!L>{DhO(8vshSY3tJjkn3|gd^TsJ?>smR6kr73! z5s873;em;*a~64M#%X2{a5`;$UmZbGeJspBT=kUAp(2~GzK$L}p$y|_5m*{5IM%vWNUSJkZ$Ql=o(WC8oAegv3oNwOnoe9iCWb_& zg4!=`DqVx;@(8-|OXX^1rn|m&WE5~Q26#YGZhAXz$QXtkefn1vvJ#V&pc$c*g&r6i`+XStL-hI6e1iS&eB$qX;_rOo ze>0!>(~_Ix#rOU$x&JP?|1P=ze^zq82F|$uQUfqD{5?nhJxBgMNB(clkrLxY-IPG5 zV+5Y)|4es(S9AZP4E$r+&B(wo{rk!r_1{{Ztc=Ue_N5Rj*Z2k*!VsYqd&lusqJNmKpO*~VR~i(6KDPOPl>CBs zO)6Q7vj)kLF3#@l3mW{P2v3Y2AOa}Fi<^y?VA+#W$VnH~Ezgs{;oz{cn@ZC@MRBUc zkbpFrQq%9KX9bNM&8-24*E8-^1D8@W-tB&1HDSiZ=ne!cF`{`QKB?59NrB#$=jZ+!@3^5#{R?#1b@i`8QM zN47%P!UEYOG6l~bj;y+3o9#Wv<8Re+WUY={?~m_C#dK}HN?YdE=_&EV9N65F9A846 zSw3mG(K{>&NCs=3H5V#vL$<~M|2i8WraYbBb9j+3Qdrab*upvk+gs0BU&N*=Ay48954(K1_P209N-;;H`OCc z*Jf2t#PC0ZXoG<;rIjDWk1;GIp!_qjJ=Qrs=KyT?5YN#ZM6~+%zFfPuSU=f5q$ z#lzB{=!YPOHD>{oMKG%5lo*FmcCHx3iq{??pgwmXP}>dBcDUAImsA44X>@ajQE7+A zpM)S7p`C!WbEe9i1EU@)m2rr29Q@*0+cv+r)o>gQ?lQ*ma|vsRl+Ca#8K%%sN)`Dc0aRiz%*lmVgSQO{49P4=yt z_-uJ13qr%;O>MU*O1`o(InDdJkv)41=ayw@kh;!HfKM3T_^+-9Y#Fv_T9YQp)xpfJ zJJ{h0Us~TZ8I{of&3pf7eMcu5e+-)l?s?$w1kI9Wwtlwr(99p4i|?du+#&8DlW)K0 zBSwj#_<^dNJWu!ersIU5=k{n5yFuYpGJX8V*ag9?vk?l8KP3FD94cvAK8T!fve9W4 zh2%HUydGpTu1(r(nGA4BQ)o(T_`C^C(pRS8@GN+8^Kuz_P)enOD%fHlM%=HCgT@Vo zrQxY*ud;igwl7|IXLoP!(!X!O-ZA{EQ;VM%EUgoR* zzU^Imm;XWVXWJVZR+DybOJR{c=Bd2^y0)9OFyfse5_RKOMN@r6za+?_%<|4zSXP9Z z2U8?kRX|x~HAR%N?vi|kHk2-*y$!WbBbv*|OV8V4Ssc4gj<5+meEFHYzkA<*_rCvEd*435PZ^m1Z5{q{&HuIup3$ypw%9*yMsTv?$lbQPon`uJ$9nzNb)lu_K>qaxqB5mA z#s0T}_3`NxETJ}^=(Bb%>wKo(88&pbRy3JXfIWUMjbi5Wjf}+v!{<$tuNJ+YzqP@0 z(YP5@)hBx@Y*KrS&FT8C>76DcCeb^wgH>}B&9Z@Q@TrNPZSdhNRg+)a;EOM9@ST!x zYOLK(nx#%V_)JP+LG-_t62R)=Urq2|1KPjsg706T>Cpj8>x?7NpXIfPy{XVAeR~T- zOFKFz3-cpQb;r!%TK5y}4(o91*0>c&nas9G}@(1?7fuHhGJ z(z59!9ip=NCS~s~h0BYhrwvC~b~HquJqUL@u~b=ELkG3xv>qh*-uE5qZRVWM_3ydU zK3|=*wc$OE&6R0?frHDK+GPGVd>8`RZ}mhu4-MnqhwZ_|?&Rt^bkmQ3Uvlra=se|u z?2=0c{{aqWp6D6R1o1Y+jpup$KzVYb8aZQiFU$LS*O3^o{pBRl{fTE9ei3lqxuy+q zZY!jH04vakhb={>e5(Cgi@6%KaI19Kfo{Jv+;J5?Qn<-*2eDZ?;pqeKh}oI{g7Do= z-EvZH>-gY=_uMz$jQ9A9-H@i6)bLI6`Uk&ksJ^amb)Tk| zq?Vzi%kf9B7h;le&c-K|R+he0a=yuxP`1*|2vtUaY9TUP6pXBMWb19=Fu#nH72(`x&rj;$anYPwbh!kf^Qe#z zq5ILpIU8{AdgUVATGPm^I$3q(QFGPGDwLONsTR3b;AzN792>TDDVBF5oz`Sip&^H+ zy5;d?0`N3JtSo<@Yuw$Oq8+j_v!@PWF@CLtJb5KvS{$udBZD@?krC|L5_uGg*uZda z0pS|JNvc`(Kux0B3^`wW=Hx=}+;glCj@wT@6c7*4=kqkEp&5LYP<#^BH`bZIbe?N)pTGP>^YB^hau98YkR0wp*Ld4~ z!-2?Zr2u3nvK_=Sgd0IR#xP^#ZPfMZdJnraL|7QKO>^+b7+q~9y&sYmEDmEb;iPt0 zJpbD#qAvno9X+D#o{mT(uo3Z(tFJtHbgrikn6a2}X0AV7cn7JE*wBgmSgO-`V|U1| zmdAVFog6Oh;UEDKB+94uTqU5Hq7D7UgnymUD`(KxAo#H2Y~wQ#MAZ#rX}?$W@-~<@ zHSvsZgce;M!K5+9t+4*#W+QSv27ZkExe!V-G}M`2&iG{~dr-maO;r$7^)M_geJ-z1 znj`9-v1M0Zf>~3(cTmetY~?~CUOC*7xU#%e)okxYo%G$Cdg}6xS<#2Saio^S2bA)~Ds&;sK8?jPJGrn^;+SIYU*Kyx2uLL`| zh%|mR3zJ3!|4@pr;wVz=-6X|p<6>1hXS44N5;m>5tU8ji+Q2d}8+&ESeX`~9-M-#^ zG*B|hCt(C-W}LXl6IO|gH!z2{>IUw9bzZN_&%JE<5Cdh^SC3y-!ndmviWA|0A0XE2 zYN)Ns_PxEVtC885XB$6w@hc?w7w(wmih^=X1}S`wEZvsy2*F)+FFIu#JV0wG@}MZh zH32@(id1r|>2kFv@(*Unrp_k!K?qaaaEJSPIpT0{2_}bzFQ^7P&=U1rHfO%-BYQj2 zZ*jL_2;$OsIldehn9<1tpxe&Cki60XgOYh|6cwc#592ndq9t^7Y{(FG6)Yy0vjdNu zo1YGg47oPJ3ubb5k5^y_MN5c}z+K-|R=_m9PU4C29l00~O0UdyJNMT(&oK=1t9pGA z)-4OO-G4tDqZkYA{S>aeYfcmW{EqsYF};4be8xL~W9PBIQ45JQAhNYphzes{P7FDC z89N}ch;U|ZMpBGQJZgAlK$E@PlDYhu9}DGe`3*$lLC~(g_%JIZ!!gKcI|-wDpq??b zwKg_~0lYQ6>w=A#oS|%t;xYOFdV=hk#n$5S37ITMRVY3UGnEhNrV{B7;Ev(SA0zaA z8HXazm>>;4Y=K91{j*x{IXoa+;J(EN1m0bHb4dUJ&! zJ{LcS7y0M&LH$92{3^rGHixFoqB2s3LfFnB)O|55H20)fgJ;*#m(C$vpk|DW z(aJoY2MKd9*n<@ShkW3bv>EuYqU|3Nkk z-M2DoDbtJBVVLsR8HWfE2Zi}2@$-F;RPv2i+J^@vz>+UfWkQn}A%wAJ=Pl!Dy3yy; zXdTJVEHH0T4O-NXrr^!ws`FT$6E81;GmDr&vMD1Nt`&*e;=HaKrjnQ!9upLY7Xtgd zm+LFMTtzG%HdPSZQ!J=RcWA-H@@*f{CF6<)*qNQNV=bdc zU9oGQU!IshuT$u=Na+R(I42(Tr{LZ048AYlo)V~r8#UJnjrd^mRTb|3O^?Lp;B0;P zh)!s^jC0~lAX+hUGgI607_kazaWQC=%XYXYpV=&bKM?UEEEQrd=`> zQm2JUEW7MsHBv9;&eNqjO68B>Jv+BBD%n+*=;V5@yTm&e1E$oA(E}EwNyHH&ML7|_ z!Lzq)zp-*5UBZa-XL~qGSxFwf=HonUYLB2TKhMpiUAmCs%1*~N$vJ15n`-C;xE?^_ z9tbVUlvj+>E~?l{qt{v{FM*fiFiy`r0t3^)qf=~Exq451!j>wdI4M$YL zd><2>sjgHDX}}5$>p&(8tv_X$Xz=h0r9cjux#xcKVw4TpTYYwDhQlcE{A;BQ9Z)dWIft=u&B)bSAz=YV^c6bRI|6R zd{eTE)hZx~{h)LZ2G(QdAc-I*cfOp~h)x=WaFe=zX_8@}@I8=@AQW!0*iR&k+jrKSTBG8%2=ej4F_j22^+$NPCiIgA=kAS(A zK|y7qw)g=9fg92pP&grcvhc{sd|*TFGnhK|-5l;rd4-XyEMx*Rdzv^WwI$kxaf;I3 zQjgYzjjt&(#IZ77ZYPyILe4*$Gm_4d?KK4DRXC;naRLIRy=VbIynx<>?J{vb)=Y`d zN7ih}QSN@Sj-jO)sYX6$)_j+A&GM4hq9LeEI^4R*n(Z)k?(S&FaoIeAHNRJ|(!8Ei3`GDp*dtZ%nX zKJZDG%$PNAhfC?S-f_ljmJ2GolC4ONkjR##?-TrrT|DB%(BFH4Hzm#vB@Z zT@`#;Jm1&3v6+q;oeGXm!$XO-KJ>l8oF0QP?j2$u0sD1*-XLxKsc{cjp0^3ZVMuTAho-M+>M45E(kB^07eKu}3tRND_JA>5gQlOSUSt-> z3O5jaO&kr&!M>KMikL{K@9CP&s0E*`?4!_|SUwWt|4u7JmS9M0>|Kp_V^yciT&}`J zd?A~{I}H3AVIUj64=9q%VB-R;l~z5Xa;UG)B@~EoM%PBCsxPQTpgIn%*00uwJO~m5 zn#+H*Z-57fOAqls!sxEpRh-%Ay_aA|rI=2Tu8-7t z8@(Jz(~xzF0$(K=4Yq*MUppCLVJ(dsVNs#yzWxPPDnFK0Y-NLe>&qZ_6@P6H{HEYQ z3Nmq*-~j$f2TYfdjx`H+RYgv(N>q=Ib&WcP@*8TqY?J-1qKg zPx+GU%Z9gO5F@!8hhc~&;SEC-pKde;afMX5x1?vluQ{<&Kt0GUNtA@RV8_6Za8a05 zX_{ev(^$2)%#2&P7a=Ka@EO)#c&Ee7P8@%q*?ZENdj8FuGS&8)meci2dka2NP#hTJ zL|^oaptc_$o;Ui$U0NbTAC|3zuGn0*Ik6w_X=H{Qy4U1g^UYA_(3#IcfE&kxiZ8vH zB(#;MHnP`@?NeY>n1~E{wQ)}yixwri<$j7`HkM8BK+8JDEeSFMssJUE0-wnVG)D%X z&mJX%Q)ba;!BoZ-kpbw8z<}x z8$^@Y$=QL(81$-A?~4^?u_s04h^rSg$yfoYLr3q>1Ggq=@d$5N?6s03`5g|C<8dpf zsZw`Q~t96gDI03!b322+;;OXmp%@llIC*ttgoDJj-$?E zC4<7l$zR4a<$v;5IM%;v*~%M78n5!Vm*WnO5vaoE+Pre&7n#lKgsDeW^;Ov*@MA@& zC^itKzk|RGwkgOC5IzR(LS7A-*eVb|$hQT_rGN9L0)&6{AN2=n&RRg>R;_L6r%3( z3icfz!ww(JS!Ol~xB&3O$D#mIn<9?KhY<$pQI%WN5%Ldt zVWkap)g4o4^g~66ji}=(ht~Jo6Ki|bM3vq#DA5oJ-_gezeZGf+ zxD3*Dh0ty%2(mD%?hoUX-+_Ol9Y!&(TaKl1_mO$@nDQF|d#s;NPc$oL9LSiFv@`1x zjNRuPSAN-uekXEEp^i|odRPk?G#WgBst{kuo@IcY(QS7GH|p)l{TmzqEpf-H0?7fo zLzUc8QaLNh^qvmA~G9RHx@av!*@~3^`F9!ME z@;1x{HTSAk`mcmCA7|Kh?QPW@etB@yYI7>1!014EO>?$Pc|5w2QgWGj9z}-Q(}3Gm zKiZB$4-_-C`w;THXyV@vgjkD4mSpW*GA2M~UnKU2w`kgq!P$ff^aNXArw6 z$ylgZC!Ijd_Y3p7DfNuAe!3|t641=ANbxoLAQSiy-;rn(dcYIN@7J;|NcVexXvZT# z>G7pidB}Fq`-;=%z+NQyC@L$}g_Yh|+Dxhu8&VLYnz(MxmV;#DeU84}fzcWCq6E)P=Kql3`4K!HKHY{y z27Az7@b;AAew^}zb3-j4Y9yBd!oW3DYL)7d5?@?gm1=NV#A>dW(v|1+fne=zXxVj# z&xQh9D8zLZ2wj8dTJ2Vru}KKa>wqw%8oB;ZF|J0C$s-0$*$pj-IasOqa12ot3cOOka~!96fX%?h`9PZRxb$dI6xzd9>N&Hsnf0N5{etu1$U6a>TWwg0siAd*Rin>p-+C-ncdo-80 zLN|Ts-?4e(@XdNMnvj0EcFC9O__0`l#~#|Tq@zH@sYV|MZgyQA)2VNR*Smcw;YSaI z62X`BLIxb2rCxm@k&yg}{=JHDZ-HB5c?#9I2s^CkFFABUuVi{ZGdb+URG{$6Q;79s z)mK}1C3^K$4=b9=HsQb!kFAq>?-}W5Q?FrL~l~tEv%E4RxEn=AlFHMts`Xo&{+%tI%|NaJE61V9R;~PU6 zTC`rp8yM}JW|9~ou~-PP;UPIO%9w{HNjTE33^y$A2<2$K20Mzvt+bv@5_Gsip(j<8 z?MfPIGX9A3C>5Fl|EOn6aO4tPv1qgRPz{(f0{qdhAA?)gS;)%7Gc&24SVjnmHcoeAn{bi4^@%L!LFE4*N zUskYvFtz;0>-lYLbe(}9FflQ)|A6^P_=K&Ku3q2Ij0ZdzZr;M?E}L82lo95il{0R z`M3M@NBfvxhHo5<@-JEU0{ZKt_a`V)KLpG_8@;zLqvzyM;uC72lo95YS*U= z?a%CE`FVG{sI2k+l6^qf|G>UKL9Iu_l>FJ9mmTX zp>FT|CF_2I{&Q6RH*xPzP{`Qeoxk1Nm-zonSZ4Tnb7M2>3IBtEFYonh;9~^h{=4x* z2BQ8Lng0Y9IAN6grzQf!IWRVNJ+c2$?*03T?cbRAC)2?1QUA`wKY@vaC_(*pkN-cT z_!7}S_pRLiwK4pYdEmb@@OLm!FfqCxgVCcTg!leV4i~g%ng7~{5-0? zUmkwnftQi^75V#Jqh6+--;QeUB;fV_wT$Rx5z!x^f83w(zo72&zk~kN>;Dn^$6e69S+q=Pr(kJoX(@IJzS(5%z5oBsj=S~#=KXH=-R!(&W|-xz z%gWAPHf+5(j9EU)LD>&iQwKXR&xbZBe_$Z7Cr3Ci7w(Jwi+%MsaPgekdi_U^4U^S) zRprSQb0z{~&5vhEut~`Jgal@Jv$=*mdp)`JoEvCz$Ezy0ZyI)Q2Oq<{=Sblbw_S4Vt@r09cx#b1@j#l`&yO@3ZjC8|>i6zg_2hrw z!k8o50@vqeghf@X%R6^e_Z!4ajv2foLg7!VK1tF)y7_q>x@&)QauDGK+$u1>Z{h!r zBG}!T_JHglwKzYL*;@|>x^+P35w2H zI83MFRh1h8leD9`x>ou#S)Xoi{l{5nVYDuuJ8MUC&8^nggtjx~>Gsx71jWA2!f5@~ zTRWO-akcKfKnx1cLxCwn9L*KFTE8XhGwrRvJL@cr)`z#8wWGOiSL?pe zHbZ9HTdxX=eVv8TdPiXDYe#cE-`T&a@`upYT6WxGEzmrM9DT{wSs1M^3QX3H<|<#U zM?zccbM37^I_oTq)<3*KG`dak|olAxF&Ss1Oy0#jc*n!gID^)6W#r*hgD z`M1-~)1d&5msAI@owvvNTY=(N1h+|2xR;d8Z4}=W7W+F-qxhGExC_o-5LCSPhQ!@_ us#$zaSQ5|DC_WULGNll_S69^iPQSA(-SkdwI?`FZH5e?+UpN-OBm4u_Q5ZP@ diff --git a/src/Mod/CAM/CAMTests/drill_test1.FCStd b/src/Mod/CAM/CAMTests/drill_test1.FCStd deleted file mode 100644 index 7a82630b5254a238ca7a8e8c99e5476c5e1fbd4c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 31979 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 diff --git a/src/Mod/CAM/CAMTests/test_centroid_00.ngc b/src/Mod/CAM/CAMTests/test_centroid_00.ngc deleted file mode 100644 index 974c8a6250..0000000000 --- a/src/Mod/CAM/CAMTests/test_centroid_00.ngc +++ /dev/null @@ -1,69 +0,0 @@ -G90 G80 G40 G49 -G53 G00 G17 -G20 -;Default_Tool -M6 T2 -M3 S0 -;Contour -;Uncompensated Tool Path -G0 Z15.00 -G90 -G17 -G0 Z15.00 -G0 X10.00 Y10.00 -G0 Z10.00 -G1 X10.00 Y10.00 Z9.00 -G1 X10.00 Y0.00 Z9.00 -G1 X0.00 Y0.00 Z9.00 -G1 X0.00 Y10.00 Z9.00 -G1 X10.00 Y10.00 Z9.00 -G1 X10.00 Y10.00 Z8.00 -G1 X10.00 Y0.00 Z8.00 -G1 X0.00 Y0.00 Z8.00 -G1 X0.00 Y10.00 Z8.00 -G1 X10.00 Y10.00 Z8.00 -G1 X10.00 Y10.00 Z7.00 -G1 X10.00 Y0.00 Z7.00 -G1 X0.00 Y0.00 Z7.00 -G1 X0.00 Y10.00 Z7.00 -G1 X10.00 Y10.00 Z7.00 -G1 X10.00 Y10.00 Z6.00 -G1 X10.00 Y0.00 Z6.00 -G1 X0.00 Y0.00 Z6.00 -G1 X0.00 Y10.00 Z6.00 -G1 X10.00 Y10.00 Z6.00 -G1 X10.00 Y10.00 Z5.00 -G1 X10.00 Y0.00 Z5.00 -G1 X0.00 Y0.00 Z5.00 -G1 X0.00 Y10.00 Z5.00 -G1 X10.00 Y10.00 Z5.00 -G1 X10.00 Y10.00 Z4.00 -G1 X10.00 Y0.00 Z4.00 -G1 X0.00 Y0.00 Z4.00 -G1 X0.00 Y10.00 Z4.00 -G1 X10.00 Y10.00 Z4.00 -G1 X10.00 Y10.00 Z3.00 -G1 X10.00 Y0.00 Z3.00 -G1 X0.00 Y0.00 Z3.00 -G1 X0.00 Y10.00 Z3.00 -G1 X10.00 Y10.00 Z3.00 -G1 X10.00 Y10.00 Z2.00 -G1 X10.00 Y0.00 Z2.00 -G1 X0.00 Y0.00 Z2.00 -G1 X0.00 Y10.00 Z2.00 -G1 X10.00 Y10.00 Z2.00 -G1 X10.00 Y10.00 Z1.00 -G1 X10.00 Y0.00 Z1.00 -G1 X0.00 Y0.00 Z1.00 -G1 X0.00 Y10.00 Z1.00 -G1 X10.00 Y10.00 Z1.00 -G1 X10.00 Y10.00 Z0.00 -G1 X10.00 Y0.00 Z0.00 -G1 X0.00 Y0.00 Z0.00 -G1 X0.00 Y10.00 Z0.00 -G1 X10.00 Y10.00 Z0.00 -G0 Z15.00 -M5 M25 -G49 H0 -G90 G80 G40 G49 -M99 diff --git a/src/Mod/CAM/CAMTests/test_filenaming.fcstd b/src/Mod/CAM/CAMTests/test_filenaming.fcstd deleted file mode 100644 index 398a99b78382201bc8789a9c368eac814766618b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 279228 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

--_?*|RZ&#%j#;8g_Yk*y zSLMEcb~Z%K6ud*hJKBh|-tFFAYQZabM_1u;-5(`eXF;dn9SYvT*yPgUkOl%O3f`gM z9SYuY;P8&xkhY9|44J2&iGPM6)x1ea2Bsmo}AA&My{4&4ow_s?-%c9 zpTjs=`qQg9h(_r*yf zZzp$wy^FFn@0Qc)W42W4lGLAhxlIL_sfx$+@1reqE>+8I9*?Yn4oHlL%q{w3SlAU= zxlPMLxlIejD>-1shLGOp5{J&nvfRAH;fuk`K=1CF3`kB(aW3Fh661)>liOUZ7L2he zLx8gTxn$@G{%O%h;;K;g(Ar;tc5lDDdQX8Lljn3IvuAov8Rwl9aoXE7+a_ z%oG4Ky_EVqV8G1!VkSinu05wl>5OMk*qNiV`8>}PSq0c|>ItfDqIS7V-K;*jK2lnJ zLVyiufg7NHCLt?CRdF^f+cZ4S95&OOb20i{b^G<$ne{?L)i{K2Qjw&z*szKug$E~I zR*gdvoKz?pg`#;<6iqLc&KsdF9qjlStXq(Sb0 zN#c+1gIEfOs~TSojV|8qt5@u|x3jx&7O;uvXoj%{AMl`aB{9=fqc)!q+604}eUua2*(Qu?2&d>$A!x$MZ+M%usH0tTj2p&~y zq+1?hbgZNVhpVuE9h5R)B_&pyzn~>0`es)C6fDC34n9Vc^~KY(RkYSQT(LDeZ^+&g zMBM!h^vapa3gs8Og#h-#&Y$PXOWtErfpifYYlDP z9JV^HG7n4`EDEd(Yj_xVjsdCBhXS0mKpo(~*YPr_2AcEhT53~>Acv>fZRrRRc7sgu z=U;J@MzvFWAp2OfWU>a!7f(;}4ppwC6NZ7vdI~Y7ETTLzm|H{=+FxY`;SlSe@*u~w zFSrC@J1~gz$teThWp1+aUF)6j3QoTdaQYn_oC*=22Bc(GB0ba`YRFd`+f?abia>--F)>9Hb@+3t~M zT`@2%7km=MFT4H$nSk9axUGS>a--$B`b7KnGj=8=dv<=tU6r@m3>MY4V6DgrS68mK z>RdEzBq<4pbncs9daq!aRQzL(xF*xSV54u>)5IKwTj@>LTZ?*Uwi2FYYkXe zDm6o;W;|(X#_sCZu2;9W3~+mSrEFW_Q$$c2#dWHl!Xc&BmtyAJ6J|~%1lhoS zxK0K`f{C0XKU`~Ij2v&|W~FR1loC9}q9E(fQT*Uff2+HQx?1ul(X0Sts-4a5ronH& zQt$L?dc`My5-x$!2J&NQ+ru3Ap?AU|<;alkTYu}w53{S2es6um3i+X^Ik!j6sY=;y zyOb@+PoD!dCjdLDlx@3Gwj;&NnKN_R`&P=f5AMseX*9cI;8Ujg$=O)ed!r8s{4wmR zZ9O6QL#RS$=#Tz0ycGJQ8~Q^?1KlQswYQpm3!V#w{-_JvDq>DW%n1lz9lM3lt0G~m zpzIa`7K<7HdjH2p@8xZgL(tgQ+s#Y;DYy;d05!wTzK2;>bBXjX^*^t1PiMtVcGqus z=%1cY?^EzKh!=I0t9F&G(svewy}R-BAg{vfFeJyyC^=nqBA87GfbR+gN6r9BG` zGHn(5L!mzu`s2XSA5T!}7Li<5^_G^i#b2t@Ek&RR_1nWO45J`1mEhoM?}V6Dv=Jk4 z+ATfIai4l1|GDM90;y&VGtkJ-9EZfR?U4%)iB}yZ( z=EfBbcJ=xJ;9-QwpJ(AUS9lnOhf#PK8O?J4j_BTffX|~;Es&rJ57P<{gUVoQX?abs zh*xQOg`~n?DjdvTeuyTIQaJoi;baoba_!V~4A%j(ve;u;Y5Ht30i3?~ZLn{ZYIlr> zX%NEbqenmZi{M>=GC#4{@FkI`2Bi?lZ9E*sX(>7b6BGzWRi|B{N@W$5Dr*g*vn!1U zsh^7ouz;k#TEmH!0v^fD_i_(AcZ~9j)UoGFBt%1zk1jAcUP0P$${DbN%ZpAPC6@PitR+P zos`*5;Qml}7ln6Gc$WjmyC}93#dflsP9L)xTXkse3^ZXHLqoTQ@=~G)3GGl-m-YnO zVGaj|;TkA?oY9>W*-mU{faT|8WGACxRt9Gu(H-8v8rehCtj%Ahre@W;GB>K|(vFKR z*)FOop>T0dy#w7hr{1y50Vy{;%!)2~@Wk$K2{3Pf3d}<=#%DiWDu5jn^G;`a{JqW3 zRY)U+G}<20NLYs*_1s07s+*NuT1{a^$t6&l*@iPRUB_~UBX49$N-pV$DjasnC7dwN zfOrR?-yCQpl(npi-}5T7zFF<^6qx~@dj&KiAblFcuzRUIb~c?x-##v!~0JVmeq6LUv&mptrY}`!k zmnGkl8?@VSN~+XmCPFWjS#to85Y07W^14% zN(%R594+S=>gn<^xsQ;l#U!2Zn&>#mp0BbG<#4L?J|V>KRbUMPlN05LF{zl$s2d12SlWkT#89Fetm>_)9AArd02b9NWup{3CY;{ zNfLwO@JNdz`p|!wuhBN?Xxy-pe!hRJ;qnlbky-{UaMmMi`RL=0DD_ z(sg@%A87xG;=i=>D2gZH%ufRC>T!_-4~uj{1C7e0e&&<(g6r+INR{&R|0pgCp4kZ1 zyB)cboR3B~J(<(0Tu0L&_CdcX+(mEjzU*Q;@;SQ;6&vrcXU5UPe3_6mG$KzH|E?`S zZH`8@a5jzuWK=FRe>`5s{`m3l+RZZ27H|$PAa=gZ(r4QV8tIm+s)Z1o#hu7YVWP9O(y5IoKUExK*9eO&3Ay`Lq) z9o7V;PN$~_)41NwmJhFwWV#6rR^>Mq$556;M;{@r@~<-A)PgHYw+@`t+8nJO&snoX?`w7-$#+!03I^$O;l26$tXHfK{>?u|lR#XFgJt`t2VX%( z5}o4)B2$ZQ{)Dh(iUy86sFgTMiDn5!rrGp5(!;FCA;7xWam!s4IwzP`*~^~ebdQ#^ zi8hD4BgaAcMRCZ}RSx9~(jC7&YJMDO**YII9SQkDFOEdvCAtm==`dToKZvP7$jOk- zr$F(1f2%zZnLdZf1aHz;OEG8iJn3K+RQxq7DINpu z7ogpQC^$p%xheVFl2pyy>-rA4N)p9lPF8`mDxJ**&7K_eTB_lY=6aS-$e-`1+@t!0 zcss5LsK%1%{~#)qgyWyje+c}C5KO7i!aRw){SZn5auHC`v&n4YS3~ zyz%{UaO;ngC=MoH#@M%U6izmIw$)1&zO(iF6`p^yBya{*y%~Hhfr}NpT&1|F0V}MDD7iB1ifUIs+~mlDsN})Ul#LV9NvawEs3aZ z#vG;p+I^pwO^)N^S#*3K_(btLK8CaB9uB;YT3NvaiBj$C9iopyiN`La>Un91h=Ts} zIoHET_%e6yLNg^wd*6f6`NiYtyI2inA&v`bYDkq7dJp$_+{`{NZq8o6KGIND2d2a1 z_0c2otv*DPC5z-H_XA?c52$B!JoOjgJZIm~gKd8mB+L2LeGrIs3YnpiKc?MBQ@C>P z*y0Tm;#=h2+`ioNMWdq7;f~AqYCa8<8rzvO?4yg*-gIU>k z*~qIT8pB9#VNhO0({PfS&O}1p1lke~5e+U9I0VOkHMSU=+0Lwk9p{ypHS*e2A|Jtd z1Q+<2_yV}ioS+T(yUB@7yyJOzqL*+}aXqu%c6{Z};Uc^Vu_(n-iLGRA6u9d_8L%(bM0Ni`j`mY7$b&p7GEgsRILvU{sOAl8P7Zj_ta2mLjR^T+EXyR!V z@G=d28o&N22=DHbTm&@=+i;&adl*MNjcVtO5cM;|5uBI5iX7~-;9J4#--GH>HnA?| zRDJp5qyPQi^1&QbI&yJ)i*9y3@WqV^HM^9Am78ATK_o`la^c_67&7&R--f|-0>}*< z9+QOz&8YH~g<(I|ZXR*xtq&KwhDT}GnEc{{ntMXj#;XK))HlH#-dP{f393SpIE-MD zd;<7MJ^4LM(Uwy@0A!i^vxLNa8aw(*&(J0OCLOymYmL}`K@ac#bdiXkBNU@*>ioeJ zMa{ip9|^QbdI0w6{VB{-W31Ea+@_ysWQp`(66S~^j%=z`Ps$Q%?X;9{Uq3PB`|GEs zFD%qE*IVBK7B{7$a>LV+J}WpJGohP*!%hyx{Oh_NdOt;R_}`!WIeh3s5a~Q1P>uAU zBX%_?x>5r^)fKp)h0`@Xfe;=3HJCPegWjgOeKYH{^ds|hZMf0SUwP!?%nyb&{8QG@h zZ1rA)AvX!ObiYR>Mq@^TGtvp@ptWe`gd3D zq#^IJ3yZh=>Qy=plNm6ZSc7}7vTgH z9+M@k)0^18<>^t#oS-6UK@vo#OZc!6WrNcU`f{33^Y?%HpN9VLB=mM2BkV)MI=4## zxkr+P_KrwR`WZ<)C8;Uhv3$KVG1GCnCNgjpn+L)gtM0U(^p-I+WC4B(7SS@6B{Ks>km;~n=o2RjyV7F#d&!6@^&_P z09Lv&Q2IK3j+$QER`s@KKK7CtqMWp>hz1t~RT9DU^DtQCS58lu`BV6WeZWatX`0g3 zP|}ROq(zoih2w--Su{V3pm34$8Ga>jr#O1(ZeR3Hb@I$zZa9K}n+12x2;0&Gy%g7; z#6BT#iM|oUJe@x9x`5*MhElxTMBj-;jkdvJ^EJ?Z(%Ma>?t_zmiG@1r$qL z8x*u19^#<W74s1~V*vU%yKAW>xFdtd>L%FDWwLv0?{QJA{=^mpf@nAF>dSJx1 zJ(N(@O$(`mgiY$$a|gQNdb(|o496I`wg;Y3#U~|*#q{aeZB93m>Y^O1POOP{P(i72 z(gFq}T6}%`C>^~QA0>IkwSbb;=Umj(NuFIy2+HGN4qj{s1IjB&T+hr1BSNI<_HW7| z+SgnhxRdjTFwylUMIj+vss#X=WGth78ZKOW5L75fVW^SdGr+~Lyl>D#zBo8p4&@lR z2^${_%}6)9?oGTfca{8%H)x#sSa^TR{G5`Hg29R4l;B>0u?oOen53Qv$`{nj+pnfX z%EIJS7L@~}=l385jr{uj)5;@DN^g7dRCCl6Eqf$W--n`JnioB&32LhuA9 z)FFIAU*`mA`NSnYxE(JN;p%M@YRz_mNX)I26#$qq4pAF`2nVY4^2arF8tMKK7|Xg3 z*Oq}d=PSFoLF!k!lITk_777+$26=cY%>T)U4f^k(PoTTD`7lU1T1&%V2{=_d>SEa@ zZu$EBIX(bmHlA7lvDo)(8^*ve%p%aUGnVd|d!3u{FjjOH%Dq@v&yDwR!)ijCi#!PN zlR_L2LNDoD)gV1rMaw=V&d2~JD*}%MTtMM_196A(Vtc*uXh~V*SNJ+QZtbl(f4mdJ z{ipw0M#;Zx$+w8Te^KW0uc!c>|E}ql9U{|dJOvJS> z+#;DPtaw6_kmLh+XvwHhTsXAPiS{k*MQP}r@}@Dj^-ot*!?vl~yJ zi0oaF#KsVwIH2ss8#naJ{vhW zGeTA=s$W#l*D-qWbrKtr_;;ER?^V=9QofTPY1A~PbjRJ*pR#?N+P9roW7XjxT96G7 zG*Tf3CO4%SBqtiDyj!sE0Q`dR9o`5jU>W_1v|6#$~I@{ZD+iHg0I2+-JVtnepcJ43NE=F%)so>028?vi`U zuDVvdZz(?veQ)~v0l)ax4EXEwSK5m*;yafUjeWX#HJv@QiktYTzV~W57&%aC2~6}#7V&ey$jMye{b1xafQIcQ&h0v!pTTQe6RJ}4z<--0ePyZ z+7mgXEStZ1dECLl@9&}5}h|vZve27@BTCsRR87-$QZp#5* z4NdH3(r_;^S{&t-$E>B;tj32!b9AGY7wdX`lz=-_iiav!_2PVg3Yx_;M$&&X4o5`_ z+$$;Y&hsy~2+bmiEX)x}z(si`4EZZn6nUNKcCaM#Wx$fCIn&jHm|2w~pD?pGIk#a^ zd4IRW5#^K1+#gPRqb1?Iv3PEFS{cTucTC(ffZQ-8lgo8((m})t50KTU4ODcTLdT_tO*HnC;^v}~`X(*XTxnC-1_y%eA+ghi9y&(bB8HK{F zFWSZQZz|nB`hNwi^48xWe1(pz5h5l=Oh&1M?}8cq8=M_uREcTqL6V;3T?Svo z5_c8{KCR(Y$3c}HYlZi3`YKJS*8VReb-9kF7%)&Vvp-*HMsTcqf7jTb@51CP9xwPc zQVWe=x(XlW)A06@UrVQk{dqnO{232U@jGCf@Q2UF<7MoRvxjlX;Mabgg-O<#{E?60 z3|+a50IeWKE|kcK!_rRZp|N@|Mq|`P85J@ydly z0n(PR0!3qUu}r3@uqYfFgx9B7nZj!q^I%5GtF3(5o8WfJSg;F&nT;6gf)FlFasTn^ z!V9cqBX7rzATz(QYmDF+rlHl0WuJE|<4k`l?p-qnhMgrE1$No()&yHv^IQNV| zYz7}P@Mk!v$D~#K7vygX#CeeQa08tDlCHVFvovk6?^b@gE*DTClLEI*7zGN;mAyrsGv zeyZgWvISN*4T;IqC~8h8@DaSCyWz|^f5cwA4x7MS6MZAvbJ2@?9QTAtd;8_V zcp^xxBMECT0Z}DlSUGKrap#LUrQ-erc8&ILT7mYV@>E)Py%ht*yAY>RdTf`!vv8$V zi&y?_rZY5pi~NC2Uf?Rh<`TX@;Yzy~fRg(LN-h$I3v3$~b7Cs2OZqrH=^HSePmdtE zas}~ORKVy2YV;EVrXk%w)=+9G0ChDzoVJmZciev^rJtZDPJ@rjDL_Ee;9cM+OAw?q z*GW?M73cAdB8cGo=o|AJMPqQSXb)-Q8nFkoA2V$}{jSDrhH`hy6UyXhO1%YN128ky zZiIr?@_sf6zlM_~5W_Wfq|2byZqZ}z64-QJS>o^_ompU_sS!3|Z z_KIy_3Iui7HFH>$2ZTPDo{cyiFe;Cl{|*14(S%!ip5n+L(U4=P_z?e|JwS zUZh4q(_xake72licL67GJH6s!u6-sd1E<6PDVk2OFksv8q(b<2m{LK8gj^_oMXz}# zTTn5GhMlXCO%$yS2Bi2Ey$*=ozs^-KIht)G`FK3QYWX8w8&E^jszL`d03qyvZVTdp zP+x3!0JgFvO7>32F+d#OE=nVKwkC*2M^@OWypW@am_)7hc zS!jyKjYtAKA%+wLp82IOUj-Kt*t~(MD2nL9{9HZ+Us!Y_3(MJgST05|h!Fhb-66pU z-XmOH#AxYcp`JE>DnRMXw$eIJZ{?cISC`7xO`;1E<_~ ztPXrnc2m(C*yI>3IlvdkAqCA!xVcXy_UdYis!wcPM?9n7$P+^KBTz$&BR* z0(s(o-U9jC7K|SHHWX7Pob=C-ic!2uvOu2F(#JCBHGBv7bWR#$IxqYvBtY17zsjW* zC+cF|VRprD;>;j=?f05%e>BYzYD1ktuuv?1(pKQVpJBI931` zln-wpL`zW`3h!n3((o4VPDBi;W(Wh4f%OnONBPLvq8;R5XSiOx_8MVA#CVN~t(NNf#ET8I$~^$!jg=I+PT9MXlD;F{&YQm>eK*ZV zg`0qoOfdI##K75250~A)@+AsNQ`cEr8OkSiYj4@d+Qo4nK}$)?53gZuS(O^yr%hOY z%4$pWlBq|an$5%DrzgymE_}j1&`5nL5WAB!a0vDFnKhQZ8Ex$g&a9bCKtWSe*s^p0 zGv0y}AyU$?fH~nME(u(DvAm(4y<$X=nmA2YqFk@j&QV=8y7?0oxu+44 z+n5o6Z2fTnB7r(IM9qW4O&=U>_@Jq8`k=MxgZ8EmdYeAD>8M(p4vw{HSFMe^y6FNy zrsQU8)fQF-=ZT`H@FH9|2iOMZzS83jcxR?Qr^$LX{ixIdNbj5I~y%9Xp3FH+*HO>n3OAU!N7&7lZ#B zVmiN_$wxz~RkMe*329&tb!GZu(rl=5^`@e+Kv`BuauNylP`?5XP{L*Rmo_HJOO5?0 z4I`BOqrILeMW(hs$8%1Rxy0HKrmPepSK9y5^noWV<&^1rD}(fj0Z(|&Zi&4zLpYX` zh&ra<98R`R-?=#jUVz5Y{i5vn2{|aY$cCnMwkgYP8Z2EP@hAa0s5#(zAKAxRWy#WU zK`QCIF&M!7&e$gF9_lxduq|f#$#b!+X8ezBV!P`@JDghEux!)tJX4AwD+g!jT%Jv* z%){R?>bSSb8Xq`nsRm$BQY8z<5lv0YU{6~~sb`P^s={|VP_m{VI!%G zgNmC(WgPGO>72ys!% zE@H}9N&2=P3WEWQqaZr%pb5q~j?l43uSAngR zBkfF#-Mo-6!Cy$I;4i=h-@n)BDRLDu$OPdKTY!9<0V=YOJF2M__emrCY5_V>pgj9_ zA0U&OBp8u2;y7V!XDJ`rWJ!F+S}J=OO*0;tQ9LKfxJIgRxVXo&-|nG@6Zo``vYv1# zW(23*w`~axyM?R=j3u(1K7g22To_&y3o79-Ru~=9@mgdg4nK}i&bg}}{ppxFNcWo( zIQ%%0#+v)}a;Suz*GDQP;xUD&Jts(eFU4sTz8UdLoV%_qb?o&`l0Akx{sWR?H`8`8 zVH#=ShP3eX`tD{(QH~*5OTmuw@EWYG3z7~^=>?%c3W(_X!KkFYB2qv6v-|JK8 zad*#3;QjJw%y{=@Z$J;c&yBHn+`ypQn2P~_`~CeRDT!oeO2J~Os!ko1Jx!e|ip-3R zjO#xlUw-!wKmG0>e*ce$Km7J@zy0~QKmEtypUVGw_)mZQ&xil{^WXmR^YQmT|Ly00 z|K+d0{Pye1|M=~XKmUyne?3%1{ZhV^r(*eYC|>oOm-uV1dvxMKuY2@!a)oOxpDX9# z_$OO>w)1n(J@?$Qz3aBXfkTbgMAda zp@u$c380Za*7{i4M`$W~iop6BRMtm*4chCYzQ&5^qrS#E>7%~J>hV$EvM(9AMR;2R z`-Z*e2O#~IwhhErTFBa#w0QZ`fs$A9K2Ddd-((B<=-2I06JfyfeWrgW*EUFu$4zDr@4U<6?irxNobgLyTePRy zbwTE&a;mRJ#T<)MyTbhNtD)6o-%qli$oQ zLfQC_wPCkdRbiAHe2lNO+cIurZ}GJ~LA9VrwaPr}Awhxpb5~yO=kO;2VUdNBl?2kd>Vz9udZIZCmW_Ow6%-?VG{PMPkD!6y0*AK}I(%XaO6V|DPz!%fs z#TRyi&qd;ivu}JI7RhX>@$reCpp&#~d6|11-ZUX{e(~2|PSbn71YFo9wl2GEh4T}> zK7nOde|+*Z$TBURW%zue?M=6oFUNQu-k0Z#vXJ%rUb%ByZ_5hv+K*oU%NV}SBEO+M z)w+ThA8jp)oi38CEDYRH$gut}c*tiHX(EU470_nFPwbU#W%1pzhQWiLtT7>K%AgD1 zkTs5VED&A^aU^8Eu6uta3^4U1S@uEcS$-gx`d2)d0xq72Z9Lnu98N>g_1(b6YsP9M zJM;-l_|3C1X}PAtKE&6Aw=)$V;9ip#b$!=c3CP*w-#dW!eqfGtpgE%Hjoa3co)4kb z8>24Y2&wakWqcU==D{)GSlT7BQOB-5wRJHLLs>OV(>COsUMb$w__NZISOKXUnB2gk zvn@pJ=_I(x?zo$#Ijm$Y1bD{1+96fQ6gRs))x*W}%aLKt)rF@5k>h?RQ6f=C$^ ziQjo3K;`--EC#WDZT83nJ$=JG>n$uc?>sN_d&5GE({1Y+Na5YN>|v6{19fN0%S{$W z^X{DPF?wHV8Mm=LpMWxyjW+v)s|xv%8@LADoxLTUm3}=*eIAJ?BI0|JM|K>`CP~(H z#<6VW`A#a;ygq08QD4m7{VpXo?jG<4SHY^!%Jj(YjNc|v24(oKy zCBB|pUHZ-=G+ht!$jpr5oM%pZZPSTY&4c<3InI(%C5G}TvSbm=cTWM?+twK`6J{SD ze_2*RFGrV?ITmgI*kspdUg1UF>(j09Vsz;hen70^6G-gwVHv+FMAT-{<#LdSp7MSn z2lgJJX5N~5Re%N0o%9FytxyIf4CNYcxmJ~&k&iYe>)h}jy)*)jpNuRiWoOO(>tcfNYA{7VWu8= zjFI0w!2M8Y$N!N=s0$E3={F43sVGP2(pPOU)MHaOqi*GQe4^BPXvVs$3ZxRZihM&; zR$X0nW!Vt2=hm9_8pmQNyK(5dwjp3L)TeqVs&VLsv1nP-W@vOPi&`{8S5d>=b$y{8 zLj%#ZmnxR7yPa7s;25m_i=08li%$a z#@MuN-9Q%I$^e_8YD+*ZWz7X*l`z=A8~A-w^M^rgf^xSGur||$Ivr%|RNkNgKq-x@ z12EH;CF!Yy)}?BPsvqmVXrXo+Zmp`0lH7_R4UD)37|gBg8OAlTR80lVRR{sGM*dt5 zZQm6A*y5`(*6IY^Fre9PBi|o+husSOJ4nh~_);+t@#a>t+qFhnbY<7ITZ#>PGlF4Z0oJ`7^wj#)Fm$Hdi?twHt!c4QS|kXAfQx%* zjAGE-In0`0Z6@MIg zJmv#JhLz)6T0^lW_u8!`Jl&2^rGmb&9jgTeSq=6M4BwCN=g8jbhZ79U;x~2AwAzaK z>Q=}+ve)`b8;nJU2e`HAtFj!cnmr3YGg{p`@>#U*#I_8NX>n_d>?>fPk)6m_P%MX5 zyP$_rYaUv%R_a=%0SMR7)NR8vY6!qs#hGjr-c+;R<=Be}HLal?c<8|J;F%hJ<5p$_ zQDGB!yKNY(-O2#lvagD-4YBobNGBe#;4eVnk(U@b8sntGARrd5BDcIi#S~g{kvJ6UXr7GoA^yP^SjwJ1xzC+TrTvn>GACPmX zpC0Lr5aW?>PSvnow4LXwvK^43z4mB@JV27duqbLIWs3rxtIDR3>}_;weL|0w1u~j{ zGiKhfRF!pEBRw_N`Xtra4eWAuErJr)dZ{WAQD|GcRr()|%m}I;DcIPpD^)31u5U2q zq$rT#$aPeR6vbLqGLQVNGgXOlE2UL=D)NB0Bfr>k12i=CGgFnwfU=WBMS3H-k$DIo zgd8(mtI7r`ssJIkLdp?IKd?|EN&s=NP?gf3P}6R$5r*iFTDA_$fw{9(l_;fpl$eyN zg9bga7p*J>#KNvsC347aMV({Kz${HUB9Q^1q*bLjtH3hDC_pP>!62VksuBZ>A@bCM zX@OLeUB#*)>?P9YsfhO%lthuV}u)s#)O}Vv%qk2uJ<@-$o z@^Yanv06}nENqAR!A8L%lWkRr$yu$rs$>miH`=Y}dGK^n6(xoUBul8uLSyYu9l8?j ziM~b&z~Zp>T2&&`?N-QIp{gJ(wwlPbRF$ZA))Ga*tt?U@RSCPWguGy>D$#O00nX9@ zS`1-~OL(E=h_aQcL^)xXU>e%AFjHj(GpD+!(RE8z$*b5!Y#44ulQC>`_%NX8N^F>g zs%&8mY)4yyS~g5u@daC0Oy0GsMEi)fQ3;sk@B!Wr`cc|BQkATw_;n@TudrJG|_Sz+OSysCVN5_*9C{a(iTy-fY{(R1ac$3O=KOesY}hmU2C9BD8W zu&L3!Yum%Tjuw54#f3&`;3p}(>{5QfN3Zbfs-=|-Ai)YNh)?ADo^j!8eVUv;aA zCoFC(iV@+1C|tPdICZu82Q9V-mIn?e+64Cx&&Mp9tDD|3I7|Y&wZrE_yCcGIG;wke zPjfdNt=QIB(Yh6NN?8eB1l|+3)37YEJy6r!itSM{7vz0|1%f*xe>Q1*9{{e zR|P5v2e1UZAOEAkB3arV5PZ6HQo>kRc4r(DRVa+V{e)kZj8u@;XzD9%5rUc z5ZzErLv&^KdOw;7$|Cqzwg=@lwTvHD3&nuQ$DF{e>v7Upt+nleYj3yWFIE_{s2=WA z3?{Zcn7DBk{t`oArAxd-=#sNN;8)BL++bM>IAn5wGqy*ALyb)-g~|L`UDQU#_GpF^ zuHc}^6S@z*j#|&y9^8t{Ej||`!d`_8wWRIQ^qAEcQyBG_>bT?>+_CLJ1nF?(Rvcnt z8YolDUzBMuQx_EfZm-G*1tU=+^Xbo}fJ zqn2Hu0br8l+k;y%&{+V)5q=Y964w6OO%JxmO1w$J4Jq3r z<)*XAyV&-S%M#ljY{0E<`a=}P_w&#P==IO`NKcr&9#7hxu)bnFa=&5QM3`Lrj`hT#Rq4_`j;Z>NNNV_7G+)4nIcpWysbb#WwEB)KI2qg4xgH~r-Bg&+!QU63B)?sqo zS49-Kl_)!o4S{nm2v*{j%P@I^ERd>hhzH{WN%k;SwtcPLyaNgvr;p5C?mjE z)?so2HHxTnE0I`6Nq`lf8igghCK)E@){;tD=^s>#0>jvQY2RO*Mc4&n33Y)d$zn0q zWSAU&#hZoj4LXP{MsKT?43pRL(iO#4+!@)ATMcI=!{jxZ5_wOQ3bRpxA|ka*{i_6< zywM}KDnf(lgEqv$Rn}(}CYMn_{ODMB3duuwv5flrqBS#Na&AStDez95Sm7&ZkO`A> zE3{{sm=^nmI8C@U5hmBIEwOYq3yPLqi*d8mzif9n1Z#;~EA%M}3UduBlnel@oeYz6 zD+U63TV^m8wgTHL{R^)V^)O;#3}Udi@Bqply4X__VRCN843na2{O%tkT#{)lhs0N;@uLJ zMVso*Am>mkYbk5C)7-#lv(bxB$491;K?^m;nM~r2sBKi;*pb__*`- zOyxaTS}5LNL6mm<5TOT{02>iq&im!@h(6drs5N3CczLpobf>I97E%du2G4@%&X&-M zntm&DK~CPzJUTIEf&_2^K^?Ry5h3NVm9dxcqzo# zFEAA4P&9F4Vc}44?v?B(Ut}&YOeQBDrc%Tma*KswNcOpcP$)k_09aulLs&l2QY7%n zXkhg!48!_Dbj9t63&A4_SHN<@7!bNp%84L`5-|J{F2l4C5=8q-@fs5Kgx_Qt!a5|~ zFn)EXh=pyXd?9|l(0(#&3hV~9xCN_NcQOk6lBp3cWDbTIA*8p__WoWismj%0EWjgo^^ z1!ZN{nV%qwTzBwfi8$4u5$sh!OJE2z349lFgm@u{W41A=AOV1cnkADX7z)w?k)$Y` z=ET-4l(vQb(md!z$p9QUu>k@V*Fwoe#p&4ZyqtU=_B$Spyh*zi!KzJw3`2a0$xUq` z7??gWDNI+yk+7aTfQ*--Zp>d9DvH|WD`L5mi4_aMlDNjGQn=V zRY}^lnKT%dWhu&VUjZ#<1!jzJ5#~g4NjyNTRWcg{G1`w|A(;^R2np1V00h98iNce% zRVt9>7t{^#kpL=Wl@AG)%!VYLC5hr>H@A3p__05rG78!n~VvgRt{HGG~Ro7b`u-`PND2b%m3&GtX^%Ky-lkGfL)KwEy-+^xKw;`WNxB8p0Ps-Ge( zP398Qlr#@HfMEatL?x1HtL=yKGPj}XRm1t$3%5YdgJ>}^bOB}vZKn(Dy%uD~cz(q6 zlSXxFoDU-ZmiH%)MF0ZtUVZngGSmOy*AIXRmS;lbCQn?Vf(3=|Bgdm+UZ86`d2hqy z*`q=%fLWlBkc2Kd9NLy7tX=PrlwFBW#MrX*-Z$ko9`(}91{f*eMU_yfsRD`5MI(El zC!~_ybau;~P=_C0PlgIG1$lF!DVJi0zX_1)ovHWr-gS$MZq$~*`-HB zAGL&7$St5PEXrDLDPcdP<<=fDu~pZz7tl|LBH@Y!KnDS&oHjMkjhj2Y62&|8x0hQL zNMP&q0So|q1<;nJ6)`BMQsV>Q-!fLg9;@yAQiT9@Cf)soXi*A?V-@M(EQPBCET)E1 zz~}*Ov2l>n6ePOu`_)Vd{@f>jTVP8BPF zj4uu^z_mB0g?E>|y#y8dPOX2g^r%~;MoTLsEJEsG!Vlm$p>+jm6x0{@UvM`BOPYOm z8KV+fVAetRsnd~)TvhX=k|u*Mg6F&p7Sydf8R+F-HTF}X7$`|WV8iQGQ^u|UTPK9> z^n*xwgv`W{P(hxgM);vNPS87W>59UfNe|4@J0-WRhR7Vj_{on%GEP70_rWGm2P>NG zQ0&S;B+QbO_YTR;@!lJ~_hlre;U~evvbrD*DOxl1AuwXl%;z5@Cdy-k3)MPJcv0IL zII4%5wnU&dalg-Du^emeBT%IZE$SVN>iEyJP>Vsl7f{#SeW51ish1XBmEz@r zw-=*njdC3My~Nl>%Sm~t&yS(RuYBc)9e#dNz!oX(THz;CV?y~uur$=;N_$K_$3S5Q zLqi<_-<&I0ZM|~#qlN>KrIB?8R)&U(08&p(txTFN2pL%Kw5y?^0oAInQSZmA`SFzu z4MkKbO}NY%{5-+}ovH?CSGft?9Ys$m3h~t0w!v%oO2PuuN-Y{MhJ=D`#MUu=t%hMT zKPlu=_b%$6*ca1}I%R$xN>DLKqabK;F?DLW?*;w`^%*!L(~o+V^%$SjyQoxbQoMw^ zzYZxV!jMp;@Jkgl%)73zM3adK>|pgn#TKKnff@$k$%jS% z*m$b#YA)`MHA4lTqG<%N)H8?* zuQc&@#*pIRh1;3iv+Njg&cORK+B%Q6e(+xT&~WQB2{B7Td~D$T8Tq~&`SzMid+tj~ zV6Rku`xu3~EUClLHL}-r&#?Dd!CLH!#1xqjB6=5xL~-Yuruy>AujvZQRK%*;`@7G@ z7o13~vlXtu0@5AXKrmTG8bgyYQojQ#R%i=ReKEu5845nfmUoAOTe@Y~DJhq|LK6d; zf|0Z(2YQKVWX>7-KDU#w*PQmE&T{7;iP>i?c%p`7oslB6skpKQ?Jy3sbca{SqFhAs z10a4Mr+yRAY)By>hR$=kC>+{F9V$Bckbj>={WuB0yBofP3<01F8k>-+x{8G-S6pcE zB*m2!nFuJmhL-HKbkk8;7;GeLGggRq*JRMh>I|;5)soYRL zy(88;QWZ$*kGZTf0Y^>mLNEV&eF9}VY4WJn;_P^i;iG|NIQ4E`M{o--?j2Fa-dZIv zL|GlUF~^H5+f4(^N6dXER(|K)bq1~4e*(Lup(>~dS_w0GA?N6RNCQxvOb6}{c>mi^ zeiIZtYALa$8ZNc+p9~AG>SyW=7a&HR%5(-X&LBD&;DtYWpX;>4DT^ePn=43+`iBe} zJqi8TKY}#|+(Ny65Vlw5WKaj`x1TZCR|q)R=%~+yofhKVjkyKbF#X32AdLfmHxT@W z*FR?d8#rJOuVwfzJ^gp&zYPP}#p*fKPk|q->g&r^lW}vbXkd-4qek~SZeE{ip%n29 zbRwTlo~5D+b3UV@VlznIf66lmwOeX59bopfXvPUqbRC@i8CpTlt09fq>WmGN2$z~SPNhmBejXvDo_M1WO?d7KAAI@k zV+<)A+*3rYSHs_3V^^=0++xs6N($prn5(lMR}HkLw7ff8p2_>HCJvHH(YI$}9=ed2 z*RhPirFGie1%d|lEVmZkbp58acp?L&sr#4z^V^^Q<4?c+TL1n~oIn5XU;przUw;1e zPk;aO&wu;nw_pFtUw-=gU;f|Ef256aYpb`ji?TOx>-FQ@#iP3(_y(;*{dy-kr9E_W z3)JL|$yTZlX{zg9OZSZ#=t^uUq*uq+(RXuaOm8%5&wXBYPf{o|Z~8>hdy^JPL?i&? z(aWq;&m@ka6@rLEO@R_|gy+7qRX$f>#u|~P`qb)(Q|0VvUIloH)|~V#IP|m{1DuGW zQ=*Y`#B)t3VK*Oq>dm)(DmR%LOtL5K{(|W5Rti3*G3$3rj%06E>)p0Xwm!?e@0NMT z-RMXbYyfh36~pI}!@C=tzIo`$E1CUU!u!Nzhtn$4=W-vs$059y2Pml2=LH_Wug+|V)g%db#8dq&-*7OKegV+CX`}n@3j1IF*HEbej=107IA?C=n z+wlWVz7R3?_L%u%`($^YnJ>>88#*z4aa!b3bUius($4kc)C>AAtAU~_LR5jkK}zRe zatiGph2Yid-gmfDe%X5WeZ6OrU2fccUQ+#Fl27!KBUj^=Jn7VbWT2bMZ@s+U=cQ{~ zbNHH_03RPU0Ql@#HD6h8<|SKNUu1~kMc90PVr*%C@#VJ!2;99V6{Zd)j8892@7x`y z9PSeZyW3Rn0bT5o1AC{vx4PducAvbGQ@5j^up6=d&$+G@E3+@w&8@j=6 znfQiw0`7??C*&TKmxp&n5VA|Yp_PFHk9T*n7S zUfrhNDPE!FW+1nONNzHCohHH@jM%FAUHGB3oK>UgXnQ)?Og~h_#%*J`doLtsS02bc z`HVXQXujuwJsKr&G?Y%Uq^-t*;Au&}K92Yu=qXikd?{tx-d*Jt_Fg{lXKuYuR3ZA0 zIqePMZ_s$x{?htHJFyL%?p+J-eS7)*66n61iYGK_vaS3V3R(#h>8Ba>O{Yy^ zqAOXhMcXcG<-Jz?X4T@QV`arDfzJg$BTB#P1^;0={ZjdMX}d+cTyN(!%zSE^S@C?R@dXpKmE(gd121;?O#&0 zWop{46T&&B{oIZrHuhdPWBKKU{>HTLDPR7}zNg0mcm4Q({O6a;7%rnfkZ)lWkSFfl$k-_=MHB2INNO7~^F zi1N5`w)>^ta7X_h)?nJTQNMwTlbt8?rmK&jJ58(*-Uf0YFoQ$-4 z3bZh1DDgVPnmjWJ+Q4ZNcNmuL<*D7ASb(ry!%h>wN-z=5GQl+8%=~sZG$yv7W63tA zQogBm4ZB;%wCG=CJ<1!KoAH8-7DmI+5z~9OX&Bwj0xY>j2f=BnbZE6h!<3S+oO@1h z*-?irx5GFP#Gn^sS=GZZ(x8HBT3Cef)x43vdG21?6sCq45<_#SBN|${htz0DJl-*Z z<&aEg1u7+US72*(z}1C&rMpVJ;9I&&%6_K1N-6J@?&^o6?h-=3p#yqLt9rmN96^7d zx$lSq8v+GH&8Ra=O3q=S;{^w5bJ_^&+jbSG8y(4m3|KJ{(Lb$N&|;}cs2f>A0aNGI zj4g)ASkTy==O?(|W5>?47i7nfBGA^TlDRHNjy)Ng14yB|sO;eO0Rc4XEWks1(2{v> zUT_1OfG+eHu%oVt>C&P(nl91b)aeoX7cFgZ5P&Z@=c?oEBo3RU`{b}(GPI#B8FR=i zSy8`4r@|yW;Kb<{TUShr5DPu59jeqx6`*8L*BrSJOD>xM`z_6MMgI~Xqmej7;|+F> z>@a=tM5OMGxg;@FNvtAnje zCgY?ybuy&}dW}o?)9imPS13&UX?jH`cd!iH{0-%=)ISB$WYSM$%KU z=sQj-8!%I6R+0{83Y%9s^Kq`(eaAD)6e9Uq;t zTk2surbyMDu<7OEawsxQ@CTFf!(j{zxb|q`yO*nHf(5!x0%l?3R-@4=NrvX-NIPDR zt;5%qYeJh5f@&2fiQxIs0+>!DbkXk;XB2JRhGs~6<^>gWKF6U&Ic_EcEmcWV>C_Rf zD^s5t9a|Y|t3k@S;q|bda%xOHiyy=I>n3DSsU&yA|p$@Ja>ZL~Ea@rO$jffq_;jyFu zjI$=Y>+hCA5OhO}?ObuB8`lhpVmQ?6NOW%&GM0|y;)`)8P|-3hD$dL>8P(xF7Ivft zzpIu)7O^tT!e)~TZbYCi&HKbLeH`9*kv{v^!mQTega{e>9z=x=7&$%CqK=dPILU9C z*J>qf1sD-~R&%&VEEzS_^$^kL;MMbrqih;Z=EN{N|EgJTjgf4nk2uls&`O^S zh!eXam2~BpJFdClU3>}(%SimV#OdchoLo}CUw6IxdkR=)&C)Q#gg)q?^m^nJHo|i~ zUEFn0pQMO+o}SwmYK8oWyoV}W*Mg)+4X^p@%sFV+^;s-9v`l(=63c!vq^+H_JOxLt z^9HsHR9ab@dJWfz4rZq0tc>Vbu;J26=9-AwO$T|Su6cH{^Ro^0M1Oae^*#k-J>r>hUOrH@)hiWIb`e*u4_7QnnB*$B8pIO zI5Z)OU~M6F8jg`|?Eqx6wca2&qSOA|RKUIK4ynTti9@U{)P->95YaNW!Zo4-vNLOJ zMF+C5uPVam$m!;6Y9Z0@1wikmn6}tjG}_ZC6fUwY$xt2O1IS?4wF+UM(8DjYDdO`` z5~rc;CxdsdYfq4P7+ysN7}>}Wu4^)&F7sqq9|$rcNHB3=ES`2gWp_Ur(!r;+ra(q} z70ZGd*u1?IX1K2R1ly#`D$K;GB=MOfiK9C<`e6txzib#$+0NUSWZ*azBZFNdGO$xb zhFqo;q1?~H%>+P|gQ-SsXD%8MwwNqFOfi{3T(r1cEL&6Ybi0J_Nwz5%fO675@rdlA zUBM+z!=4?AR1OnV8S&Se?cyh;Wer~D|&uK z2`Rx(KphJrV|(4p02Zk^L{LQ9%CAj2nTgDEOQ5H(OQKPPk_dca&u^S@>|~N#VIDxo2ckDa zLj<2QhOx3S_sE!&lOS+UTxB3My_`eH+iDJ(ylu((8;ShGBPP*kOHi52>GgOev{=O- z;~;aQ2FqV{t6-thXf}x{ZyQHc8d_N=CL!Q;H^LnpO-!b{kOj4u_tXyeFwo$lVh3TO zS51YRg;`V3b`>E|>#+6?9vqgbpo%XlQ>Ix&Ab`mv_2w1!Pa2N!--qti8Ie{=^35O^McXryZh!DKqlxcY{ z^Mud7c*_E8$3cOIo|tFF_%ZSfpM%Z!jS~v>dxPEaZ~o3z|Hi8ie=ounT!Z}`yO*%t z3m>K%!WX&0Ze)^nUW~Hb^9D_iGeHz1vw~5|XhAWbR!u@HJU8`nphP%j6lF3VU&1)@V07n498^Yq3KJ z_>$%E+`mvYnB}C5j)b9fT@!-908Uqq5Ya)3*JBjV#xv>J{e;*M+F~ao`p5-tCqxIf zNQAI!M2Pl4Dlww~NPDjjK__EVok&8FWbHe$z3nKRz+P($-#U)yBfC-w0AyP-0g$?V z$r0p5WNr~MP9y+$CNecEDqRVH{dCw6-a?Tn1Mmt_NiNnzWw@?60%Xz+oe>{wnW#K` z1BTcT;6mw1YrKLGKzl${96^!j8WDmGqgZeTaN)`U?B|FLAufy$B>`ST2tq&N2tX() zj=<*1TM_Z$no&una5%d~gp5Rw@DlKDUQh)|VhqAC9+RaH(f;^&Z0)an$SdpS+%Mb|5rj9#eM>VVlAUaU&9sz0{I?0TLlJKp?x(Eio>CbU#mxlE+Mu}$ki?( zwr${{ZxuAQO9+t{79uC@YC-Wb#MZ&=g-g0@p&**+iH2j6R3(-U zi3}O4tge`RfP{SW{w%c40ND{SWS|>^|Kc*;nS>&w;6_6YjXq@dtS^yAoTUSBNWhu@ zXU1c-l|@7(^613eiCA;yXhH0WNt_Eih`A39QD!UbKzIcX@q8i0AQTqFk@~4o%;?Fl zz+C{K3N=^hTj<&S zue3)j@^hn-vOjTpQvUZBJ`7AqvomBEzlx)vsu2F)i{1rnOe43cO@N$scmqQi>( zhW6AnWF=wbl6et(2DIOtz?3WDMQ(>J9nyRR;H=2V`HfW7iZV2@+bMW>32z{cglnT{ zuSA3a>km;su8pMjLI;_4NMs{%%4*8#xE!c4LoKl68oK zT;-wy(W&}Op!i~D2K{Lerf5Dg$!U`$SEoQtR-aNE%8<%A*0z?-!~?q|LG~6!v02AV zRstHD61l5yB4aDrPYa_VbKU95X_@9QD=+& zSllR>E$Q_5R|^nptk9Sdr2ST${vZ~O+1vIPiT=q^LiyMvhiEdP57V!mJ|1YMS~^0x&{&TTR!iJs$im* zg-2{fmz}=p+Kq|iTyFeH_j_ZB^Bo&po{Gc*kq$+9`1sl5b2?svO``s#5*Ot5>fL!Y zN3ZHn3X=`nhvtKg3dCOtaNHf!@r+FWC~NA~ip<@|0JhZO>d?&;Id!hdy`tJ#1^Db$;|W4^79M}?;upN*=JfmaOT6E|sYs(gIl@$ulvn<7z34lo<=)i<(2 zhExWtki2)lH;;mhREByDmxD06dvyO(vTDj*N05WDc3|nluLroggp1<6@_t4_0P|U- zcH6Cc_O}iTVK2!-RHO&RD7mTsZdP40M0kpWd(&&Zijf$tmdV23EG}_dmN=*LQp2r1 z=eLo#(O|=)ZCn-gmITr+Jcl2Zz4o3fyEv^x27u=V5}M(*QpyDjc@wI;WATe_6>DaW z>e?qvKTswaZuEmZ7&d!z(fA0o*m#=CX!dy@xUZ7ggfr9L@o30c57VFN36dCf!V@E! zdR@NpiyC?^@yKi46_D4S7$6Hv1h#ZS;kNOd1vz8rRHZjD?5*?lBa80nM{s)y&n2Q z-85udv=^%k<@rHA=hks=`;b7_eEn%L<_|i0nnxJ7sCqS^<6fgGB!s9v{72XW`( zZw-$#mS!pn*;G|9A{H4>hu*Q#I;UgCfd z+MHa-iYkjAXl>k~#p&tC>uSXpUqw%{ugz_5bMx2d>JbJ47ePDqPp5&+=FAE&Xc?Nt z??cY@^>%vBG#zxtH=rB*Txh;m?#fXBeAM}BC71G<2h{T;4 zIw3>pX|nO3Cg*tO)jEkOzJQH|(47(p&au3dg6;Myc098-_}FusiW-gstWQ{}WZk(n zkOZP3N4f`=cBVp!WMwwP_|bruTxkvpt*D0ABBPPVV zMAQqY-h13juo1GxBZl3(7g}>`T#{74YM8!jSb^6EI5m8tJEWSYI`+UCTJg~7-k#hj za&o={QM`Ip?xeBbzfj$9?Az1+j%s6LghyW)eY}r`0WRc6%|*PWgssm!y1Um zjeT*x9%u3lA~4l3(ouJNUUN9LtlNlI@=D+5-iw<-x&aF?p!H?9+HT(u29XUHTzFQv zTHPE$3ikasU30_ZF6>hoXk0_b_qFui00O%@at33~Y17s+pB@L3i%T}y(N8Ta*kW4U1o*N$cQ&rbzS4a;BeD_%*T91OKpEfgEG zI+0&_`_sGCL%%HoJpR~VlW?@HP%F9F)KDtjo7eoQ0yx(!YG}LEpwsokubPzQ*7(NO z#Qz#bSbdAVyz(hai=J4c%6p6%w%}`B+wU~6+g#%?#o#SyqmJolTa&f4<*~5aAJ+G~ z6aTx5f+(H3{+gZ^1wbC0M>=_KWNnUc{L#|%1%aUlqC(* zN0!hGxu52$h+f$FN+-sI_1RffRn*Fv-@p{nP^3Dmv-e*Ac z%&+fN7uQRriR}c=3kDUgVigv0|t|%g^?8vX=9qFXLDf2XfOh#zf*I9mQFvjg}V zd~RKg2uwmE7oJ`(vg-;O9cghTE{Rk|BUla_Hp2OFVO8$XPN1ck=s6 zSYggYH=UPbo#s@mCu-J#jry|(`Oq}Fq9ihH22e4TCVYPA$c<-j1woxz)_tro$u84) zw74H>ghk1#;K!o~Y_}5R1AR3I8Zi0I7x4cM4mT1tUY&vg0lA?80U`YV!C~fqlEced zlMc9&NL{m<7tG8}p#jt9R%1QnQb4#fnMwHKK}0yo_I-i*HYzAxfF=y`fuKS3v0(uV zpGrWL4cd%B54Z2j*~5*;le1?F;F+iEa5utmCJ_H~EueEfp{_N*)sVrF(Q|eGY5V07 zRlxxgfr~_55S0I1MyuzstB@Cz-|^Jx@#T0D@;k{sw~q#!);BnoV`|${&PEGOIW;6& z^P_3c0Pv)}6YJ}%Yh-7pu_ameb>KE?Q=?C|sLy*<8%fPBq7+iRFwrN&UO{;A2(pQt?$bmk zai~YJg4F~6N#e3vCudfzR&tH207_-OyrI{ulnHk15=7(EVO}Z&PUg@Fp>*BxJ4ips z$Vg0R6l7e1Jv5uRWLK~FA|#jUy#?^<(*Rh}yfN}T2{h>K?$|Og^y}$#EkxV#>v^&C zMF^BIc4qAIzq@`Uy^@B#7U0VmeDk3zv^+_(s09@fnvMWk+WHK~?4DN*ywB8hol9HK zMf`g8_wqUw@X27q1DklrPq_1-h@R`? z&%?%}?WXWCc`v^fCdDnIz@||y%(t#T)j{~A1CKu>RxEmImkcI96-$g{C*%5kY@W8A zkW@7{fCCcplJGmafz%iO!ZsSDRM-+a$JQcO_}VxTgm(^hCv{*-segCoLd_$Di`yqL z$V4KFNh=4>@dB@B8k~)G9L-yC)#fIztOWl&qah*Fm*oC*65-wi*#nh=E!e+7StL;5 z>==oE=VDbJ+Q{6p2Lzwfi3w*vQtX<%ek(^OhMki*LQ05d52tM2cZ>AN7o!@UQQ*#S zjfQ?JJ^N1eoFE(L0wq|*TH6>Xo4qPgEdlrW;k4U@=jvA5?;XmRCm=Tvu3S}N!UQq} zrZhJw5c9LpmN2uw!<{?Sh*FoD915}r$1)Ft@@?%fFT*gbm+=~XWPIaxCo272BF9z2 zd1bd=5MbQP`}dRKZ5v+9y`&GiIU#<2sQ8iYY#e>Sph_0b1lpP9>FN)wy+QJ zl3REWNJetRU)Z5o?8@JQ6c>zj(87lMS?>BkRW|R9=j{XhlJ(HA>320uI-?{h8!SDz zZ~tdkRcv?k7csB*IJlHk z9%id4EK+lX8SYKnY+cr`Ww>>`WjaZYBQN7-D~G1U%D`Ox?!l(ne8IQ(SjJIIL_xkT zpXaOeTiUnuTSdb!@AuJuo`ue-@MMnVI`z`*C%Pf3EEgcUfC`V{Uj?CF#p zxbxXO4mZn!GI;)W=jOxygx)gyS9W@Wr$6gg*Vc6(H4+})X0&h#tf8BWw;ys5BYwVr zeHr-`4fx;wOwaCiI_@MC!g;s}C=3r-RkJNq*vja|hA?k?MxrnM87mKH>>oK(xo=oP zDH;ci@@cI?9mX_G<*4&jW<%r6(tY~@EJ;a4_ zWF0*%x`oHU=iO^9ykT%W7!a-`kBFTKZ+lT0%TQ2Q?kU$ZY-%ldL3vD2EP+(mN{^6Z z0{>R|?XX|nwy-l=u)dlkEEb=jZif38Byl-oEZO6({b*eKy0V})!tC2egY;MI%0Z=4 zT-IPEaLJRa;a<}#a+kx^(?E3$o!3eJ^jSj|qL9*FBipqW92pCaFtV*V z=E$0vSCu*sQlA2IwqA-|cLYvM72shcgqk6>IdD!9;-~=66I3+;8LHJQ(Vp)lrG^1! zaLG^Q*VZyMnIo+PaDYjW98#)hO+i|Lt^s(`Xk8x+L?X%^`h))Cn~;Ze50VTPVuPF( z_|Q_2qh0oGZEp>{r#xVtmh6PI9V5!r^GzSIelp!wE>pwZfc02lwQ^R4{<<|Ni$b%j z+$F_+Xt*bYeA36R`WZkw2C zi?7VbE_q?%9EQIPo-#ar-K`sFH)!SuQ|p6GqLo4eoHHELkd@SEsy}1aTTu z6rkO1EB`7bb7|doDDE+zQAlpEbD%bO8QLP%i_EIFuNWhJ+;RDfA$rX}MO*#Oc@;?JTP%xjUbMA$z&y7BKCQ7JU<@20^#O^1bN&pX#^Vx|I@Oxb%UZ$(WhVqJV5jb9E+7KI)xel%n&iWlLSSD zLTXK}5DRK#1&bYtB;{A+oI?(bl<}bl<>V9B%m1gv4ZshhS%Cn@55w)nSGR{clU*DN zzYd4TCjy%caYQdl9=azQ$Gnw?N~Sa8Occ_0VaYW%(w|r3mcjDU2t24l>1^CsL5-HJ zLgfE?ZXZDTr$uRFjkgB~8+rlQ3zAzcD(40YA=niBo)N(dc6VQL&eFQ}NB{8s5HiYq zJ}@F9bElv0o;9_nFY(sc5S_PjjnqCvozOJQI71Gpah`uPm?}AR9>=l)W5R4|koBfg;2%v_byxQiyP=snE@{b-i#9gVtC03=J7g*W5r9-b_fT7lxeaL=hep5n^dZ zF}53s_Q~cpmI;?jC!~xG*A=oU2=$6Vtz{oTXG&d3Xhu>5ha#Cy~seRfn zMQObwxkf+3(#AGhdh&a))r7VCM{SDJGq9jERNq%?azpy~H`73V#?FH>^SR6ZUgXbf zguc~q1YC4veoS-8``XeuiT;7_v<)-pf+$j&HV~wFCgxvt`P6-*)g)+ z_D9!1J zgZJ;zw(dk(p+tC3vbtbUdo00odyYrWP3~9y*_!oBKm5W=jqU)CbBTm@Hvs(M?grZ5 zVW-xIwN`@772lkKgpciw*CuJMsc~FL{!!2yyKwY8WL`@ndUL=<&mOoWG#yn_!B>|b zB-%cTwpxIwLjFpkb=NQY``2@RF#};Kj2#FFi}-Po4R&AX-LpCF&8u8H!C(N(yD!Yd zhGwhlg(zp*}rAR#!o=QYy zWcog-7K3@+v!Lc}#b|kMh=L+@DlFn%pNDf~H>p9P@)2rVJ87}Ow z*dV%go+UjAPC`g9^U8?i}fk8?k@HN?5AQDsB}4oIqPS+r`rVu=L`m6SUreO z0;s6#{+OID5i^cB4kC-v&FM~{WN39ybBST_ z=m=&AjHxYU{BH9KWAJi~sg?H^hU%z|>d;mSs9abc(Vr8MkJB)e@|5~ub7KDp87%_V z++Ec(5GHlAgRz)2vwLv7IqS&AC5*~m{?yei*}=5UVD6isn`TNmPj8lwo;QHYNX*PM z9fBhji}fd>4dTAs*jXl&&R7J|b$EMCq?!7_=etC9sp>5YOi$=WoH7beyem{dz01e5h_WUt-oq*Dhb{4steDiQxR`9($WZnY+5wCPjsI$HwJ>7@><3`tdX0qNmzpu0b@ftNgByBC#F$5y4|2N z=bo;;HGBebyv$_>saw&0The|`oQEGTPi2tcB9Q>DCu_ESizqkyYi2T(r4~y*RcmnH z$m3C`xy4V(yz8N#Tv0)+pMycR+X&r}gBJmao-9LOK>u4nxz!8meT4-AVpaGLSYZ7x zSlCR!YeSB>exrV6nYX~RdAD34M0z2^`=4}NMwmmiKbIh;fYFfBw^Hpo&S0NFn z4aIqKSu-({(cs0stN(I_`<4i*# zVOmnbp{;tbFcberV3YnLBO&gL64$PpL7Y&Qo;a(^dn(smo~Z{bMP_0L{ctN|5v}TJ z+}E)C&^Z3%rEZ#CZk@+aW}3TL%{JcrPR|mjd7i9SzZ6!j`n0$#dB;;(_4?Mq+PX`M zp9+fpUtXB|mluR=>s2`lF~sy`Sr|I3mItqVSTCoc+1_wiqRQu)W9>u*`c2s>SW2dO z7`$Cp05s)Q#M+J@kHUgbj3;0dsx8ZLfE-FyTLg}DEBrcmhN|$kBC8rHK|&9(Bxq#} zjBytK=t4WpKj|i%912+C(G(h}qLt*wK1JTTGBF+H>~$Qt=cQ=Brf0zTtP<9&Q>TgL z{G1rpQGnZJJAg*iN+%8nL=z{gSJ>%GjOZ+qY7(g_bu+s%U1%6gunTdT&+K@%@oUZF z`{jrMQ+oa^z|KHF{4lSXW4|qE&k3LaU zQSeC6&9vOHfJZym8q$Qab7Gm#8E%Bi3}VyGt$aT57^Z2tV4D&m2M z=#(a^p89Y#=rsA6;Yf_#hdlb;xP%nfA4)$tP5+A%9$uzf1=CVe(Za}N;EF=b`=p*m za%0#{<4_+dJ{=slS}6RgtHBdi1qOXbJ3 zoL_0{(${u<=Cm8DNPZpY3*P<`KR3VLZGM;ac-QZTimzKoeKZ;#PZlI;jGhKYYYpvF zgrN4BDJ6#snSQEEddXZ7EZdh?-=JU7v|2kKt0KC&Rm>O&)CSbY<6 zn5APw9@Y#5kvEETC93ZQs3`89 zB~1tIA?qZu5jkShV1p5w#O=Rk5zVuV24jIZ=I;~XM*6HQ7*8Bc!obPqh7Fjhs#0;3 z2)*9l(FgoGZBf$E=G6;|1N#HCCX9eiOo`Rc;8Y#-6&}=fgYOUKpLUFb%jeDS942

#Kc+4iUrH&`J{7NJM2Ah?3(<5g8IKWMx4;x)zKMz~MQP zgDAy`(vY$$VsE973Pn0LldIoJ{oDi)h`}bplHBA4xG8&kvQYlu*^6Yu=V!}V?b|4C z3X&2*r=bq^EhkCkDdqd&Xu;W?uf_HXN?+qgR#5ID^la!2SQ*q0MEwU5x_n>WWH-3u{wi&mMU-6|e7!w#_qRW2LXB`n3+2wvceeKwXoB3vXU~+5 zJ8m@GJDpJqwL$LVS{8uIYYyqG&gCB3#E0P}B?kzYO+t+gum9kfoG&9o<4&R5O{=)5 z(V>Oz?rS0C=;faT)6W5#voVnstodD4f!QY5L& zA$Dy8U6a=PeXR?ZYg>0I`@cro-(uN=VnrPNZvfV;UwAVOiC-R30Jd>huBEoM2h!11 zP2}6Qn-rIdb_d*ug=6?V8zx$%N5X)A9`v3R|+(;E3N1BzljGRD#g;lF&G5z*%VA0>Eq z71rFp-qDK~2uV@=LkaTZBpW3@|3e8v-;@9eJ+X!9d$Km_(yjz3!mpM$W86^dv7+z^ z9%UG3A{-9sGIBOG&)J3@CwcT(k07CUcVrgd?0{b+1hA!AQ@DWm^k@7rsxG8;u_H$> z3Sr)G#rmeWpoXyu$KCK}V`qz9ZkB7jbnACW6hNDD12X-3b!&Xt2Tcv{Ny|7gte1v#kbPOG;)(R7D7x_pS&s33EVb|SLaR6Ya&)r^#f zJJT_wKKS!^G8t{WUI}-4)>hTT5#aW)xpGHfPU;1x3r1+VVyP|WgCi228@l4Z*p_8Y{|RDu8g0nsPD@Au{Leg3ievgO51pNwYdAH#a8or zRWeM-KbX+240tmYcJTz;nzM0Q3>3wLOf{0i&%&|1xz%VZ9fmjSB~;28g@*;_tl{sE zPmTMV?Z4Rtf2qASwGBW2guRusvi(qb)jBAVn+w4qrcl^{`;cx?Z+!(3Z*8839np51Z``;(Bd=Z0)J|S9GCZVZu^c z_y&7y5e4-Y>}LO|AP|P{-K0i6?w-{Cb;fi!ykt$2uFLj^QPe4_8Y|- z+uTL;q9Vm0Iu&057Uvl&PD1g&hI|?fFVczhYZuL1Rd9Wqf6_D9ROK&;ewG`^uVOK> z=5nXoTibT6ZMhvZ%ZN^ueTmNNLv1y`l}kpIDmCGT-7n=l_1CK3A3owkPWU4DDUc*Y z^kkZ%rw~Rs-$XVh=u<~gm?}Ia^G0yH%jR%m=S;M>`>T<3ZJw#DWwFkmob@hem$JD# zSJoPa)eEpgro?)Hoxo*3lyOgBn*s<46uw|!6JIpWkPG#tiN9-_q$h{qs_-KUg7vML zLo_}({@RiXN@-CNt5zS1XNa}C$arTo9eI7Q3`#=ZV?whY_j87MD}I9i@AI4Rl=n;o z0RnoP|5*Th;WdyX1Ht`>L5l%BYY^kc%LhD%hw0x0(v zLLzI?9}7uDsJI@Fa=1qifgw15^-temj6SIH*Of7PmG$nGja$AO)TFC(RIe=G*|Aq) z-qhr&+4g7`G6;pvEZ>G#Ex%IqGcmH*r?+)?bVs zE>{Tb(q<8l1HuuWPpnHcXuRoSD_I$jGZ;97-Rg_G9!ub7=7yP7on?}WTP(|AfBxVm zhh+ECDRwt*pU5G(p-TyqnX)?DoYazGu+TPkg%NJRkf1fxvWyMlI5LM4XA&dFW+{0R z$h7c7${qg^R_?wF0}q>o%*-ZvHOG(SeNWT2lD(?DKyU~@U5Dm87Fwj5eUzaFRsoTE zEbXYM&Yo*#BPRXR@ZsHDhtX>6!PxW5mDymXa@dV^_rV87BKvo~&A^$mZS~Ps^jQhT z{UK#yu3r{xP0ZT(hH6x<+{#1?(Xw><6e_GjB}-R$v~7aNu&upwLSn&I=N>(GS&pZ{ z`az@Xu_-rDkv;I?yy?TxW>Fa6dp%R#lRA7D52YG+;Rta4Fpv!GG~sFgB|l!Z3)C)0 zDDl>&p8i&U;1 zwnHY*I=fJq1NoKp9v8WJN^-L5U*A^EIMrbyX4_sqo{Kx=O|whe;ei;DR%Y!Bis^*( z+^|Ei?xvik4-4=9FH0XLAHUFubGXL}$oL>GJ1VS?X?x}5fvbq5S+3-*#m^T-n3_i3L1>glFT_(txUi1a31NJ%{9AD_u+Z znq_hEso+00W6$~i;r;h!*5#v`DvjQA1mw}{bGSsl)wHhsW^hYsH^nEf+w-;4MZ=1} z1czda#i*8nay9X|#aYe`t`^_zZ$kd4?d(qX%O&K~N1Z4fFT^Vs2I{W{m-2sAhh?nc z4=4LxDA@Kf)eo!C_~nvQ(+YC26iz``M?|{2;S4pE$6BnDh@26vZpEFqGki?jrITKc z)o{EphRY%zr*PnvNaoE9y_-TbL-`tWUf62;fe_@DVxw5ZLazsj6&HoQ8Bh z*N&t82XI96y#Em_nsnrk9e+s&0A8PLX2FZAST;r!IWJ_fQiNsUC#kg}N7C3XLh+lR zTl#G7r`nH5j?MC$j?EOfDObEJTN{a73A9DOEzG&V z(N~xD?HTTxQ?F||!Qz-en#xde5k?udhAb=C!Y39Qj-jZS*{Y#7Uk$>fg z@5OQDHfWj{5&>N4cG2TL=`O;~D%cI(4Nmi>?3M1~aS|{i5oS&QnwKe@rr+jeqJz4s zSF-%UcO*;HBlG0d3HC+^lLiwQPTuP}xd%VD&a&+0QJDN1g!0gGS`wWzcW&M?Q? zKVm`4!41PqJO@PdZo)0j6&|(mLP`?@+H9j8=zMY(>3KU>NatC|_(5ii?PRt+R=i@r zI%DFuRv;s1uRT6RQGAh+&bl7U<4;53X}GNgdJOw3tKNy*cmS&Z9E%_4I^zz2dY6s(M^%VW@Lb;66GK}Cz?RF{FlD&4%~IJ>kpu8PE_)}#$nw+maai6Bq6kh zXGn9o;Vj}2$PMk zm5|qP(3JyR6lhTRur{9tX;`VicZRaiVl|T5$M0Z`OBQ7P&QlbtWWtj0Kc%DOb)ocU zn>AF%up^n{t=~iyQSWS|$O(x5G!!~Np`QyV6s1;d= zq_O?7$`eu}cfrTiw^su>p?C^_sGL1V70{*_2AMvCvQg#)iA-MMWKlYiIyw*QASU$-rV!(YS>PuU=I~W6uNg7kw#_`yYUZTbuRR4P;bX6(r8-ldUEH< zk^X{~wKCLU9qpg}DTt8yQWfBuoP6=Cs!G5!AV3J{c9W4|e}M$0mo z{rUH;-LwP!ud7;*2ovj|tCG@=@gIW^Ff^bG4z$Ekq+DV5D=DDV4ufs*gf^K6M#x9| z-0Czap%<(hxeU!z>wF2I%1KIjI(x-PaW%hm&0c{QYd#`H47c*$x&I<~&Mu~4 zkt0y>2+Gl1k#KnSJ#Q#`8Imw%CW<*iOuFKoPy4$wT|@B;7<_K4;-$f9V*7nSUf}Ng z`&%Ms-3OT4@NHCxM0s6g$~24r*t{NT2k8HhOD+jZLEhde`fpvxke_*nwx7 zBI>NIN;~inXP_tYsvyGllEwns0p&t+=RDdJ^O)^Rj9L@?9~_AT2aHy5AG?Z?BX)XV zg!5ze5N1oo#jA)P4FwkXKeU5XSHj4-%vt&IU=6C5M?3i!(+Z4LiyyFt)66|@WN;fI z1{}(l?Cz!9m>jF_?2tDwRObL^ZbU{u?ye2^zW%&ox_p{=?0(z?{JO9oCcS3EGSV*4 zq#5F^Bmv;_r!6QoyxBZmJ^`NkX5L081vu8}#uKWAHzxsXeD+Tm0B~MO!m^;hm$l_6 z`Ii#U0dR1QdT8qtSi*7`Tbk!`%;`5s5s=Cr|B=H zdh8=;xw%I^9&i*G@1-nu2@-0fi%TeV(wRG3r|b#k^A+2ryjvA5n3(U+T{kbNmgq0~ z7MQ2i`Z&>V_-P7yjXG!>D};1ZgMZ#?KfcnO_B5V1^7oE;m2A```71LkoT$a|DyYLDQ_(v#413vB<=&}DJ%@9TnKGFm;^Ql=#h}D$X?(4 zaz4iv*r%BPg66!A?l_~L7keGNz&YjvBC(zjjF`#hTlY2PCUoVR?X51 zgZnx8dP6hFPEa-YheuC)b#fmy7!R(^ugfUU>+)B<;dDH_@MTx9Xsb5E24}0QmZE?g zLGxI5{_uU;*mD3uU1{5i_tXgQ{_Q@Syu;I8jHMUfwaR=acw3Ve;)y#3CpjQ!czQ;^ z?_l}nBg4+HsOzf~lwJCVi7g=k%c z*qVV$=y#TALr5Bll#vxoccVZnU z6tjnfNWnNwpB{*hm+2xK-LLyw;p*aZ-J3Fve6$ zGxU!DmsGnmna9j3E4<)k>7!qk20Tm?RSY;+1Q6s6#8Qn#YbbBfE~B61!Rk2eBvqg+ zzug!X!(l0>lCOHvqZ<7`&!I2~7s_5d`86{Rq@x(H2neD)*!<$RT>4W*4;TSp&tJ#E zS>s%zXu<|p3xVsLH7wy_4`8Y*iv{SVLjk31YT8yvRRY03Q|#GdSEkW|uaUOrr*cJD z%Us!9N2#YxVy=fPf9q2C$1qLWi3Of-G!Glxd{v9g?^+bj5Y4K3l9XDT@B9@X(tZC* z1_3gt@KskFo3hWe2%nNLGTkx2`>KG>K-wXDDt8>@x(_*g>CD*8%C;nVyajkqN{Kgu29BTno12;gC^AJA@ zLj;v8N2%j!neKc#!9n7uEoz&TwUn{z@}pN11TzZCG@|P88@C1=G5M*Q~S+8+_`J(8#+{@34J5Kjf9f`Mj z9n=;lSF26W=(Uo!#RI1a|Gb9E-^%#uw55;Ca7PFq(j&snH8IwGx`H13tA6iA-Z%UG zvKFCo+&|;^KL_m&VMm5SNl8NkMi$OX*(``bWh@F-}u#}K~VgW zwp3$ua39PCnNZKiXJuqE7c0A(DyeoFDfZr+T2tBiDbJ_+;zrl&--jy3-dCTuPNxWLw9 zgM7PVL%{CRgxFK|YdF^;`6TA}gVp7?i*2;t`WL0xyO8R8NhTe_-co8|yq`M>AOy_&u=MEXDHc>++x zQtK5V=-KVcjPP-E>&5!7{s%fCTw~(D_gmVRY@{>u^+Ze^*UUG>*ojJDJFcd>n&g++ z(BiV3@cU9hX2ZT-uNfYI@UAu&9Cp)wWYB!-sX@wy8Khd7urn9ym{5G;Nsp%`n^3U* zNlmtKYhl!FQ)J$_kn7tyiK>)vHaDKdJxQi#*1lF@pi-dmwR5ENJ8i&O?j(^Q1veFK zTz}1QY5X~y?f>7@nK4drYuxCn(Et~xmNXCBo-_Y6p4|HDI!=d!zAFyw6zagjO<+vAmwVLBbr7hvDj}9X*sbk7v%vSqz zd^N)R9;!MmPoni_sY!>@@n$ZErQ)BvQ1~jBoe+C=xx2sNFd1SkhlNij_5hotvtXq@3Q2SoP}qdT0=(z;esRGRP2@FfXDpLf`E zLz7FoNp}X{OvYJ_6kGz>FB#AM_D_tTs#i)9OY&uMueBqsK7i-Ttmp+et(!w;BWpZE zYIU-~5P$hB)USrkXftsMkI?RE&aVS`acN2(pv z)gk7(poO8_N1>0=4OUSJRH;`^5GI752>*x9C_o795C95=^L3Lo2lyL_Ib3ZAtVL-a z3+M2oevIf`FXjpOhV_{Llw7DBy!#(=E$J#~N^Pj84TVI57YMGBab9M^Z7Zu(kPoFX zDo5k;i(-aYCcbZUl}4t3KrTp+d3=5>c9=K+c(i>NzFr_SK*c!6$?{wS=fSI&DOirw z_#Tw6A=djDzV|@TwWDm$Jv7^X)cz2VkKdDdc23N81KUk>p0=b9G^cXdQxs%V1Aqlr zd8h=sNiI3$1}Q|f$M>7~9FB+vl^p>)mv{c4NB?G5I3dGtb{(r@umE{CMJlH8(uHSA z4W=HDp2Kw@8c4i3(hO?)09Lcn)9BbIMST6n(ilF1$lfrBI8O zAyMl!lJO9o+WO>d4*o8?odx+slJW2nYiVc@L@=~@@IOQETKhV$KS5QB#5CT5I_%XH z{JK3{=KVrPM8n(pI@!*Ca{T^^K5x!ON|sI^KR8~0dA{IFHKxei=#Y7i;p$!zlsA@tiT=EQAUc!5-Kk>RcC$gPwAJfV7MAN9Kc(Sg`-!X|<09v& zoghFS{L0FobPi);%TxY}y)&sx?nXF)^&zxOLw*I{=F|2M-y%|0hfc3ncWVkDDn8kC z^%z2{Piu*K{~xyADM*xH(Y9^dwr$(1ZQHhOd$rBgwr$(CZCkg`elKpsK5>8Qvnnd1 zq9SwV%rV^Vgz-6i<^>460(yqaF4{_;8jixCv|CiKI8WU}rF{BwZp;<^5YT%5t?;nC zxt~6b%iIQw%38_sQ*$dWJn8a3T8OZF`n|w3H<3;Dd1s!1XWto?<^J1Xu7bo;712D| zTcVd_Ac&-wv_Gk{gpP&lQ4iFbcHx&^yI^0|2n07*O6S+^xJ9W}?a`e(sApn30^D`M zWwM%dtFia;xzcF6Zd86dx%D7<^ONm2bH6#By2z#T1H381H-DIdXCDF3Z2>>}8wxnn zp^@x54ofSX2`g75`Eqw0HMO#1XM3*0zA%xe>*$IXc4po5fO7&U#_b*!6vriopF(hS z3Vq$4%$(S|c!iGDra-QCrjwI;?Rk3i_GabTL1_+7rAVwQ+o9Zw-27wS5@%-}6Hy~# zexf42VoV6Wfoq(%BZGjrj%tCz<7{unjUzKiDI!VPKuEE0r{r)eqJf-B&$iRurHXDm zWB`kE*?SOnPvw5`EJT;%;MWtp#H^BV)Uu|)j>yKURsfCZk{R>?Upy~nU6@@bXKv5< zSiw?2{KIvEcjj-$wd>_9(9`!xQC19WApwpyY{oezefF#zQ*QZyj#;yc;qn=5sKB^( z9~m=-w~5Ollc6%pNLS(h>HY^ScbJtIM~M zub|LkHkr4axzL|Ibd+2h4Bo+F;jEWoH}_?>a#mdbOopq#OLZ}xIXzWYw)q~kMwVA+ zf`mRO$Vx9y4-UDL7`7r4#N<~PB_npISc6=!9k z4-KFymK}Lx)j%_oN)P_aF1w~P>~!>4hw1&Li`?|t@8dT{Wl z&Oz9kIamq)E{@Z70~Dt}H3?dIx0$T&m9=4_wSkCbdM;0-O!)^ z2H@8c`Q%r*WkPRtA`uLnfQhP$o~~xjI_P zY1Ei8;;moth|cIUrIlY8F=E2lnK>9zzK6quFHR0yL&0MosDr@~m{iEh9-7h~y<_Qw zGNUW};}PgE@NO+LjE)U{94EbZ%t5r~8$18Iss{+tDq=JC*U z&oJNV2`TT)uIzRhP8yNkH>subP3t%dXQ7R;!xrHxI@#}DS$Lv8yZr>hnDjcHbA*pQ z4q+S+f=!pmEA1&D{I)1G*TAeo@kmu4^bvxD_3j9kI3#abB1*}G!?V6B$Z2y=|cYPZ}1Jx=C49TT}qRgx3Vm+w&`vgH1Ic{Y@upDp`b4Y`bu_+y_~=z z+?2j-U~uQOsY-=0Z{&;u;ALAW;^IZm$pkKdw@Q^;b@g#Ug48w4nr}+L@|>NLieOca zoNeTA1`m1rqxkw~gv}cSkadoq

*BkfMXbqLur->%*I{H5Ri5ZCNC)+PXGmXSSrv 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

!5A9Gs?ab?p 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

U)*l~#Kl!& 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%0AxvhbwBSCsXW1; zHGumDWkjZ*y7d`De0MhUN2*D$j{JdW_MlB=Zk6?eXz5(#A9;oVB&_agv^>za-M(55 zXYp?T`ffFP*hke+yyEwRQL*CSJ3`1|k`k3JWF$a)i4XJoyRDr=PYt==#7jX>VJi0P zOtCZCjOOGrSzGe7$wx4sv(C1Pw++TE5v5AB65-#cO&RFHKV}Oqwq^2YxuzzaAE9`Lx76xkKp@&x{Qyg(r|%#DfmWJjkv-?7zSA zIynW&rEOOZ#W1R8h0e|}ye46IAy>zXZ9ngvq)qNR71as*_FG?-1K9OK|fw zcZR6#qrl9c=cL1wL7@YC5$AnhknZpW)p&Qs-n9jsF!&z`uPTf&*Ifg&OIE1QBoxzG zVVsm`5dH+}>-C&nzamd7ljw4+y@sK2s=Nf+=TSY#JVmYMr1E(p0#5_c2Zm?-jKU~V z6Jg6DI?v~9;j98QuNgSM>z6@UJu)q)|L^yQpK6YRzz<$?{e#!^eE1y z`*Nn>`~866`+j%w_iMc8G-8_xWpKUN!?F7Vl)7@D z*vs9}E5*o|I_Xbs_nRT%(X|Zj1_|LGX0qi)7e(3$roB`Z^1Gy7LdVM>;)U2a2;SD!g+b=wu6e1Hs%!GCGF*Sm+1)Br7BJ8BwH< zY+u@RFWR%??3SbLJHS+f4E_z7z2t8^t?kp{3HB*|%|hS9t=P)7I**FCm5c4%Uoxnb zMm&j>`TF=a?`0()vBQ^(p49z=TGnmj_ATHYsl0s5cd*z-p0Xh;2>gKr0_)U7^uc=U zojhFqRyRyr$Ub$4_BiB*rJ-oa45r=f@PzD~qZjHc{+x}E*L8`fPmv8mk1?Sg@Lt5) zU>oq8lGg1WRj0~9Lciy5LQKUW>u}Rf zd^?r?E;68f>D*D-+O&~-7I9bh zGrha{Vh6NDYk#$D^*`;}ya4z&(hS$D9|9A|8{RyG|MxL{&CPkvEr3@p2|u*C?FM9? zFkoT8;0?PX5Cm}G&H@g(Wz+0GC_@Ro1y6!Y!y;oMhjIH6fr3GOnqqcqT=pM3Z7jH_sYp|ai_yg- zj=*fyPtRSUyj6}nuI_r#c;@+;;Bs}u-2~lpD^A&)lofs^qV{~=-YiP1c|SiYe7`xj zofA*LQ{05jUm>N-|Jerh%WU|Bz!lNXNH-I=*z{M8Ztgx?&<~z{zk}l~BTbGwUmq zPgsMOu4G4cAys1Lu?sEl^yuE_3k96%#E)fKEfYTUYKbQ?)Ip_$C@y=u1;0lhHM_HH z;r-=fYC+)JeGglQnN#MwG<6S@Ta}{wsKMyc_Z#>>oQ+eh#1Ch)B?j--_8-os_P?AB z7viwdDs&zTDxJB^xqwxM#h{Jj;AE8D5WzGPa>=B7b6@!6wYX^O0Y;=4!D^%CB_Z9i zt_i87HoL+GIyAmm%WoHx?@CE&n#p9Wv7rcYvOtl+9(Iu8r?WW!o8@u{) zDCXRPJ@7PH25+7nFm!-~q`XUf>)s*u{F^Pn*{9$dJk0uY%Z$N8*i}Mm?^tzpc&g}6F(rZ>USCj@Uk8NlwHLHu{ zAIH#G%#aRn2Y;`)kKkN4$|0r? zm4S#Yl};v++;KW5iU3nF*T9%0O^;X8$R8l;(ZGZA5h3Ic&@@V4{!k8>9cDI0Jjxzo zS;~)_?0j7a2ewTJJ|1(5VuA#B&EkJl2ePjE4=9QwV|r?JEj|MBE431kX$^1QG5#$S zx7DL4yUE^leH`;7YVM;7qRiz=;k)8w%S7+hPivcXK89Qvm<+dI-@IZ>zcl4DMF>w% zqSL>X*t+SKa9raR78LT*F~0?%Byc6e9=O@0WJIXYpD~d{%*%mhk0|B#-9^xbi_YC8 zo<_sEX`y~zprCp@Hrl>iG`_oN2VNe{9fG<(j zjyy3wAakPAzk9nS6RIQE-<#dH#F6ra4IK|!ji?Gq1Hso;I?wGvdXRAuu2*74!Z!bK z97kL(=DnMxF(M)3?VJ@JaENVsn^K%*ogd-}j;_xSdq7@3_zbRX(Fu*VN5|%ZqH9>D z04Cu_S{e%Hvl-6to*_6}_%e&JFn-Uf9vIlQ6PBz+?=dl!+^C*&K3ac@;;-iL?kV>i z#$yO=K0am9&Vx5oGmJc!MP{HrjHO^s4~Kp|$V(%*+kiKq#g)&hQ+d<%^x(AP;^t5= zZ~x^uE(!MqFOo7W1~sXL#92T>(G~q;5Rcn659$jYc}nqP9U6}ouMg&Z?3DsyHl1Ta zhuL*&_7iNzrk2J|b@Cl1Z2X)S=5*vMr}c-ny}1-`Ip<80#LIfkxIM_g;gL|`#aiF* z7FI0Cbuco>65X)m>2mj0Y(Xzqq?)5+%zk&%Mf-dn!!UKi9T+WZ4`w-u z!z5kx)>(6DxDe;C)SX4sRy)%Uv+>nkEetUR45m4AgB{mOp8VmrsB|H)T; zeg0^--*}`;zFBUSzK{NwQ?Q&_GhQZx>ob{kv29Gm+BzBL9#~34%y2u`cd4W4ex&5K z^+ohSW!u0^oXO^aIU`qeY{N0?TUc6(3~>W+6x)^4R7TcL!ZQm1fPvdC3CsJ6MHk4G zQQhPY43L5m2C?K-GVV;(tv%gFpdsJrIvwfeGB2B3a`U?hL3m<$ zfak8JS~16LzkA39941*<`0vh`spMJ2-R6LW86vx}prZ3Qdruw-=egS8fBh9LbW^{RPl{ur ziiYO;TT9WUkdcT2T&Kr)4pbK}Zw7;0fdk#}%J4(w82(T>;4;+d4rIiyIBl1kq_-kr zCNbpU@4^%$3qhC&Z6;@@z5kJN0DQDYJX}6PKf3g)d#X5>u8B$qYrb$;6o_ZE6C!X3 zTEHRh)>xW3km{+b|B4J^9?@frfiz=#?hO;+Uu}v3*UM8QVBuAVp(KPD1BWpHWR0ex z>#imAS1fID%_|hK2O(ID2bt6cvz{?ETsTb01HAk83e%DkSPkm-MQy;KxALc9MgBl0 zAvExdQU3!rYAU&ZGmrl=q@dL>PRyAyFN#lo#j{@_QM=;576*`Q0scso+mRCokZE@Y zs0yh8oQhU?sVvcxiWHJe-~OgLbWLJO%OSTojE||F3;WQd_Ih6e3ybLzWsRqsMq1+H zJCn8wRv@-K;1_M|+)ed%ps46f_d2_5VjyG&CWBAge!taIG)Md<@}b&MfIK;j=LN`j z1CY^3A~(A|g3*GOZ6d_<34Hqc1f5d$>?m!ncXcPdF%iW!>49*k%J0xZ-7I+Sx;HQu z2t9bp%DX+&MJW>!3ewP_taa zrv$-hxyUKz(PIcT4C26Ho$xT5Qb91GgFc8P3Q;ZR&iWBY3Xm3qYUz@aTH2?!ZQ4<= z!Qu{j!S7B@&g^@rr$m)&p8AukFdla=qeHFgg1s<&2J9Gl#wHv*`QLq!A)amq^K@E6 z>Jklk*gB$fx26F}ySDN~s0!0m;qn)yJaLW^y^!gCI$_yJ`_%pqYeDUVxFr3hBuQcB zSUv$k2+_g92N)M*&xt2lQgf>e7iHlHes&Xa<23` zRF#;B1lpOk`$F1zyGX=&7~1qd|CgAjrL4Mn{Z3Jyj%gfo{JzW8dAD;CKQ4Ad({<8J zIuos(^=blg50nii6cjDL0Aa7B53@xenk`~?vDeaM*mJW;?i$E20N&hHUk%i3dYmv@ z+m#m%C+GtA-mS?zN-SJ3>NZMA-R&C>yP*o_j`tivx#e6_Q?~My15O||<0zT=cON%= zU{RVC-OROze&RQ(rnAtoxBm7BzcEs?G*$^8L&zZKN-GS#0k5v7L~Vyen4@RZYr~B_ zQlofYri#vX8y37cptKaInfr6_;2_RPGl{G~a{`jRd?|wXSihiN3kM_$D1X>3lDbwd zuxBep{X|Xmc2Ls#ET1(*T%nMgqzO7Zg*R{5KDU<_GJO8MC_OL8zNcwD4ZlTBvEwqT&eue2*3#55d_EE!u#@+y5c! z9)d&(ws29mZQHhO+qP}nwr$(qZQHi(-Nx?Q=bSgW_f0CI1~tiwip-*Uk-6u^0 zZyNb)1!Ci$;?>^%)($VeCU>}^N;(W^sYXtTtavT4!BR6TsWj2kNDf2khpb)^3)0&O z=Rq%rtAiMO!!6W23nqooUnoaWy9@}Km8wfe)~Q=t2!$xM23**tp=QS z?6T`X1c^@>VAf6r4V)nXCnfYJnR)3a)PxsaiUj2Rw}QbLv{tBM+`td~b7IAm^P zE~aV2Pv|a~X-;GG6}G692(p6xlOBjCPgsC?c-3Jb2ZejALbPXcIN_ff>;Xm^>>1rH zRD;HCvdy9PKm-UhW4om(5|G%H2Q}0N;-V=;_a1UYH{q2hlydO^Fazq=;U4JpRNCb9 zfIe0py>K2$xk%ck5a-C4$@2~Ke2PJFog^C{Oz4Y_M507mRMb1qf*xae$r#FN#zLh0 z+xOjNG&SqKn6r@#tu5siwqz|=^K)&IrR?BDeSa)!Um>mjRQbps}7IP ztkGZLv@hevq-zu3Gq~gdRJj!L8sgf=mW@{1>lgF(`c;W+w~8G*xs1fI0&T;5-f9vD z>DucM2R7TpF4AYS3W=;^oq)t0FE$`>oPsNi=JF+Vc$IRkZ|3E2M&|A^thezxh~Y>8 zX`Y4YfuibBF;Z2Dt~Z9^B$G2o-teL)MsGjll&-UlI0;tJ7(GgoU@r?Xw^88(V78yI z&@+)|87lHJNxJt)Sn?m`B`!iGzEx&ygNg(eEBV5jN-Alcm&i^6G1_R^7R4Y?NTwnh zC8li^PQz4xS}jRLJJ^(}+O1B$iqLDw{dP!Ig7_8Z(5#k7N;hLokA47N{56XooB+tl zLZMPRpVRoZ(8D0!9++T@U!-2ZEFVq8s>4`lawUmQhcMs>YCTtwqp>t1?q4oZ%x8ye zxL&lV><9pOU>?*fvkv2~W~g5TkvQN&mN3kpg3{7k4^Uz!Cn&e?ZH1M}E0i^qM?jmh ztDVzsX+-t7Kr^L0TeCEF?3;J17{OsG{n-0wzw!-EuWbY6Lrh!=^`7fV&S-_;KPn^J z^yHaL?vXmuEi45}%*6qi-CnX>;Tg_dS^J+k8xuwMX3R|eome`;W;nG*&Hz@K8EzOkBg75}5G}~%<#=fT{ zPrD;Th_;a6o4annw`gWR9Q@&aUaH^wM!gyXdfGZcdPxUspx^%RgDn082HydrME|3MW%RWW!7swNN4jS!w{=EWjXGMFz znG}L=v?O5mQI{H&5_TWPi!_lqjp*IWb+?)w^GCrxi7*qkj=IbL^(bkwvz@Bla8w2gABcJ1B zrXiHUzZHdzm)D!am1a?U)Zi2)pEJsTQVtU}Pv*ihCy1wNH;Am^5yg3%A-uy_95aqtG?-+sSxdD9h%4>MtC)%{8FbNX_V}HHg*- zrPF@l0U{m0R-BED-XX>W?w5Y2Tc|z0Jb6yNtVK_^1!-k~u5w=YSY#3+zJbVcT~pT* z)&vp#64^C+@$IMk6)lTx`!C{trO!lvCCQ(kHl@OG(X(4hjH@NXF-H(n&d8$tv8L*>|>0^r)`+zvP40m_X+; z8ov2Mo!QPT>9eb8e^Z!E%^(vo%R6S8ry?cim;0p|DbV-YffY$Lkd-LBB{>Q~iPCA5 z@z(lpluo~~wxv2*Q{LNnwIWq>#-+-1h10Ja>qYzVw{t)fl%u%dJ z&rDoIaI2pbAClw}BkV~^Osf0|7=V4SmRfz3?dLxV34oOLN1YHXn zU#4$IBJj$59~LgltCG#dtkBG4ckBtK;T_VnA-~mW%jsy_D&|udnwoyZDNv)?$*%!-AvQcdFOWZA$PaA>jVEZFLCA%3>I@{6M;1Hs8MN$p$Ra{t+kWdI0Z+3-Fodxs6B88=Ih4U(u=pjLkO=so%}36zqI=1` z13m)F=2*Qm1R8k3!7bq%=~K8mI%!3gGI=rrRPd*8 z4I2%&35_JCvqT^mSA;e-_QV~*fk=jNxviF^lNi<18iKhQP>x)2pl|0vt|2JWxl8_V z=+1WtKLZ`Y9VFU0k11)(5FV;u14qBvp&L0fbhngD<8L}!zJKYq_gFQ04FU=tgFrM1 zt(Y<|rNGLreuKaP2^R5Xx7Wz@OdZ1HCe+=B`k8H7U<2FW5A#+4W>(3D8ce z%W2I442HE8pZ|b84!MAht`t#z%L*6sE;s=#B znyVftCz>ViZ;g!6#6?&c3X*sL(YH?iC}kk0>br?3T=men%c^!a@(z>5^!iR_kg*fr zwfHpg@7TL7?Vm^u6i`(gT7&Ope2nb$*b=)A(sv~O^q3~^T1UOsK;<_t?*c-zL+U@G z`jS8QmtLG?l9{Y(&^KF56qaJCQ32 ze&GLaj7AaBAq)5y_EG%*!9L9Y!7yr*WUWIO5W?>6>c<;(t7T+ny2eCbR~Lhtc){JR zK^dd*p+E1qm8erZ5=tfK4`fd>Wy}(;n@#fZMC|C>+D>F~PLe$+@J6aHw2#Wm*w?D6 zZGzFYGxB2PB<^9(?+{$lf!1#Lo`5W1wK~HSU~Zmq^0)-~Pct0PPsN|^vXZ&RG!}ED z|8n}bcBEDOhSOG>m!3&k$GU6pNo`f6=$kj~Wp&reD8oH36NK1U@XHR+i#xm|nCqVo zdP9x<)nR+b29eX~ENkyy0gy;K4uypypSEMxmdFF49Nhy_1vjiQ`CZOW-q1y{l9B1d zTKnA&@?Vw$R5-^M{Q(DVN*Mmt$?#?VaxWvG|5#gQi(C&zpXnuUtdyZ7Z4tf}is4HVaM-6H|Ld!8{*=hJc$#U^nPvMXjnBBqUYIygzMTn6~|T2;{f=HRD)U>V~J}6F+rkq)C+q$ zn2La}e3q0JaUkY|>3nWUDOQ`RQ*At!xa>Ds!N$3AH6iO`b#8aeX622ri$=S@oW1#x z8JCvasyb*2293FY7NNy#kg6jhA?g^ZANahW%P9xDSRe@06+u)VD0E%tgKyBGgi`Te zB*#z~Y8}MR!<;#9kwcV2xBGRTK;`AbTSM=-#cTu`B!A+z?d41mn?WIpT9)5w#?hI+ zEx4`w=o_K~EfAfwV%yqlL_h<_+Kc}U<;paGr$~8939a?%F&{eUO`zMG@MsO{n zNxpNSOOFt|!F^nMUB&Qz-_e{E?wp%IA2b}W<8P>byU_5vgV^$FDgrJ`ao_*_^>~{v z(T@oO0K7N;zw{34|L7gbc$z84(${Bdr;XY*%?M8osFE&rUONyz!W zySof=vk`+lOT|RZ*IlF3;dIw#9x!)yc6M&J>8}%Y|DSCA@9+AbgQqS1e>ds;{~kYo zc)pF__vC&~+<%^*`uzS~<@@vfe4O?D`}!?gn8NpaUz+;)I&tU!N!9=7R5ke+`r}QT z#$SG@yVq?)@4Z)>i`{d#s-H_O@~`u3?*p%+EU1f&;bGX4G8 zw9v*+;!;KX6nkNTK=e#KYLPn zke>GA`XN3?5B!7u9N)Cx-qfPM(%te>N=Kpn*}7EU^IcUf^`~ahG*wsaqG^(@+C|ee zUA3#GX)0-Hur3gZR%oeD!*)Ce*anv zSzb5qD%~fKsDYeOf+B0Y9$RNT_q1nre7+SNY90O54@CopHz`z z=vP(sHPMppqEXr0HPltWchk+jw``7c4=em$r)nN9XWTXC7W)0|Fc108p2XBvH@|a4 zvcZgdV)Z}Iy!~z6LHzaxqx#(<>l|+lnIPy_tkVx_V|j1@T{5Imk6~ga?8%4(cj{Y zTZ#IeBVn1>Klap+?q`;qnZhz)Y)wPAe^MLli9vFoG?0RDd*^lleO_5+Z*APp#H%-Q zg45^4UNz%|;h6a4dEU~rvWU?EL(LyJenvUIkwsWXL-THzg zIeDDpvpp7Ki{d=w*kBd!dmG>~Y@i)TJNCT(abnbrU61=6Ij|p{cdJ&JuUOGOTl>sW zgK7{0k9p^2Od(*FRR@?F%R`4!w zoAX-l^P=fX;%hqvEMvVafeJ&}{OkjZI!!>)5I- zUCU7deZoT7z4w)G{e#;-YBhPY6<>xp=$`MhV^)YI2H=y5&8K&kz&^g^BqTI#uyb+k z^w)aZTp8hjy_zgH!RX&|Lx!gF_A-iEdsrmh4t4NAT!e`c7KV?|=souI8XV86LU>vr zX>^{n_VmT}JC5gWpTyL>xV~+Xx!L~Ybu6;Xp=lQ_fKJM7-8)l6%{O^dkJZc|iJiWV z*8F3igKN(5T#8KjJ*(#Db;8c!`+etG9Y21^%=7m#FS)q(OzzGVRDJCSjmE?BdB;Ei zSuA0f6_bkd+Q;k69^RWq){2=Cbv64shP~MP-!tuWfejJ?4xcx6~f`cKrd<8fUki zvBXZn_p%FP^~Aj98e?Twqfo;bL#^I7fAZ+*^wo$95O6|Ti4Bgl`j~D-wTXpXb)QV* z(IV&i>)7w{%j^j^)J5l>)}H6xMBA-la-P(lr!Pe8&@dVc@e-zr$S63TlF0l$J=}SF z=GXCce{i!7TkS3wjc2!2eF5dr^bB`tZWGtvwkNr5^gf$zs>C!ASc}-;##{q$H3#Hz zKJ|pS<7$&fTb0dey;R#o)$!MYcO`uNxd7PhEcE^aX%d;Ku$zv=zzV3#zHg$>L}U~W zOXgft*$T(OagCY1CeTkP)@~N!i#e%P7t*iS>Hi5EC|b&(z#aGKGj5NT)79*Sv)Q0va(hVI zH)KC#PpxllTT41g^Se9@EuH=WbY2EsP}9GHU$It;f>r^@GEhY6#xOT9oePGDy+t6 zth<6v^%3lVCc&mI5jL}6zi(r?32(#eBu^98f68s($!fa}!2Q%!ZA^DHHkP$qW#^49 zwA!qzhVBuV(#?dqNv=r;QoO!j)LZxms{3D+=V>VGKPt~Arb4Ld-l~<>tBk>zb&RZZ z8pcpjSvNse4=v_v(`oR45$=Cg9Of zVrebg2%u3MBQ^3yKj3tL#|qs4M~X?4f=5!!hw}wV$kkflf$*^#k15!)wCpKDP*f)2rtQGAmXdxw zt*L`bjc+iv!(g%5BLa`DW&u#O^b4m909&Lw$+K!Qk9(drqBG`K=5hIzd89aTWoVjp znAKHhj4Z^w%F0&yxyq0+I@fW=(8I>u`$wMC2sX?e#7= z>MM2AC=Gbge5Rrw=DKbaImV->t$S}s8PRJ=MHk}B6Mb)Np~ZV!{XC5!A=Y{2Ei>Rx ze`OwIA)M_&|BRc*jGIr#Ev~$k&8-?uNEyAk-e8{1E|L6;^m~?0yioSBmAB1xENbiv z^tNA_$M7_x(b)QuDusgzqBg2Z=p`LVuZgj+1drc^^b>pwS_>@MM!@ynB@B)qMs^~t zUoG^}&>yokeAcX)N+F=){GJ*zMQ{bKLflpJr3+0AK@eY$=QCK<}jA zVx#N|tuvMLZ{)>agFA!P;-yaH{snlJxwAxC9O~eAQ?`pNH<_G^1q?#l6^Q{u$tL=V z{pxqwEnVtplp*17d;|v+Uq%#P_#)%FoY$QfqiG$?cmgaQOnQkhi2F}W-pPU}l`&!l z&I|~hmxt8a*i7-UAeY9Yh@40n9b1!6(~9Re(ThyO{k%;2c6EE8hR}VCwJWYucNOxl zYB8}b2S;wNn{Zw?ATS&>mTdZYglYn~hLC6ELbqn$y+@PF96v7nD{|c`uNWtoa5Q^> z|JgheN{WWdeZE^4s9FGxU1MySVxZiyw7)jbayH6;ZJyX)n+MhJC4kF4Us{O<1@Z{* ztY+xFUW~RKj~Q*OBD;4)AW8d{-LCOY(+)!>*~j4h<9-cU-)Lxgof~Q8KB|@d3B!O5 zfx=tm>n$o#)lg4Qto*k~Ox^a^=7C8c;_ZkFehF^qWVN~Vx`Lg(y~-OZPRN8*cxeJx z7ak~c{k3_3Mux0Msq=b8K-XY^$aur5HN232Z63{En90~Enx$Ya9X=ApMD zbB7?1XTW`cDVam`vV{Jk76uE|mH@c@2%E$29eszEsb2q4JQ2k3{m)10#0D!(X? zw>o?bDA$r@!|6SAHqtm0lYnwgUX{ALhH8NAJulilv;;;16moQKEbiQg7*Ibq$?^0m zj-Mf1sHm3mGT@WRe@!0Y(|&rX)0=g<0eu0J{(yCIL`M;C*uIATTFN1+ZuK2XeSDSSOsCC_!ikXg1at;!so)Hy(fr#RSr3D*x)BANmsP<;h)S?G`}|DK)++ z_8zkTcsxOxhV;inIkouUt%Es2{hA5aJxeYKq=6rhMbVf9DVwLjJCASg0zzt~$oGEK zp@R^zl-weM{yt76lgG*E!zooHSb?h=g*pzjUPg9-1roWnf zl&WErNnzym6vHyoNa$*HT*7}ip347mJdQ94DBBQ!?M!ZrJX*gWr9+EA^J{y>XxGZY zih*$y-AEjW2{rs;h_Qj-^+GgL4X zK>G6o5xe7y8d-x?C}WjiExgDOgY)Nsc0tn~!io(E&(L;(4wNXVYXAf2in^))NAZ+( zbQt|oJOO+Lgbn(&99YB7pz+m}?}!yFI{x$Q-HoK;zycw-d9=NtBnZn8<-p{r8koIf zNF)hlBkt{GBDMya27UXI=uu%bf|U|t}{!7?^Z`t=^$M{%DoRrD*%^95C{ywo%LN+e*zKTqfcl$MjpeN zMZl!Y4pCU`0ZK0NIWW8j+hu4ye%k_f^-3z)PT*w03-=Wh0#~U zJhQfS0)SpPYbcFEAlCu$q7q0Bt$1);wn-z0C#YE$iPK2Ou>jDq>aCR1MdWDocLE?} zzdd*VFU0edzwrz41c1OIwn5G59mUfDXa|U027|Dq-U`o>MntF+ij06lX5oEFCla@L zPhc^RLf3F&ZR*Xjq<#-99~>i?9*n@sYdEkB?>Ckqa3Cpw{W?4pZHE>S)TO2sWkgPb zt%j|DdxZ|@1b`&aJiLWgUVsR!4J0LEG-PTYNyUL65kH2qkV?=|LQ2EDj7m!}#egmo z_%NvCWgt;90FYMrkDygn^bAK9<7dm8Mr{rhNa?~nB1S9PabiG_Ak)C|!f^oUVE%%A zHk??HBpQ|MmK_7YkUVi<(cN1?AcJV_1c1=@XyQl3HTkx-JG?3PwGY8$Ol$rZVFPhq zU%g@;k^QVmUcPE;6F9^)JSZJVVHchIN98qUa!y;`j1Hcy(>~Ogv_JTVG^Z@=+2{;DBK%cV6Am^FMGSbXHtkV_@GvtWSyJjx2m^y9T#VX5n$BLU|EM1iq zY{vk!>|r`%y|lrmVO*@EA@GfODJCU9cAgb-Z$ohytfJTl#6lXt_4S``Qdj3ZuzzNj z5u6`qQ5M1RW{6|pd(Y;m%?k&QbT~<8$a^pnMLJJpamwbXl2~T}rBWIT|6|e8GfSGw zDWePlmEhwMrbuS$VVbs-L{n7Op@@+H5|lx^yJRYFRhCkDFrSCZcx2P2bM8`0kR3Kt zOl*XRj3U(hy&v@(g(4Y`VXow$zw)q5r3RH4C}EaGH5efr6dTo2AnK3eq*Vu`KG1PD zvShwT!o-CNqs=vxTSCcJ*RcteZIHwahH=g+i|K*E07XW0z=?-(){@m8;W2mE3)8C@}MIT2n!iOFuIqGRu+w4f-uVJf$b+A8RP%_H<{@0g{o#?b^mZ5bF89ganB z3y1FSSl7ipWs!A^J24vw1SuOp+zY$Cw|%Mu0r9L^Y({}8frt1CutHk&e9uWLE-VcZ|6&I~PbY8xuXK62{b$>K@r#Bl`1|p-9IK=Mvgu2?Z62 z0Z0d73&8$?l|`UR%;k2_v*%kH&wSJt8vB~*Na(@IJwVrmhkGb%$QWHVVS+3{NJBcJ z9lFFY0JRbfA@hK)@^N&w=v^G2J!!6SjLkU9hzL(B4Y4rEw=^ij4?rVBYJ$JOrqDM* za^`LnET9Y&Q_l?fn?{}KvUd)tj2ScMa|{#&AU&9(!M#*4l=Y=Db;gb3+!%)*9Jlcu z3zplX;bS)mK(Qmw8GZro@X{rY{WSIz`DQmCR-+_6D|{SY-NtOkhwo;R7Q$Pm>HD}cF(@2I!oS7Z9<;y{zSG{aVp zCCO0>HYtpc>WgS2LOW40aHAT>nOD-3w~|CIoJ$HIfP$)q@gC7djtJ_yu}JwI54xW6 zBU0D>tp0?TM7h`Rfy31K?;}K~O+sh#ECj`eRh8s4lTi9R}mH)GC>00tB-K zn05OHVkot0m_w;_rP<`6au!Q7^r*2@hlIBnAt9uI0F|H|EQ5u=aWQMq@*iw_=^!`P0W1&YdG1Yb~ z4iV0wI7EsfE@ZHN%D8-l29D1)pQSgaViZnsUJQ+;qay<@8@!?IW7GnlkUoMO8ET6Z zBud5w3tK@2K&0YmnFQcC`Im5SiS<0wc`&M3fIBUx$Pm?cAge$IIs*09Vv?i_rzsXH zxOxpfO&(&Z)NmQgjeIg-naTp1hzZ3OVs4cVx+EZOJ~N0LEl^RAA<~7K(gQ zp|Bv+E|66z)CJk^b_57E`O0t4F{T4xY6WOorh)Au=XswGV(11cuo;tvFAODKOr`B3 zLO`zu_Gg5s)GU@h1c%C_K- z5GcxFeWS11S=46pLuW-L145Fri4cTIAlOREVtHGI@3iDJE3|Qx0sNqt_gZbFbSkedqC|7S3&AS&5WFdBpZ0CilHFD zfRlL8PQ)0#tH^~z;W6<_BC3+Iy`nZiU}})iv9Q~CQYR$Cz;S((4hBm4yN&#lNoVNF z;cO;avFKOPa*^;wpGZMZOKeOquu1CCO?{8T4K%5V!8M>Nl`_#|hgEz)m4i9Z%mOa! zCQcI^>71~rDh=RLC`hPNx1^{b1PW!x%@seAze;uO1&e|(Y$Sjmklb8*9uTiRRLKz? zRLvaT(m11NN5B>QqK2%lmf?bfB74u;Pj9#s3@}Q8J|2|{*Y<7%XNExWMGwSg^Z=a> z@w?@QZp}+88~`=8q9P>#n@LoluA&edjzYOHm7H^FT&3NKhZ4p1QZB8^AugQ%0rT=+_Y z+nwqP4Is4m*ph$mWA_=q2LA!5C1?2Hq6%qTKQ|p11|V1;N6#B<(ENHKtiaZ3q@ld0 zs{UZ9!Qs1!xcF_c*^#*4oCSrd>6(wE%PBKm9A?1yv?SQQh6 z#z?;|ky9!D{6_HcB5rEc%M(9U!k*9LPGnT?w>Z_yIrk05LU|3Dx>zy&4{L$+;$ohV zlXu$LT5k)6d9{e!cvhhwx<%bV_60bETqxc>bV?}8cSI*vUfH?Ozb3O&=StMSii^HF zRw)or2{gBrG6oj@9Z?P+5a7|juVw(m$O#q23@i+hpk_Jf^Q3KcFrJ3va~zW$+` z9}nMB({JMJ`yC=%5Wh3upQ-5Gxga(^$!-P9*8(bBTlMrvV~YR9#>f9RQ>6AaJvlcO z%smi@a~=a?Tx{f5!J+q}H9qDgKufAPDW;H#oI zzp__uJbct<++1}V6fOwJX|%eP1tBA4R|OeE%qTD@a}m-kfhzoeZ!Q!HUOX~*=9+p53nsQJ0t=w+-R#Zc`7te&k`uFvN5?&}BgMg@FjSMJkudC)nu` zYY(8b(Mb2u-eTpth=gcHFm2vnw*7FTSRR8Rh5)^WizZ5$X4YnS5-zn&H<4pdCa%lV~jzia4gEPN(a7Gy! zOB{=h#{As0;L|vY#V2oe75VoU=;(5OrIVY+_aeM~$Rj35hAx7Xx}F1by-E8eENLCD zS7|T?4qX-k|LsWkCh#a_9F-Q8v4;%3>U6YI*%uC}s?JV~>ikAl^ZE}lrhEKaL1u7} z)+|uSU#S=8k3d7pyyivN6uOWlQYx+}QfITv#2#I#=r5j(SFTI>(0?iiqM$l7tpPgG zR|3h1$fH?B&SXrDlZOcd1y3nY9;)+!OCa1@8_Y}aR3{0uF6PQ~@FxDI7P_E_cI_Sy zBnK1kJu$qT2n&3nGo&;4Zm^^#IM{_jIBC5 z51ofx-1QSt2nXNO5GM$@sXRkSRicux=O9Sz)P#x4+9!Mv48miI!mZPvTc-;SS0jAH zW%7z7;(ugioG&&;L?sv=LZHJ)+FP2FIzhwhc7kv5I$ueWHt3p3thC9upl~dQsUh0y zN)*3npC;%uX~*kBaj595j~7giIXnV%AJd{PP5K8xiX$;t*;FFEG-dm#SUHe?4(MUA zjP(a0)OGnBRc&6A>;7UwmYndSNuXhce?j4`oTNj5oGgCU&up#KDF4#@%`0NqSSy+6;05(bYg8Je7X>u=Zn%;k2Sn;fDzo$@x&F z9S|wn_=g!q9U0gsRiRiac!yex+?dgrLGGP?^s$wERRn|Pt-Y||P5^Q^AQPWQOZu*} z$OJhSbe~JHf@94>`-wum7~XEH@Yw^vT+gX{3R~m@!loGO#rt<>yZm_wQ8_p zb&*!)B<#av)SgO&(X)+{3$s{o6;#PNa;`Ec$zg}Xyoz_I_3PFt(_zg9vcOGHJR0>aSCG+cM7#SFsTqU~^E>*5jBN8CF_E3qqx(L=* z2^JpIC2giT0yl;8>jh=P?BFN|u8ly{y#U__l*x!uNSX!q$p(9M5VTZ}1xgENplAlo zs*~Rx5%39a@J~NH75oGkbvQynhzhu}C8pV^V@0Z1tH1`}2q30SI>uWfzt+azpXwoF`y@ z`lDe@4sQH!gtEItKceQ;c>dlud`cfKy@SLu^EbKH>_&ORY@PH>Rm}wpS5#f)Bu8p) zWTtYdR-+3KFm7^;rU)VxRN1M~#R68kkw>(QSFHtjdWu&kd12MPADt_K3fTLDp*M9U zs@V#U5Brx}@4#ypW*#_AoW1b1Alj-Z$)ieEc=VwAXsM#xemh%?&LJ51s)HZ?N%#04 z0%<5A($v{}z^FlnBS3q#%-o!Pr^q~$R7&QEO?HL1%HKeDv?wZ5NL{G8Yr z$$5KMKERh4eH)Kd?>D1)`GkPxO%3Z$6#PGE(;xw_L7DPc0nFEH1`rRD_6$xkbgr2$ z00JL{IMw)_-m@@1TmSq+ujrEMeQxY&V)T<6GhpKE608XD9gDh>0y?BdvhSZ5rgH&p z)D)n|3nZ`tE>Y&sU^XtoyQjCIMFvjZpuhI?|Gnk+{hY`@*UOJgJ$?SXFWLWmh3Ee~ zefs|#&G~;1(ffZu`@hfh{k;Ete{o;pYQ$a?L;i%L6RJV zJ?MpjEC1UWFnJ9I&{we-%#E)_giy=$aZj=t*Ey|toFc69xNi{QG!Q?4i?d#!JfvE| z8^J0{kZ~b71jg$zW?Nn=&fwS;2j?zDes=MJl_MZT?==WmOkc*2-Jpy|cjG8&kCU-?IWUa}=g?q082^Kx*=+o+M%AV+B@E62 zDPqEjJX7ic7ztLMry~~lo)4Dgs}x2*qu!8QioQa>{zsN zQjXSnYq1m6fyU>u?5?37*4}fLQuk&UK2)n0Scr$`p+Du58X#cnU~*84gz(TmIMkS2 z!4?($AO1&Lz^AAYVl|)x(a!y-cA*jie6*My<|+o}?98IzJ;LzO!J00|gDmJ-FiLnG z5~5FKdDUw+I2Z*JEG@76;Gr9 z#I&v9Ezn-2RWW>@X>r+cTioPL#Jk4c9;XH#i9NcP`HWnd$OL@sWGPyVXXhLlcI1GF zFyDfPNmZzf3wP~Mnab9)&|61|l!IlbQm*jG>j>*eBC|_)H3J%94DvKz0%*Gleip~6 zX`T#933eyVt|dAQ=ecb#&3?UgVr2w+_0K%wSb?>A&Vt_pV4335=hib1@h5kbHczlV zGH$%^)UG>ZWz-le zsVHMI#{(_bt$5quSoCIZ6iB`VRCg9GSEa9NMG;MfdKaC(P+5sGJ8e3t8$@1|)zrOP z#sMtfE1gDo=}3elkf z_yUUCOd7p`THx^ZkQI?Q-M5fMOhr_5Tl5t~LxKbQHIR?&)^7(N3K}6-hdAg?l8zP| zmxjyAWMR!-Xc#G|%QiYt^=k;oVK;ysrY@jCSY3RNWbwa`TYFzub3bBuuq%st{~q-9 z=+t$WJ%0{QR&C~*TnY}+sRnwx6`J(T<9ok$rue`9PNB;8?*0>bXUa+?_q91N8~bb0 zTt>SPt8sOKs*?Ul{xJ@#8W=Kn`Q)w_uH82@yHWaU{&>mQHc7`1`4)_sl#6-)f96j& z&7XRr; zdH)WHSlP_oApR<0pZV(3;dURlmhU@$=2PrSCH!kdiM$zZ(|IWWF2o*NtdXH%Z?=q_ z?L!Eg&_&FsG+}~geAS+o0UM{_Ov^nY{9RAuav5(WLpeRXge^ZtO$_&{Q&Vb*WL9-d z$?UY5DYl+H?1frwTuuIJ!qT*h$(YG!Ppn_@XQ^P6K0}PvW57v6TMoCIvqscKgWdeH zjXnIi3AC5i8|@*I%FIN<(aU8;X}D(pn95H2V|~2$Kje=qrQl6zMnGinOgQrQ@o~At z-;*~I+WD&;orN!h2+KIRnn&?7u*poki|< zOJZ1-y^Sd?GnGSevPm(BvklGgc!b#^`Rz7Hna0f7A}92^*(GN%T4Q8|9u%sis%6Ee z$Cp_{vAcxso7CoH?P)N_TV}(^MN$6cLjQ7-fZYakT6IOkYU0L6qnbLg7A_7;Zr?8cFTlNO4vzESRQhjQDXNg6rSm@F~M zh#cLqi;Q&{5`r~R(=ifQ*O38CtTiZCcdJT8R2w)zUXQ=e_ZBXjF|;=3`1Qwi%baOj zRju)e-L<1b^z>L5rzk=8)-B?!(<5}-en?#wpV8x?oAhETDT0=eVJHL6I};zK%D7vB z%ehqj-X}BeWR1g;ai9wY4ft-@rRK8xzvNF@&kO8Y-*}7t%5EjIgVUxx8y2>rOIu$^ z^gmPW-E~U4FMt~gP}%p8-hB$=IQXGATkh$GBTrriPCBS*(R>SH1V=g2QQWO&2v)W=h|Xt znEJMTFOEV?JU5b7w@u>HnM4IlLhYv2?*Zq0loI)4Y=-_MVd6oWpheD0G@Wr(8J#?n z3pkrn|4=X&)~sobYIKN6dLq*`B->;a5nQ)MyE{2i&kh#bpyob2hG`9xFA&CW<|Xt)o@3Nx zVJ&BA%wuh^ChB?#4%LyBZ6EKr2^i|*7PAxIVY_WF3i;BF`6Rv_d7Qm1(YB+V-mHq< zaYhCf8deyMsIAK=hiDt%Yz3O+4mQ;EWTCdPkT+`T9qIcJ0s0X43Xb+%Hfo&fsg2B; z0Ymw>(|S^N^c1zS@dZ}#t40py!L|yax`KXqiP2qe?zg+oyotJ9Y9c>WF@c=YHqT#6-;etcr|`sHj-ER%K;AnLMx|9#O>bt;E&iOymW~sWVRE(pQfK zsnJjQo2oL0lGUjajhCZ+i8ta=W6vAv&MCh|xTTq77olGbTR_%)pHG=^-|0AO{20+b zEr_uwJ7pTu=TidIxy+{WAY~*wep-UM@UyNVV5)AAy_n49Wk_8Wkz^9syE2LIFQ$p! za=NBkm2GDF)5yqV%k~y*Ldp^&v~8_m!ekGO&8QXh4b!nOT*?po!!|H!{UG|#48t+; zjbbeY_9-$L?U^O*?6rza{3wh2c!ij$R@LPY9 z_?!8*Vr5R#K-OZ?axhxCx!e26FmO~@KKzY5W4DbdL6{wU-GI%s_6zEs+kqPg2w>?o zA$VXg6QUtOuhsZJ+)s-nkD<`h6a+JA)zsNI{XB;UykQQ1q3i0SY;r6lg~%nlUOic- zTxtRl&YVBGbL=Vv*+Gim5^T`gCmKPNEo4{@fOgu+A$Tc*M zwT%C7W}Ps-xTv1ja;89?Ml_`5ThJu?AcLzchJK69NCig1jCC9$#=ubW+GX}pL<4oe zWe+!!jBT9K-+swp!e;#JcT#`_&=&iGy`|2Lwj<%VsKkLdYSMSRuP7;@cDLbgqXBAJ zi2wz8#$xv_KK#TnfxR4EAm*Cw(8crcq4?GI>QdvmZE{rN^CeYy`BshYNJa$we9~Sz z1^X6WgDxk^ivSW+Mg#Wx9I8J&B>?g)b1t-sZwA@y+f@TMV2^h;*>2-YKCClK9p6*Q6c`MKL@zrJ}?dVIX9*3DmHhu7fmi{CF!GFJoJ01tsz z;v%`HUq?mpbcp8NTED;+4is%@^uuYgSIc{>)kow{#A}!-IITaKgB8e#HLnH|N`cFV zksNTn@W5g=MvlE0DAL7Ahj>-5A*tJt5|T0!r`k+xOVYJrysIt-q>RD&iMpwgJM9{2 zNXBx1;I~LRz=R-}-P2BTC$0P&!mAXdf(xhZUvy=Lyd62JPaIjO&c`-H_pPZ)#+XAHPF&Ua`bZh))+c6-#r z_sCK`?x%^n|70sNj%5!N7X3Uy@d8)9eaTc<g0&3qTNCpRun8Gvee7od)fs$Q z>d#@Tk;=64Mnvpc)W3a%etZMhq))a^Va_Z?MZ zxqV8cn4=2_gH~xI zJTRbV!Y$By7xrj4(BPffCg+cY#DeoW;pp@jYuv^C6)zoX$-STqs6z@YXU8nVw{JGj z2yR2)Yuf2wZ3zVJ)Se`u1a@pkWHa|)hvfF;_9E%lBk+R|D%`OjOT0k$7QU!>^F&W>jJO0V?H`maI&w&L^!Ez{qA;@v%b?0{M5|Jy7Y2^DO(0M&6of4q z*pJQG*6Ih^31RgaGB!F;BgaTcfX`b4f3FK9T_vRsn5JB&!^mMneG8i264(L-JNdRe z4O!Yop~4Sn<&FchbDAEZ=>$c)jYh7Auk{W*hJy8^>_>d%^*sj&Fx13bABM2P5|qF$G38 z??%VRN@E(+0$1JSH%2u;Bqm3-q94*@o@6s_MzQ`5z`CGu6!2-Yrc(!3myRmism4qQ zXrwlQ=F(+Gzlyq)aFBk-R019{*}aK^Lgg6=#^)3&8PLGr^apFKsa3f+zJf8mN^8_h zz-J!jEE9)bYcoCn*<&CV8eOSeex`ir;sooyF3~UleNK#f>k&q2bE+eF$oC!^6d5Kt zjlVXsehB#0ZG3y%`!)yl7zK?U%3&lfFBcoS$3SXi8aI@mXdd) zi2vIMc^v)2enw;v=b77-Ab7uC!h;)*n3#ZZP5#Eg(~Yb!>P=CyOAuNfh8a$x>ZnrW zV$$GMadwI6_v$oVka6s}9%?%ntnPfh`2fbfo6C)qqZ(s+(dd2KGhA4QnG^LdC?$Bn zy3q*ZQ492^66~Btot)F?K&LCJH_ik4#KKNJe;_McImewgei?INFEA|CXW5_&S1o-| zH0cEJPejk)Bo>A&)UJHPYb!Qbt=d^Y2MZ+^p?6~xlZ0j(+gOE`knImg<|Us_L}iLP zr=H0r>XYr~KJgQ)Pk@lPIvz)1Y)Q4tR3e}y@#km`gQ!l|=vgZB=pS^4V;&JnpS{G6 zIRsjWcdqNua#c)G&DE>8amYcQ{)mF|6_mc*4*%|M8un}+#gopDD$v)RAj)eIaNY{$Zr=b6H(kw&6emXgOP1w zVoq*pskB;po$>xHE7|j`!v4`~)U%d&;`dGsQtNqbr7Y$%%bpR+usc$8A8~dv^!}Iv zp+t+7vNq~k<0np+7G?l1PT<%}lOpcYDP=O*95!&jQwlY)ntg$xw9l7?cBwLHV4L6PFi5}GO%S1y|{ zT>=HB0OFTW;M^qW@-ItYk?N&UP1Fo}uyB2J+3h$EXRCju-y8gnhhwUsk_buKhtO{E z;m~3n3%OjT!L~^cPB}S_M9uF521NI5_i`-gErh+*k|w47z}XnUu8$=%1kRnRm;(rW z*=LCf;^rek<`nj1(73EY1wb?9^?yVH=GFY2pr-NYnUU-f_hg3qbWD9Yb#EtO-Y0zi z;o8Gy+HHfVpHQA<+n8N8oE~Wow(O|ykTQySjA;ADkwRUJj;}SxLW-cVm(l^XejaU2 zGK)cy=4^#n{BnRM9`gevJqzRXGIbXQ?qs0&N*uoX%RWvePl5_=Z?FBD3VkA~iY$7Q zeNA#wpGqO{8b5CmNqO~M6B((PMAc4ifgNH^W%l(_c`X1Qhk)1kV=B$3iYMVldT>({ zzDa^vHd$^*n>K;yw$eyGf8N?d{yYX2f1bg?p`SkgEmUE)fRIpRE&8fKu8m0I7?6ZM zJu+B1Ejd45&cw#}M-wyx3Oj{Zx9@NeNT%%U>Cy5Uv4U#%VudT-Nuk0NXpcc+oebS` zse&+TkBpjF;>I@qkRAZ?CW9CQdW{k~RaTOwA->UxM{h@jntvyD$|`cz;L&A8hf-NH zrPGiut2h;DxTH+S%mxQc&ye{D@prCXzO`wHc#ztTg-^Jwk1m>qFhsw%k}JLf$c+k* z;-ODHI{Qk4CHWSkVm8pBHY*`2Ng&i(s7Jx7vlNlNq^RrI^f9AgbCPlvJlH?!)!RW2L1`N<$pdM)F|l!Wz)JjU+kYcnl>R zBv|l^GY>qaG}~)PHX6=U)GCjyRM}}b;H5vmw-eKu1yAc)zC{h=DC7xTkyRcR1}7UK z?lle-76F(j_tAQ^x?Ag&;;T}aJm*_yPS{L9Ppbzkfe9zWkQO(Kq)X2SY!~dMn~ky3 z#~1qHw40A33+?U0<@vqnH4?0if%95ZVTIrKMOUKk8E-s(uS_haAJ%FQbK85}zi7pw zm8kFF5D>LYi&*0qQ~OSa4sQETSM*K#@oY?>{t3f@!CIY1pOCRRlj~2~8(j4*DHL5D zSg*Ia#+Ul;-?wr_Da5b=WaC)(*3d}(RiAcO$U5)a zJ#aN&L8t?COsZhwfr-+%QxWU>%aP%fkg!gYN2in#o@HR4UQ}N6WNsAiIGV8qz z!rwu0W#<^@nyF=0_GHqqEQq{_eWg|eHZzO~nR)7z4vPW1@hTUGjw;t<=Ot_3khT@; z+`L8Wull1o%)f@4lQpI!4jgl!+*})7Amo_q&S|rMYG)qKKC= zwmdPqO6y`zl8z(+fe2jW(+q~GsKLlmvmE2F(O&pm7}zcv)vvy`R%%Wkf5|c{67PKS zMZ`sVZ%lF+&)`d{FHL8}M}Odnht=N4(VW52!-pfXOS2E42&o)XE%YrNmeN*E<5;Lr zB+YFvG0t2z5JN=*;yXT19V!xZir&h8O-9|r@bHDne6EN$A-xmoVhpZrj1aM;q-jy` zB3I6~5V=~o>-=}+&op03&!86*r&nbLl6DY=;O@IIsq(a0-CUDx#aj*>1)Xx2%AHSx zg;myF!$vl6!Ztgg% z#(PqG&E3)NT*+DQ5ZTCVbiEp|;^Z+u;2K;0P^mUxLj)(BLDmoXT>vK24tO;_b&V!FmKQA0T-5Euk01U7D>8K=`S7{X z*cZz1GTsCZ+Dd8annFV5Ak-e4h>zIOMW9&4pk^qm8ZQoHXxi4@ZtOg9p4>jHC9@9< zEuNXLO3wHf+Z{P6wdJ@i50u!Y!bW3DaV0f@JH+NKz7lo19s-G>HE>=SOQjim+5V}k z=+s!(cw(TQqy^5woWk!&6pEjZ`U*7H;Ua@_cY;1q^YUBHcvVV2u3rZ#0{Ph*!pZpb zLmy#42POjj*+6-)6s@2;WiCPip+F}xe6@xq>g;QO4~i~@lj8cGJBF4^Fo2tFw*hUr ztJaw5wD)s5yEMZ}u$H5UA3i|ngz2o;5nUT1wpT22cXd|Kd-xI81^q2{PTqy$#90b6 ztkpGe8-N{+VkmKr%VD{04wjQw3V%E>FpYY^I~4efiQNR4HW;0pm=KSLc$gKvK1@ae zGJYVGqweK;+bZm9o9!g9a+XVuwfFxUI4}t;jnBUusmId{f)TKV) zPpy|(uoU$`>i?wAl%@qga+^96v!-G3VZt0**u&UtgJOkkW0`0GNI@@7TMX+m$B>4Y zF1$wCqJxztR^&v=bGXAiq~tq=v6ymweAk|_0LgZub};ksFjSePyR%ctn^$*!>2ufB zrN?$p*EToZWtPZM)SO_z`c9-sw~vf26NzB8T^P%s?jg9JtLyk{9V2WT)P zfqNq&HB8>5{=$^I`JtlQnE4^C-BV)iNfjEV60@gqcq>5`eaNlEjktGKO{~z9Hub$S z1&B_jI4!%{>5~GEzcB>-93a_zfk;B|ZgrEgFgAzYL-2)H_;#QH4xV6uCv4g2E|PUK z$x<@v5a9Eui-*f(VV9>dU5%i^ja007|h|1*Z!{uRR;Hdt)%-Y06ebx|GA@dFoz z_$6{(fUNT;K?$5-FjTiB{C~Sm$*Tx6lqFZ=*GUt1pVE&f9fD1#W4VwcR!k#SN^B3` z?ho!g+}||1m-xo;XnhiUWM5hq>^QgNa;mSZliBuDj)t6KGPul}dR>)c_!N06L=p{V zl(ZEdxtBs%Y}j+1W!isPZUa_XD$qnGV(R|Ei~o~+j>Wiw0;rmLq1UpnIxg!6{5TG6 z=HR7fzH_~N&>lA2%{FfV*%fc~O%}NsMde;ZlG5^eg=Me<)y<{p(tKH#8${(nd1|$` zR^MQ3Ix(G0gZp0>sMV*IKXdm$ zRxM>jk#NDRF@9JcIMt%nN6))cpE_m`JVYLW5TSrdgjzr?N-e4pS-*NPKhloesDk|) zO=8wpg;9fh)5pVhd#ex5zbB^~L8ZK{in?C_o%73#@a$9^6em6cSG9%LG-X=#jZomS z)->cyNWne8t3j4%q15`eFCj*lmK}$I&xaTVSR|bDH!6Gi7&If__=4sa_u^z3YqN#d zxHyIWRrn_hXI0V~NPq%y6;^@j^vnW4@MB0SI1->-P1HI!IsOvv(OH!a*C6+yxJc!6 z89^vt@=6X}3Y$|nXy5zamyCyvinHVUJrD0!iLi!{jhHE=I&k(x`qUT?P(OUG)&#YzzDz(#GF(fs(SuuSG z_3+6&0vsRp*t!k3x{a6^RiYT4rjg&6mr}#KSHrFl9|1^1Z)+iEMC=M=ch6rYWYs5L zq~SrLEDI@%__cS4g62V&r9)JA$62dR>t-`zf9>XDX8x+MTGX$0L+n?Z+-WxSdcfC; zW$LjEW=s=zeU1R8o%@A%?Rpu}DeD|4b3LGPHM_X1{!&x5J4#Lr*|QgoU03F_hq{@$ zLuWslsc@fO)s_$buB-;E=ITq*et8I7tC}`Zjl`JQ9!@Wjhvf4X0nGd>(&x8=knEY` z{mNJhSVM*k){g;I(EnS?F~<11507Ct2uw<4PK$QM;-Uzx0Hds)aY%(39K+vagZ$QP zUs<%lLcmem7bO>)Xv%DO!I~@cu6wNM0OR46n}6VCjSlo3{XnQ&Sk&>8GIg(>hQ7w` zgPfb4)@1}puN~A_O@J|E1hP4ym%&e`H%VdQN zQ2+qxiT_E5vHyz>(~P0wawyulP&>6B_9Q<1x;Jy$@JYj(h5c|ZxH8AfwU3@d1OEH+ zP^Bk1E*-CERHUrZA)B-%)}%5CJ*G!*FciK!Vf**5YtP%q&fkH(m!8j&pJh!)G2hOw zC-=VCe>d>{e(d>re_!PE;Q4-h)$Dvfl=OT)PR!7KKcw(^eN**(9?7e0Nxdm%hab)4 zxh=8L-5J+(IPZ(rbUdk@K&@!`Ol6Ag_YBTAWcDm?uB3B$Q6$m@?PK`U1>GC_cx#aB zuFn@xbx+p(hZ5HFpMF?2!s~w`Vm{_lG1ag2G(C;R&g{w|_wsnrM|uC>@i0nl)ivXx z6Hz6M`}N^Nrl*Ezl$Hxj{OqEjIB|v22o^#5MQRZ}~ z3<*E&Jm^ZEho0+QOPOUu99z8_X76aoxaU>)JLtk6Kr93&Z!UNBA3$s-pvd~@z}iO2 z`IbR9Hf#OaDk}F7ONB(i_qbmD3@{|gZcTUF14gEMdy3t`=_BE$=?Ge~SbNU()#9{0 z-y$RbI;6NvtOl>Mo7&znhSDpOYY3*L+0*QXL4N{c98SZnJrm6NFSD+mFWMF8&Qea> z0yxb|;Vcb_<1M69BRsvt{?R=z;?`K;`{Vi;Fjc3+XG~2;ixgYibyd@%IfJufh&H9< zFre=EJ#!LE;z!cz0gBaQa16gq_R?G}bhR+;>-w>LPZMM+ ztK5L5jopQ5_IN_aU7D&Pm=)$E(hjg7n9{IxP!>eiOwKd8<4zX0@|_`<(`jAv<$HTo zhQ8UxonEzcFZ1!&a?1idwSH#^P2<|tiqFyKm@U|+l}_``E)iK>9ai}%4$7hFJR79d zWm9Yxw8=v$=t>m#Z-N48v8(ceAPZ7USMF}74Nc* zPN-~uf)RgY#x0@N?V#j2Dcwdq#iUS;<$ip&(uzAlIEXDoot0@rLPPy>w+uNzuPem( z{wx&4nDbbRHXv51M&`Jtr)L=I(+fBiNxGn#U4l|zCOt4H_SUq)_wc6%2=K`|<&KEn zcv-;jEEC=0dE|?pXGVMYYSRELVjLbYZG5kYv>-WUMPQ@-N}kOb!AE`H8hAJmH=&7r zsA}~m^)=zUUDf{masX>*qx*|FPo#vz>jZE$-=1%;lxG&$iZ5IG@Nv$Vjoi|(Xem6T z6U*?$@#<8*?EG zfEi&?PFXcrQ_Gc?t{4^IC9>S&h1m>I5~QC*!=Fz&tx~pwQHauMhAY|Y?dTzSmFdOi zSY`w@hX$t2-_x5nma+o51rJvvwV@;EoNimq@I#K}0D?{gl3k}w-o8;vH^{|nS=FBt zB@bk#o71&Ksn)`iXOmxxwtZ+Uff;T!KHgA5Si=-A#SbSmH1GRUAw+Oq-0p21ikON5 zBZIElW~zsPvI0@tPEz-n;kI@75q$<}m%a znIcbB7~LmB?5vr*7QnXi7Ao*>?B`|_tF<@V zxO`s)d;u)0i5ot;<=R)?M8veJftG5ITI{4>{&%909~A~!+c!kjTeXaMFqNOjw!VL7 zd@KGYz3&-6{7giEc3N!!-79EDy5$Yc4nzJH$)}~=83F*(by#^Ahb2#{n}`oo1l^9){5E0E19qX{a>@0u@@RZrRqYhfW#yTmu1b;k%aMd)stZqt zqp8zSdiSn4vx9%MJ<(G7?rnjghKuMc@$S0>_eHM=my7uxaP_{(0)v_bmUF{@`T^UK z<$6AT&s=L3MmjOv!cuN&eQ0by4oN@6b^{zEZV4j;j;BB39_70e-sGV8yUr~#JA3ltf;Lvb30 zAQ-VKaX`B9;%4HSmVS|eq>kC1XWKL-5h5}hr!r)Abk74B#>WAsv+`h?R9Y-k-%oCo zpN-yNTAtyU6c=kU4>=t>Lv0I;`)?>d3;mZ(R^C^DCT~{v|H~#b_?JzlCH-$US!@6Q zvdJuF*C`PMlQA=N{%>qD)yi!h^?I6usBlBGk>C;_ml&#%QoJ}NNe^Z+&S^`F6k{m$ z7|@-2gU<#VPh_}z!iRKQPl-tlO9_R?1AU{8PMf{amSs9yYCrsnFi>sKx<-$F+IBZwH zm_{OTG?moy1{DO%RFqNbdlGNsqaaD1Uh}Bd_*Qjh8B>jb%m0)xIlbzeNj8HtQzriU zm?-6OV$D$T2ND+P(3QxADR<%Lu}J8r)~^Ojc3{h-kBWss2H03omsCQzthgk@*-+kM zEMY{81!w*B45!M)nZ@N#@Wub&g}v5a$C#>Xl2C0vGMI)Nl{T*$g^Zm{{1Fe+oxan! z)l@Iez`ghl2q-p(CN%eg&v`sv)Ez-LJehO{k>8ha7hn_$-^!jZhM|1-=>|dyj^sbV zo4vZ8>h44>j>Z-3fEgDS5oOf_(}deodd>o~$@F0C<&3OO3o=67RqgE~=G@z3Vblm~ z+ETFMvua4D*Q+nk@CF212lWXMp~)veK)>;l-mY}=xL~i#cc!>xm}tn}=mxAOno3k} zE)X9MRq*%SxFk*0H_fIdftF3!BuM<+@p0pvXXLo3!GPaOS) z-4tiq*BE1k$Amgsfz3O@pSXp4t97W`v`wF0=HCA(Be*B~6%hg0_(EKH0MgR-l%dxO zU;erD^)B!qn=D)G-)*wUKhMA|Z7jmBT~A;mhZne8|C>z~{Etlr@IP&`)c_<>0ShL< ztoA>6tcJ*}or3S?Tw-~=k@o}kD00T`d$ODT@!qs#Y^BhLdD z>%BSI>12{q@26X3PUZAi`MpX!jgw8J$v4gR)+hA}+LFSvkq)IKn05z-(t#%H2IPaF zALk7x6u^^1178WCF1R~@>63@|R?PGoGzCL6mA-Fy@8o5$$!>Q$j30&Z8K7$pBfpKJ zjD@AW4nC5GTfLV8HiM0%CW9e)2^T3i4go;DYe2h;rq38z%u;1v+ec)vXDSU2zy5=H zodWnat@Ybk{JvEup5ccjb%E{K-T19^Ne0gt2$}&%mt&R2{7E=QSt|;dUYo~1bo-`u z3!RHoGpfyCKF1+g0m1=@4?+4Pdku1>I;Ojnnp|={SL32XI~6bJP3@tvlEk7I`4zMC zsMK87mIG|yFVgLz#nR)WkfL&7ab-V6f%v(KkET^|eD-5QMKl(d0-Mb{z~nZ=*Qxv+ z#Pp$fthOrIrNd{`YqKPSi))rkX&TGj9Fy)nXAG$4F(ANXy_q16t{W zxb+}={4(vpiKXcL!X|qglvfV`r&DgB{lL{zKEa)#PnAX~y$!Gc_%!1F(@kW zOE&=d0(>bGmA|SYxB;;?@>HI3R&!wb{?&j1EdK$N*#&6W&>c?wA3zyLBTXo+iJ!d1 z&-cN^!3i*=hKghFM-?&@5pxkW3h39}SR7@7oDRGi9y)9(KbKy; zsIa_J&0AR@^%-vk;OTxvad>d`~fl`MWON9r{Kc#a5&cp z_M(&hWf_r!2OVLQeJ|LMjR%*AI7Y%hV48ugFtS~iJTk0aZgbr6EP17NC^o(Kn9;r0 z{yc*W{C4WBL0b=c;~-R>?w3`f%#3LVQd+za0niX^w?Bk?_sGd;jID7qspK;y+Ln3y z7_L?qg=)9;CG3OMW#-J6z zQ{s}I3wwJzGyKBVDCnBd?sP^JDqpP{q9v`_M`=Ph_B`HBZ|=y z4o}v~2oy8$aDY>OC44f5T>2z_;I)#~8r4jSR}?6qHY#0ac} z4$wxV$6q5*v!Saj>b;1K38bJ>4aLF=`aFAbepV>qva|wRyTth!n1GjTgiz^Bt=i%& zc6u=D{JWLgjW1)?QrCCDa>6&d9EQTjB8lAVH;))1y4njVxJZ@_PWBN_oNFu#{3 zSCw2<1a$(1emdBl498-Ai4vPWiaEP*9U4yzCQs0H0T!e_>Hi?p@UcZv7tBD{6>JJo zEk>>UOCcJ;*-bw6Ch0XTXW_DwSb{p0{DJIgLB#~BTifUgtO z1pERT1mhC5Dugi;<1PY@M41=N@3e)i=n54we&!_N+x3U!A%tU^z+EA{&|HmA~HfA&@((xG3APo_cT zW&s%oM%0dlA^C^o>l#ce16-+|%V64w`@2v~BS&;b2yp3tF_?`2bq=L@P+gE!k91A7 zL`zA4klR2;^s0LOfN8C0gplzb?y6)r@8YRiW-p*{{pw`gU}v!XCKHqp)PS|REueeP z0r54D!GE2#x-hKPEXjzaH4v%}){Hvw_%e|C_9gdBJP)FH0Zt6xByHI;?6>-@r4}j# zac%7cF^BNw2gyGDN-Zhheuw1Ot^`&B{s)=1TlWu{wrGwedo$5Rui$t`btHJ!+Y)P6 zhkCmvd}I1B$&XR?^4R@hD0F6qd$nd2ntwoH5R)8TS)^r zzJez6*42+=_%`I2OA1G9$}wo=UOYfd#H5-sl0%gB8ECbUNDFtOlUUo0aV7w6|B1&n zgHkSrp?q5~b5D~vlLku{#1C*9VG3lVcwj`5Ofl*+=nd)*C8nP+x~m?O2D?f`RQbec z1TOBJQe{lYQ=Qce1Cxa9B_x)NG9qEZu(&j>34tfl5A_bPIb5Ft|;Ks~IWEeGKoK37t0W8?% z1&Th18#9tusZ<(R;M?3>P`U?5vAcc8kJ5fDPSGWzMj9t&A)h-vUf z^}27IQf%3e!@AZE!TuoVY7dB>)@4QbpCRCM0^a3~t z@3@hkGv+UPRoiZ9u%aIX<11Dg>?Zkq3)rUcQq*4Lr8u?Wpwn)tI#Pn9-|$LTh@@1l zEB2Co7Uj8^LYyieu@#>gck@)rEMSr1X0`3*?;%xJ76#f1?~ciE+paOwBA(5t2q-vB zJ57QNeo$>7}xMFL6W z4A2dTur^%nJe3_3$~A#v(P^ZBX>wvTohWUiw1XGfHK*V_Q2p>}7G(^)b1fwmVY1_w z2qE*#9gbp5`!ZK=HDilXTH8J|)tGu4BJuZKcsX=#faE<-Qb(~Kvb(Y$VvVIPSQdUpra4c_J+d27^=;Zhs>k!pU5WN^SKR zzHGZ7Ng9bW#q*MS3zcTSHP8bF4y{B2{(sqNSc3m;rxiVi$8$3a6eIqlM@gg5?*7&& zjT5QRqli$Z01OR^6RArrf=pvPR!D*}Tf;XwoXV}+``A1EUv?V2uUIk)s)>1NEf|5p z(V*Uo#KuIJ;vxEz(V=lf!~nU`1N<^+4f++)Bj}!yqCiFBY~YZ%A#iwPk0e;rKX@9& zFE5FeT;o}AszgEDz(aW03Mb-a0DT?)N-HU;>-nQ1GbDcj0MwaKNs|3}1Yx8NpfHg> zEaYNl?EgnQZLyCT1_vts4pp8?MVw9Gv6f(!ooXQVzv;Bpx<}pa(EiJCKqibzUIywS zD*25=P(bfGxLt6DRxolP?egD|@;1C_B29zm2BB-JdLxjBE#y54wn=1kl}Yw0{l7rD z`Aew>tEK#!mlR(JD?}=gjHj)ei~)}*|3FtjbgG)~tOv%ywdXw&k_0J@22q0_YeQyI zf*A2OQhJjI>HZObzOJMZGOxA*B7`dig5Y9uK#Nc9bza++XJg+i_b8YzogGl|1g#1_ zN8S+d77Rk(r(^i5>KeV)AYRG=ajR`r-yH$gu>GXxG9Cv@Tg0{{GW6hO2U88X@dVrr-?BwA_VM_VA7puP%M-; z7K_LCf0(&OQh|)t|3#iW2`;s~524oVW0a2!YI7G3XMEBX zi%xY-CrK~lxfJBog!(vrtJqGR#HT2{0OKkc4n2xON#hh|L?s4dY1n=BRKW5-Ppe=HgS+#QJzV{C7()4PU^GJ<|{;2e>Lqz!R zR0qLIW^7sjOHBp@3cch&$pvkCAQx^|vMoEi^Qi4UhW%I{UG{*GgXYiiVek~16| z;0Fk|a0T*TOp^uUH5v}QTwJ(Mm9OImCk#)bd3vc=NsNALSD_OWr4p2YZD#7F!g$6C z^%CN~f=h-2K`WyfRA})IMB_$7jY?Z&45y`IqWC&PyUk}aGPeFFBnCBJ#L^r|Z<{P- z!Ff{=*?@z)hVDniCFiO>tgE1Fv!1q`bn;*1A~AhLA^f{x%+NYjzHygJ4>l}?Tb?RI zvc3J=chY>i&S=L!a@zHeoYwmzr!~d>pK@Bn18ZH0qDty!QQGHQmZ#Wt)jx8YoNeQ8 zWB7&m)Ks}1;`f#_nhAL;4txWNyT^o4vw7W50@EateoC-w#5k%?zv*k@6q{1)mZEPR zf3KG@IoZUQs~yEU&mHrDP+nse663{>dtKmmVj63T%T{A|pDU>d`{r@Kj!KO4Gyh}E zZVVs4U1c}W9)1n+g_zWeYx8^BLpnG$Ar%lMF?O}; ztqBOy_@rucMkadbfc=s=#64OnlDIhU^VE9vL1&sZR%+>%n4ljNWGI z(-jjryyPp|NED(7yBbMH?9e?k>P9lc!Ulu_updV3NgNI zF55HmSl39UtNIY-;^e(Lz#N1T%wT=?M`iF=H}HWCi3+!3ML~K2x;MSq&lM?=`_UK^#s&f6=oK~ViT8`V2^%70 zVS$y2!Qh9}^UIg`0i0oA=Zq5QaF!bU5Nb{SB8k}0H=8EUbn0cC=CSjzg=)P}LG6{} zcAQcGsPN2MKMwBOt;Tb~!(`pz!O%hQ9rTtujLwmd-EwgFVrem8q2fX20Bx~@6De}N zTwMDMbi|k94TgaPPCKVsWD(t$9go!{`PSjG`u$7zK>!U_gMK3N>uu7$$tdw3UY$QCaDtW`7szl7YlnN_ z!{u%Vk~{M6D6%P~U*yS?X|tZsE)+EPIAcb$3dNL}rQsZup@wCcht*nO=`cjD5DQ$U zgr*gf@E8P=FOV^6B82P)wNpt^PO|MgWlRYT6e%Hxkj97)IEqGVP6}azT#UX2V+!nnJg&GO_>SFGPV_@Tt~g-B_%Q5;N&D^SBimEmX(2V2z^LNwAyY~ zOSbE7C1+^~?9_7aY$?QygQa4$U$NJDoX+Em2D45=k^AJBk=vA%Ri%Nl*o&QtNzVKj zO@84)@j(;N#4u25oxTr}9X@&iknBm;|65Pn6y)pewFfpJcj=GqKmD`%E z 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 None: argument_defaults["output_path_labels"] = False argument_defaults["output_visible_arguments"] = False argument_defaults["show-editor"] = True + argument_defaults["split_arcs"] = False argument_defaults["tlo"] = True argument_defaults["tool_change"] = True argument_defaults["translate_drill"] = False @@ -123,6 +124,7 @@ def init_arguments_visible(arguments_visible: Dict[str, bool]) -> None: arguments_visible["precision"] = True arguments_visible["return-to"] = False arguments_visible["show-editor"] = True + arguments_visible["split_arcs"] = True arguments_visible["tlo"] = True arguments_visible["tool_change"] = False arguments_visible["translate_drill"] = False @@ -436,6 +438,15 @@ def init_shared_arguments( "Don't pop up editor before writing output", arguments_visible["show-editor"], ) + add_flag_type_arguments( + shared, + argument_defaults["split_arcs"], + "--split-arcs", + "--no-split-arcs", + "Convert G2/G3 arc commands to discrete G1 line segments", + "Output G2/G3 arc commands as-is", + arguments_visible["split_arcs"], + ) add_flag_type_arguments( shared, argument_defaults["tlo"], @@ -703,6 +714,10 @@ def init_shared_values(values: Values) -> None: # values["SHOW_EDITOR"] = True # + # If True then G2/G3 arc commands will be converted to discrete G1 line segments. + # + values["SPLIT_ARCS"] = False + # # If True then the current machine units are output just before the PRE_OPERATION. # values["SHOW_MACHINE_UNITS"] = True @@ -906,6 +921,10 @@ def process_shared_arguments( values["SHOW_EDITOR"] = True if args.no_show_editor: values["SHOW_EDITOR"] = False + if args.split_arcs: + values["SPLIT_ARCS"] = True + if args.no_split_arcs: + values["SPLIT_ARCS"] = False if args.tlo: values["USE_TLO"] = True if args.no_tlo: diff --git a/src/Mod/CAM/Path/Post/UtilsParse.py b/src/Mod/CAM/Path/Post/UtilsParse.py index 6c763a68ba..6b9713f05b 100644 --- a/src/Mod/CAM/Path/Post/UtilsParse.py +++ b/src/Mod/CAM/Path/Post/UtilsParse.py @@ -37,6 +37,7 @@ import FreeCAD from FreeCAD import Units import Path +import Path.Post.Utils as PostUtils # Define some types that are used throughout this file CommandLine = List[str] @@ -704,7 +705,12 @@ def parse_a_path(values: Values, gcode: Gcode, pathobj) -> None: ) adaptive_op_variables = determine_adaptive_op(values, pathobj) - for c in pathobj.Path.Commands: + # Apply arc splitting if requested + path_to_process = pathobj.Path + if values["SPLIT_ARCS"]: + path_to_process = PostUtils.splitArcs(path_to_process) + + for c in path_to_process.Commands: command = c.Name command_line = [] diff --git a/src/Mod/CAM/Path/Post/scripts/centroid_legacy_post.py b/src/Mod/CAM/Path/Post/scripts/centroid_legacy_post.py new file mode 100644 index 0000000000..1c33671f40 --- /dev/null +++ b/src/Mod/CAM/Path/Post/scripts/centroid_legacy_post.py @@ -0,0 +1,347 @@ +# *************************************************************************** +# * Copyright (c) 2015 Dan Falck * +# * Copyright (c) 2020 Schildkroet * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * FreeCAD is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Lesser General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with FreeCAD; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +import os +import FreeCAD +from FreeCAD import Units +import Path.Post.Utils as PostUtils +from PathScripts import PathUtils +import datetime +import Path +from builtins import open as pyopen + +TOOLTIP = """ +This is a postprocessor file for the Path workbench. It is used to +take a pseudo-G-code fragment outputted by a Path object, and output +real G-code suitable for a centroid 3 axis mill. This postprocessor, once placed +in the appropriate Path/Tool folder, can be used directly from inside +FreeCAD, via the GUI importer or via python scripts with: + +import centroid_legacy_post +centroid_legacy_post.export(object,"/path/to/file.ncc","") +""" + +TOOLTIP_ARGS = """ +Arguments for centroid: + --header,--no-header ... output headers (--header) + --comments,--no-comments ... output comments (--comments) + --line-numbers,--no-line-numbers ... prefix with line numbers (--no-lin-numbers) + --show-editor, --no-show-editor ... pop up editor before writing output(--show-editor) + --feed-precision=1 ... number of digits of precision for feed rate. Default=1 + --axis-precision=4 ... number of digits of precision for axis moves. Default=4 + --inches ... Convert output for US imperial mode (G20) + --no-tlo ... Suppress tool length offset (G43) following tool changes +""" + +# These globals set common customization preferences +OUTPUT_COMMENTS = True +OUTPUT_HEADER = True +OUTPUT_LINE_NUMBERS = False +if FreeCAD.GuiUp: + SHOW_EDITOR = True +else: + SHOW_EDITOR = False +MODAL = False # if true commands are suppressed if the same as previous line. +USE_TLO = True # if true G43 will be output following tool changes + +COMMAND_SPACE = " " +LINENR = 100 # line number starting value + +# These globals will be reflected in the Machine configuration of the project +UNITS = "G21" # G21 for metric, G20 for us standard +UNIT_FORMAT = "mm" +UNIT_SPEED_FORMAT = "mm/min" +MACHINE_NAME = "Centroid" +CORNER_MIN = {"x": -609.6, "y": -152.4, "z": 0} # use metric for internal units +CORNER_MAX = {"x": 609.6, "y": 152.4, "z": 304.8} # use metric for internal units +AXIS_PRECISION = 4 +FEED_PRECISION = 1 +SPINDLE_DECIMALS = 0 + +COMMENT = ";" + +# gCode header with information about CAD-software, post-processor +# and date/time +if FreeCAD.ActiveDocument: + cam_file = os.path.basename(FreeCAD.ActiveDocument.FileName) +else: + cam_file = "" + +HEADER = """;Exported by FreeCAD +;Post Processor: {} +;CAM file: {} +;Output Time: {} +""".format( + __name__, cam_file, str(datetime.datetime.now()) +) + +# Preamble text will appear at the beginning of the GCODE output file. +PREAMBLE = """G53 G00 G17 +""" + +# Postamble text will appear following the last operation. +POSTAMBLE = """M99 +""" + +TOOLRETURN = """M5 +M25 +G49 H0 +""" # spindle off,height offset canceled,spindle retracted (M25 is a centroid command to retract spindle) + +ZAXISRETURN = """G91 G28 X0 Z0 +G90 +""" + +SAFETYBLOCK = """G90 G80 G40 G49 +""" + +# Pre operation text will be inserted before every operation +PRE_OPERATION = """""" + +# Post operation text will be inserted after every operation +POST_OPERATION = """""" + +# Tool Change commands will be inserted before a tool change +TOOL_CHANGE = """""" + + +def processArguments(argstring): + global OUTPUT_HEADER + global OUTPUT_COMMENTS + global OUTPUT_LINE_NUMBERS + global SHOW_EDITOR + global AXIS_PRECISION + global FEED_PRECISION + global UNIT_SPEED_FORMAT + global UNIT_FORMAT + global UNITS + global USE_TLO + + for arg in argstring.split(): + if arg == "--header": + OUTPUT_HEADER = True + elif arg == "--no-header": + OUTPUT_HEADER = False + elif arg == "--comments": + OUTPUT_COMMENTS = True + elif arg == "--no-comments": + OUTPUT_COMMENTS = False + elif arg == "--line-numbers": + OUTPUT_LINE_NUMBERS = True + elif arg == "--no-line-numbers": + OUTPUT_LINE_NUMBERS = False + elif arg == "--show-editor": + SHOW_EDITOR = True + elif arg == "--no-show-editor": + SHOW_EDITOR = False + elif arg.split("=")[0] == "--axis-precision": + AXIS_PRECISION = arg.split("=")[1] + elif arg.split("=")[0] == "--feed-precision": + FEED_PRECISION = arg.split("=")[1] + elif arg == "--inches": + UNITS = "G20" + UNIT_SPEED_FORMAT = "in/min" + UNIT_FORMAT = "in" + elif arg == "--no-tlo": + USE_TLO = False + + +def export(objectslist, filename, argstring): + processArguments(argstring) + for i in objectslist: + print(i.Name) + global UNITS + global UNIT_FORMAT + global UNIT_SPEED_FORMAT + + print("postprocessing...") + gcode = "" + + # write header + if OUTPUT_HEADER: + gcode += HEADER + + gcode += SAFETYBLOCK + + # Write the preamble + if OUTPUT_COMMENTS: + for item in objectslist: + if hasattr(item, "Proxy") and isinstance( + item.Proxy, Path.Tool.Controller.ToolController + ): + gcode += ";T{}={}\n".format(item.ToolNumber, item.Name) + gcode += linenumber() + ";begin preamble\n" + for line in PREAMBLE.splitlines(): + gcode += linenumber() + line + "\n" + + gcode += linenumber() + UNITS + "\n" + + for obj in objectslist: + # do the pre_op + if OUTPUT_COMMENTS: + gcode += linenumber() + ";begin operation\n" + for line in PRE_OPERATION.splitlines(True): + gcode += linenumber() + line + + gcode += parse(obj) + + # do the post_op + if OUTPUT_COMMENTS: + gcode += linenumber() + ";end operation: %s\n" % obj.Label + for line in POST_OPERATION.splitlines(True): + gcode += linenumber() + line + + # do the post_amble + + if OUTPUT_COMMENTS: + gcode += ";begin postamble\n" + for line in TOOLRETURN.splitlines(True): + gcode += linenumber() + line + for line in SAFETYBLOCK.splitlines(True): + gcode += linenumber() + line + for line in POSTAMBLE.splitlines(): + gcode += linenumber() + line + "\n" + + if SHOW_EDITOR: + dia = PostUtils.GCodeEditorDialog() + dia.editor.setText(gcode) + result = dia.exec_() + if result: + final = dia.editor.toPlainText() + else: + final = gcode + else: + final = gcode + + print("done postprocessing.") + + if not filename == "-": + gfile = pyopen(filename, "w") + gfile.write(final) + gfile.close() + + return final + + +def linenumber(): + global LINENR + if OUTPUT_LINE_NUMBERS is True: + LINENR += 10 + return "N" + str(LINENR) + " " + return "" + + +def parse(pathobj): + out = "" + lastcommand = None + axis_precision_string = "." + str(AXIS_PRECISION) + "f" + feed_precision_string = "." + str(FEED_PRECISION) + "f" + # the order of parameters + # centroid doesn't want K properties on XY plane Arcs need work. + params = ["X", "Y", "Z", "A", "B", "I", "J", "F", "S", "T", "Q", "R", "L", "H"] + + if hasattr(pathobj, "Group"): # We have a compound or project. + # if OUTPUT_COMMENTS: + # out += linenumber() + "(compound: " + pathobj.Label + ")\n" + for p in pathobj.Group: + out += parse(p) + return out + else: # parsing simple path + + # groups might contain non-path things like stock. + if not hasattr(pathobj, "Path"): + return out + + for c in PathUtils.getPathWithPlacement(pathobj).Commands: + commandlist = [] # list of elements in the command, code and params. + command = c.Name # command M or G code or comment string + + if command.startswith("("): + command = PostUtils.fcoms(command, COMMENT) + + commandlist.append(command) + if MODAL is True: + if command == lastcommand: + commandlist.pop(0) + + # Now add the remaining parameters in order + for param in params: + if param in c.Parameters: + if param == "F": + if c.Name not in [ + "G0", + "G00", + ]: # centroid doesn't use rapid speeds + speed = Units.Quantity(c.Parameters["F"], FreeCAD.Units.Velocity) + commandlist.append( + param + + format( + float(speed.getValueAs(UNIT_SPEED_FORMAT)), + feed_precision_string, + ) + ) + elif param == "H": + commandlist.append(param + str(int(c.Parameters["H"]))) + elif param == "S": + commandlist.append( + param + PostUtils.fmt(c.Parameters["S"], SPINDLE_DECIMALS, "G21") + ) + elif param == "T": + commandlist.append(param + str(int(c.Parameters["T"]))) + else: + pos = Units.Quantity(c.Parameters[param], FreeCAD.Units.Length) + commandlist.append( + param + + format( + float(pos.getValueAs(UNIT_FORMAT)), + axis_precision_string, + ) + ) + outstr = str(commandlist) + outstr = outstr.replace("[", "") + outstr = outstr.replace("]", "") + outstr = outstr.replace("'", "") + outstr = outstr.replace(",", "") + + # store the latest command + lastcommand = command + + # Check for Tool Change: + if command == "M6": + for line in TOOL_CHANGE.splitlines(True): + out += linenumber() + line + if USE_TLO: + out += linenumber() + "G43 H" + str(int(c.Parameters["T"])) + "\n" + + # prepend a line number and append a newline + if len(commandlist) >= 1: + if OUTPUT_LINE_NUMBERS: + commandlist.insert(0, (linenumber())) + + # append the line to the final output + for w in commandlist: + out += w + COMMAND_SPACE + out = out.strip() + "\n" + + return out diff --git a/src/Mod/CAM/Path/Post/scripts/centroid_post.py b/src/Mod/CAM/Path/Post/scripts/centroid_post.py index 169031a322..7daf3651e5 100644 --- a/src/Mod/CAM/Path/Post/scripts/centroid_post.py +++ b/src/Mod/CAM/Path/Post/scripts/centroid_post.py @@ -1,8 +1,9 @@ # SPDX-License-Identifier: LGPL-2.1-or-later # *************************************************************************** -# * Copyright (c) 2015 Dan Falck * -# * Copyright (c) 2020 Schildkroet * +# * Copyright (c) 2014 sliptonic * +# * Copyright (c) 2022 - 2025 Larry Woestman * +# * Copyright (c) 2024 Ondsel * # * * # * This file is part of the FreeCAD CAx development system. * # * * @@ -24,340 +25,172 @@ # * * # *************************************************************************** -import os -import FreeCAD -from FreeCAD import Units -import Path.Post.Utils as PostUtils -import PathScripts.PathUtils as PathUtils -import datetime +from typing import Any, Dict + +from Path.Post.Processor import PostProcessor + import Path -from builtins import open as pyopen +import FreeCAD -TOOLTIP = """ -This is a postprocessor file for the Path workbench. It is used to -take a pseudo-G-code fragment outputted by a Path object, and output -real G-code suitable for a centroid 3 axis mill. This postprocessor, once placed -in the appropriate Path/Tool folder, can be used directly from inside -FreeCAD, via the GUI importer or via python scripts with: +translate = FreeCAD.Qt.translate -import centroid_post -centroid_post.export(object,"/path/to/file.ncc","") -""" - -TOOLTIP_ARGS = """ -Arguments for centroid: - --header,--no-header ... output headers (--header) - --comments,--no-comments ... output comments (--comments) - --line-numbers,--no-line-numbers ... prefix with line numbers (--no-lin-numbers) - --show-editor, --no-show-editor ... pop up editor before writing output(--show-editor) - --feed-precision=1 ... number of digits of precision for feed rate. Default=1 - --axis-precision=4 ... number of digits of precision for axis moves. Default=4 - --inches ... Convert output for US imperial mode (G20) - --no-tlo ... Suppress tool length offset (G43) following tool changes -""" - -# These globals set common customization preferences -OUTPUT_COMMENTS = True -OUTPUT_HEADER = True -OUTPUT_LINE_NUMBERS = False -if FreeCAD.GuiUp: - SHOW_EDITOR = True +DEBUG = False +if DEBUG: + Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) + Path.Log.trackModule(Path.Log.thisModule()) else: - SHOW_EDITOR = False -MODAL = False # if true commands are suppressed if the same as previous line. -USE_TLO = True # if true G43 will be output following tool changes + Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule()) -COMMAND_SPACE = " " -LINENR = 100 # line number starting value +# +# Define some types that are used throughout this file. +# +Values = Dict[str, Any] +Visible = Dict[str, bool] -# These globals will be reflected in the Machine configuration of the project -UNITS = "G21" # G21 for metric, G20 for us standard -UNIT_FORMAT = "mm" -UNIT_SPEED_FORMAT = "mm/min" -MACHINE_NAME = "Centroid" -CORNER_MIN = {"x": -609.6, "y": -152.4, "z": 0} # use metric for internal units -CORNER_MAX = {"x": 609.6, "y": 152.4, "z": 304.8} # use metric for internal units -AXIS_PRECISION = 4 -FEED_PRECISION = 1 -SPINDLE_DECIMALS = 0 -COMMENT = ";" +class Centroid(PostProcessor): + """The Centroid post processor class.""" -# gCode header with information about CAD-software, post-processor -# and date/time -if FreeCAD.ActiveDocument: - cam_file = os.path.basename(FreeCAD.ActiveDocument.FileName) -else: - cam_file = "" + def __init__( + self, + job, + tooltip=translate("CAM", "Centroid post processor"), + tooltipargs=[""], + units="Metric", + ) -> None: + super().__init__( + job=job, + tooltip=tooltip, + tooltipargs=tooltipargs, + units=units, + ) + Path.Log.debug("Centroid post processor initialized.") -HEADER = """;Exported by FreeCAD -;Post Processor: {} -;CAM file: {} -;Output Time: {} -""".format( - __name__, cam_file, str(datetime.datetime.now()) -) - -# Preamble text will appear at the beginning of the GCODE output file. -PREAMBLE = """G53 G00 G17 -""" - -# Postamble text will appear following the last operation. -POSTAMBLE = """M99 -""" - -TOOLRETURN = """M5 + def init_values(self, values: Values) -> None: + """Initialize values that are used throughout the postprocessor.""" + # + super().init_values(values) + # + # Set any values here that need to override the default values set + # in the parent routine. + # + # Use 4 digits for axis precision by default. + # + values["AXIS_PRECISION"] = 4 + values["DEFAULT_AXIS_PRECISION"] = 4 + values["DEFAULT_INCH_AXIS_PRECISION"] = 4 + # + # Use ";" as the comment symbol + # + values["COMMENT_SYMBOL"] = ";" + # + # Use 1 digit for feed precision by default. + # + values["FEED_PRECISION"] = 1 + values["DEFAULT_FEED_PRECISION"] = 1 + values["DEFAULT_INCH_FEED_PRECISION"] = 1 + # + # This value usually shows up in the post_op comment as "Finish operation:". + # Change it to "End" to produce "End operation:". + # + values["FINISH_LABEL"] = "End" + # + # If this value is True, then a list of tool numbers + # with their labels are output just before the preamble. + # + values["LIST_TOOLS_IN_PREAMBLE"] = True + # + # Used in the argparser code as the "name" of the postprocessor program. + # This would normally show up in the usage message in the TOOLTIP_ARGS. + # + values["MACHINE_NAME"] = "Centroid" + # + # This list controls the order of parameters in a line during output. + # centroid doesn't want K properties on XY plane; Arcs need work. + # + values["PARAMETER_ORDER"] = [ + "X", + "Y", + "Z", + "A", + "B", + "I", + "J", + "F", + "S", + "T", + "Q", + "R", + "L", + "H", + ] + # + # Any commands in this value will be output as the last commands + # in the G-code file. + # + values["POSTAMBLE"] = """M99""" + values["POSTPROCESSOR_FILE_NAME"] = __name__ + # + # Any commands in this value will be output after the header and + # safety block at the beginning of the G-code file. + # + values["PREAMBLE"] = """G53 G00 G17""" + # + # Output any messages. + # + values["REMOVE_MESSAGES"] = False + # + # Any commands in this value are output after the header but before the preamble, + # then again after the TOOLRETURN but before the POSTAMBLE. + # + values["SAFETYBLOCK"] = """G90 G80 G40 G49""" + # + # Do not show the current machine units just before the PRE_OPERATION. + # + values["SHOW_MACHINE_UNITS"] = False + # + # Do not show the current operation label just before the PRE_OPERATION. + # + values["SHOW_OPERATION_LABELS"] = False + # + # Do not output an M5 command to stop the spindle for tool changes. + # + values["STOP_SPINDLE_FOR_TOOL_CHANGE"] = False + # + # spindle off, height offset canceled, spindle retracted + # (M25 is a centroid command to retract spindle) + # + values[ + "TOOLRETURN" + ] = """M5 M25 -G49 H0 -""" # spindle off,height offset canceled,spindle retracted (M25 is a centroid command to retract spindle) +G49 H0""" + # + # Default to not outputting a G43 following tool changes + # + values["USE_TLO"] = False + # + # This was in the original centroid postprocessor file + # but does not appear to be used anywhere. + # + # ZAXISRETURN = """G91 G28 X0 Z0 G90""" + # -ZAXISRETURN = """G91 G28 X0 Z0 -G90 -""" + def init_arguments_visible(self, arguments_visible: Visible) -> None: + """Initialize which argument pairs are visible in TOOLTIP_ARGS.""" + super().init_arguments_visible(arguments_visible) + # + # Modify the visibility of any arguments from the defaults here. + # + arguments_visible["axis-modal"] = False + arguments_visible["precision"] = False + arguments_visible["tlo"] = False -SAFETYBLOCK = """G90 G80 G40 G49 -""" - -# Pre operation text will be inserted before every operation -PRE_OPERATION = """""" - -# Post operation text will be inserted after every operation -POST_OPERATION = """""" - -# Tool Change commands will be inserted before a tool change -TOOL_CHANGE = """""" - - -def processArguments(argstring): - global OUTPUT_HEADER - global OUTPUT_COMMENTS - global OUTPUT_LINE_NUMBERS - global SHOW_EDITOR - global AXIS_PRECISION - global FEED_PRECISION - global UNIT_SPEED_FORMAT - global UNIT_FORMAT - global UNITS - global USE_TLO - - for arg in argstring.split(): - if arg == "--header": - OUTPUT_HEADER = True - elif arg == "--no-header": - OUTPUT_HEADER = False - elif arg == "--comments": - OUTPUT_COMMENTS = True - elif arg == "--no-comments": - OUTPUT_COMMENTS = False - elif arg == "--line-numbers": - OUTPUT_LINE_NUMBERS = True - elif arg == "--no-line-numbers": - OUTPUT_LINE_NUMBERS = False - elif arg == "--show-editor": - SHOW_EDITOR = True - elif arg == "--no-show-editor": - SHOW_EDITOR = False - elif arg.split("=")[0] == "--axis-precision": - AXIS_PRECISION = arg.split("=")[1] - elif arg.split("=")[0] == "--feed-precision": - FEED_PRECISION = arg.split("=")[1] - elif arg == "--inches": - UNITS = "G20" - UNIT_SPEED_FORMAT = "in/min" - UNIT_FORMAT = "in" - elif arg == "--no-tlo": - USE_TLO = False - - -def export(objectslist, filename, argstring): - processArguments(argstring) - for i in objectslist: - print(i.Name) - global UNITS - global UNIT_FORMAT - global UNIT_SPEED_FORMAT - - print("postprocessing...") - gcode = "" - - # write header - if OUTPUT_HEADER: - gcode += HEADER - - gcode += SAFETYBLOCK - - # Write the preamble - if OUTPUT_COMMENTS: - for item in objectslist: - if hasattr(item, "Proxy") and isinstance( - item.Proxy, Path.Tool.Controller.ToolController - ): - gcode += ";T{}={}\n".format(item.ToolNumber, item.Name) - gcode += linenumber() + ";begin preamble\n" - for line in PREAMBLE.splitlines(): - gcode += linenumber() + line + "\n" - - gcode += linenumber() + UNITS + "\n" - - for obj in objectslist: - # do the pre_op - if OUTPUT_COMMENTS: - gcode += linenumber() + ";begin operation\n" - for line in PRE_OPERATION.splitlines(True): - gcode += linenumber() + line - - gcode += parse(obj) - - # do the post_op - if OUTPUT_COMMENTS: - gcode += linenumber() + ";end operation: %s\n" % obj.Label - for line in POST_OPERATION.splitlines(True): - gcode += linenumber() + line - - # do the post_amble - - if OUTPUT_COMMENTS: - gcode += ";begin postamble\n" - for line in TOOLRETURN.splitlines(True): - gcode += linenumber() + line - for line in SAFETYBLOCK.splitlines(True): - gcode += linenumber() + line - for line in POSTAMBLE.splitlines(): - gcode += linenumber() + line + "\n" - - if SHOW_EDITOR: - dia = PostUtils.GCodeEditorDialog() - dia.editor.setText(gcode) - result = dia.exec_() - if result: - final = dia.editor.toPlainText() - else: - final = gcode - else: - final = gcode - - print("done postprocessing.") - - if not filename == "-": - gfile = pyopen(filename, "w") - gfile.write(final) - gfile.close() - - return final - - -def linenumber(): - global LINENR - if OUTPUT_LINE_NUMBERS is True: - LINENR += 10 - return "N" + str(LINENR) + " " - return "" - - -def parse(pathobj): - out = "" - lastcommand = None - axis_precision_string = "." + str(AXIS_PRECISION) + "f" - feed_precision_string = "." + str(FEED_PRECISION) + "f" - # params = ['X','Y','Z','A','B','I','J','K','F','S'] #This list control - # the order of parameters - # centroid doesn't want K properties on XY plane Arcs need work. - params = ["X", "Y", "Z", "A", "B", "I", "J", "F", "S", "T", "Q", "R", "L", "H"] - - if hasattr(pathobj, "Group"): # We have a compound or project. - # if OUTPUT_COMMENTS: - # out += linenumber() + "(compound: " + pathobj.Label + ")\n" - for p in pathobj.Group: - out += parse(p) - return out - else: # parsing simple path - - # groups might contain non-path things like stock. - if not hasattr(pathobj, "Path"): - return out - - # if OUTPUT_COMMENTS: - # out += linenumber() + "(" + pathobj.Label + ")\n" - - for c in PathUtils.getPathWithPlacement(pathobj).Commands: - commandlist = [] # list of elements in the command, code and params. - command = c.Name # command M or G code or comment string - - if command.startswith("("): - command = PostUtils.fcoms(command, COMMENT) - - commandlist.append(command) - # if modal: only print the command if it is not the same as the - # last one - if MODAL is True: - if command == lastcommand: - commandlist.pop(0) - - # Now add the remaining parameters in order - for param in params: - if param in c.Parameters: - if param == "F": - if c.Name not in [ - "G0", - "G00", - ]: # centroid doesn't use rapid speeds - speed = Units.Quantity(c.Parameters["F"], FreeCAD.Units.Velocity) - commandlist.append( - param - + format( - float(speed.getValueAs(UNIT_SPEED_FORMAT)), - feed_precision_string, - ) - ) - elif param == "H": - commandlist.append(param + str(int(c.Parameters["H"]))) - elif param == "S": - commandlist.append( - param + PostUtils.fmt(c.Parameters["S"], SPINDLE_DECIMALS, "G21") - ) - elif param == "T": - commandlist.append(param + str(int(c.Parameters["T"]))) - else: - pos = Units.Quantity(c.Parameters[param], FreeCAD.Units.Length) - commandlist.append( - param - + format( - float(pos.getValueAs(UNIT_FORMAT)), - axis_precision_string, - ) - ) - outstr = str(commandlist) - outstr = outstr.replace("[", "") - outstr = outstr.replace("]", "") - outstr = outstr.replace("'", "") - outstr = outstr.replace(",", "") - - # store the latest command - lastcommand = command - - # Check for Tool Change: - if command == "M6": - # if OUTPUT_COMMENTS: - # out += linenumber() + "(begin toolchange)\n" - for line in TOOL_CHANGE.splitlines(True): - out += linenumber() + line - if USE_TLO: - out += linenumber() + "G43 H" + str(int(c.Parameters["T"])) + "\n" - - # if command == "message": - # if OUTPUT_COMMENTS is False: - # out = [] - # else: - # commandlist.pop(0) # remove the command - - # prepend a line number and append a newline - if len(commandlist) >= 1: - if OUTPUT_LINE_NUMBERS: - commandlist.insert(0, (linenumber())) - - # append the line to the final output - for w in commandlist: - out += w + COMMAND_SPACE - out = out.strip() + "\n" - - return out + @property + def tooltip(self): + tooltip: str = """ + This is a postprocessor file for the CAM workbench. + It is used to take a pseudo-gcode fragment from a CAM object + and output 'real' GCode suitable for a centroid 3 axis mill. + """ + return tooltip diff --git a/src/Mod/CAM/Path/Post/scripts/comparams_post.py b/src/Mod/CAM/Path/Post/scripts/comparams_post.py deleted file mode 100644 index bd3fd6c424..0000000000 --- a/src/Mod/CAM/Path/Post/scripts/comparams_post.py +++ /dev/null @@ -1,116 +0,0 @@ -# SPDX-License-Identifier: LGPL-2.1-or-later - -# *************************************************************************** -# * Copyright (c) 2015 Dan Falck * -# * * -# * This program is free software; you can redistribute it and/or modify * -# * it under the terms of the GNU Lesser General Public License (LGPL) * -# * as published by the Free Software Foundation; either version 2 of * -# * the License, or (at your option) any later version. * -# * for detail see the LICENCE text file. * -# * * -# * This program is distributed in the hope that it will be useful, * -# * but WITHOUT ANY WARRANTY; without even the implied warranty of * -# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -# * GNU Library General Public License for more details. * -# * * -# * You should have received a copy of the GNU Library General Public * -# * License along with this program; if not, write to the Free Software * -# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * -# * USA * -# * * -# *************************************************************************** - -import FreeCAD -import Path -import Path.Post.Utils as PostUtils -import PathScripts.PathUtils as PathUtils - -TOOLTIP = ( - """Example Post, using Path.Commands instead of Path.toGCode strings for Path G-code output.""" -) - -SHOW_EDITOR = True - - -def fmt(num): - fnum = "" - fnum += "%.3f" % (num) - return fnum - - -def ffmt(num): - fnum = "" - fnum += "%.1f" % (num) - return fnum - - -class saveVals(object): - """save command info for modal output""" - - def __init__(self, command): - self.com = command.Name - self.params = command.Parameters - - def retVals(self): - return self.com, self.params - - -def lineout(command, oldvals, modal): - line = "" - if modal and (oldvals.com == command.Name): - line += "" - else: - line += str(command.Name) - if command.Name == "M6": - line += "T" + str(int(command.Parameters["T"])) - if command.Name == "M3": - line += "S" + str(ffmt(command.Parameters["S"])) - if command.Name == "M4": - line += "S" + str(ffmt(command.Parameters["S"])) - if "X" in command.Parameters: - line += "X" + str(fmt(command.Parameters["X"])) - if "Y" in command.Parameters: - line += "Y" + str(fmt(command.Parameters["Y"])) - if "Z" in command.Parameters: - line += "Z" + str(fmt(command.Parameters["Z"])) - if "I" in command.Parameters: - line += "I" + str(fmt(command.Parameters["I"])) - if "J" in command.Parameters: - line += "J" + str(fmt(command.Parameters["J"])) - if "F" in command.Parameters: - line += "F" + str(ffmt(command.Parameters["F"])) - return line - - -def export(obj, filename, argstring): - modal = True - gcode = "" - safetyblock1 = "G90G40G49\n" - gcode += safetyblock1 - - units = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Units") - if units.GetInt("UserSchema") == 0: - firstcommand = Path.Command("G21") # metric mode - else: - firstcommand = Path.Command("G20") # inch mode - oldvals = saveVals(firstcommand) # save first command for modal use - fp = obj[0] - gcode += firstcommand.Name - - if hasattr(fp, "Path"): - for c in PathUtils.getPathWithPlacement(fp).Commands: - gcode += lineout(c, oldvals, modal) + "\n" - oldvals = saveVals(c) - gcode += "M2\n" - gfile = open(filename, "w") - gfile.write(gcode) - gfile.close() - else: - FreeCAD.Console.PrintError("Select a path object and try again\n") - - if SHOW_EDITOR: - FreeCAD.Console.PrintMessage("Editor Activated\n") - dia = PostUtils.GCodeEditorDialog() - dia.editor.setText(gcode) - dia.exec_() diff --git a/src/Mod/CAM/Path/Post/scripts/dumper_post.py b/src/Mod/CAM/Path/Post/scripts/dumper_post.py deleted file mode 100644 index 440b92c51f..0000000000 --- a/src/Mod/CAM/Path/Post/scripts/dumper_post.py +++ /dev/null @@ -1,98 +0,0 @@ -# SPDX-License-Identifier: LGPL-2.1-or-later - -# *************************************************************************** -# * Copyright (c) 2014 sliptonic * -# * * -# * This file is part of the FreeCAD CAx development system. * -# * * -# * This program is free software; you can redistribute it and/or modify * -# * it under the terms of the GNU Lesser General Public License (LGPL) * -# * as published by the Free Software Foundation; either version 2 of * -# * the License, or (at your option) any later version. * -# * for detail see the LICENCE text file. * -# * * -# * FreeCAD is distributed in the hope that it will be useful, * -# * but WITHOUT ANY WARRANTY; without even the implied warranty of * -# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -# * GNU Lesser General Public License for more details. * -# * * -# * You should have received a copy of the GNU Library General Public * -# * License along with FreeCAD; if not, write to the Free Software * -# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * -# * USA * -# * * -# *************************************************************************** - -import datetime -import Path.Post.Utils as PostUtils -import PathScripts.PathUtils as PathUtils -from builtins import open as pyopen - -TOOLTIP = """ -Dumper is an extremely simple postprocessor file for the CAM workbench. It is used -to dump the command list from one or more Path objects for simple inspection. This post -doesn't do any manipulation of the path and doesn't write anything to disk. It just -shows the dialog so you can see it. Useful for debugging, but not much else. -""" - -now = datetime.datetime.now() -SHOW_EDITOR = True - - -def export(objectslist, filename, argstring): - "called when freecad exports a list of objects" - - output = """(This output produced with the dump post processor) -(Dump is useful for inspecting the raw commands in your paths) -(but is not useful for driving machines.) -(Consider setting a default postprocessor in your project or ) -(exporting your paths using a specific post that matches your machine) - -""" - - for obj in objectslist: - - if not hasattr(obj, "Path"): - print( - "the object " + obj.Name + " is not a path. Please select only path and Compounds." - ) - return - print("postprocessing...") - output += parse(obj) - - if SHOW_EDITOR: - dia = PostUtils.GCodeEditorDialog() - dia.editor.setText(output) - result = dia.exec_() - if result: - final = dia.editor.toPlainText() - else: - final = output - else: - final = output - - print("done postprocessing.") - return final - - -def parse(pathobj): - out = "" - - if hasattr(pathobj, "Group"): # We have a compound or project. - out += "(Group: " + pathobj.Label + ")\n" - for p in pathobj.Group: - out += parse(p) - return out - else: # parsing simple path - - if not hasattr(pathobj, "Path"): # groups might contain non-path things like stock. - return out - - out += "(Path: " + pathobj.Label + ")\n" - - for c in PathUtils.getPathWithPlacement(pathobj).Commands: - out += str(c) + "\n" - return out - - -# print(__name__ + " gcode postprocessor loaded.") diff --git a/src/Mod/CAM/Path/Post/scripts/example_post.py b/src/Mod/CAM/Path/Post/scripts/example_post.py deleted file mode 100644 index 9c6d427b2f..0000000000 --- a/src/Mod/CAM/Path/Post/scripts/example_post.py +++ /dev/null @@ -1,100 +0,0 @@ -# SPDX-License-Identifier: LGPL-2.1-or-later - -# *************************************************************************** -# * Copyright (c) 2014 Yorik van Havre * -# * * -# * This file is part of the FreeCAD CAx development system. * -# * * -# * This program is free software; you can redistribute it and/or modify * -# * it under the terms of the GNU Lesser General Public License (LGPL) * -# * as published by the Free Software Foundation; either version 2 of * -# * the License, or (at your option) any later version. * -# * for detail see the LICENCE text file. * -# * * -# * FreeCAD is distributed in the hope that it will be useful, * -# * but WITHOUT ANY WARRANTY; without even the implied warranty of * -# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -# * GNU Lesser General Public License for more details. * -# * * -# * You should have received a copy of the GNU Library General Public * -# * License along with FreeCAD; if not, write to the Free Software * -# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * -# * USA * -# * * -# *************************************************************************** - -import datetime -from builtins import open as pyopen - -TOOLTIP = """ -This is an example postprocessor file for the Path workbench. It is used -to save a list of FreeCAD Path objects to a file. - -Read the Path Workbench documentation to know how to convert Path objects -to GCode. -""" - -now = datetime.datetime.now() - - -def export(objectslist, filename, argstring): - "called when freecad exports a list of objects" - if len(objectslist) > 1: - print("This script is unable to write more than one Path object") - return - obj = objectslist[0] - if not hasattr(obj, "Path"): - print("the given object is not a path") - gcode = obj.Path.toGCode() - gcode = parse(gcode) - gfile = pyopen(filename, "w") - gfile.write(gcode) - gfile.close() - - -def parse(inputstring): - "parse(inputstring): returns a parsed output string" - print("postprocessing...") - - output = "" - - # write some stuff first - output += "N10 ;time:" + str(now) + "\n" - output += "N20 G17 G20 G80 G40 G90\n" - output += "N30 (Exported by FreeCAD)\n" - - linenr = 100 - lastcommand = None - # treat the input line by line - lines = inputstring.split("\n") - for line in lines: - # split the G/M command from the arguments - if " " in line: - command, args = line.split(" ", 1) - else: - # no space found, which means there are no arguments - command = line - args = "" - # add a line number - output += "N" + str(linenr) + " " - # only print the command if it is not the same as the last one - if command != lastcommand: - output += command + " " - output += args + "\n" - # increment the line number - linenr += 10 - # store the latest command - lastcommand = command - - # write some more stuff at the end - output += "N" + str(linenr) + " M05\n" - output += "N" + str(linenr + 10) + " M25\n" - output += "N" + str(linenr + 20) + " G00 X-1.0 Y1.0\n" - output += "N" + str(linenr + 30) + " G17 G80 G40 G90\n" - output += "N" + str(linenr + 40) + " M99\n" - - print("done postprocessing.") - return output - - -# print(__name__ + " gcode postprocessor loaded.") diff --git a/src/Mod/CAM/Path/Post/scripts/example_pre.py b/src/Mod/CAM/Path/Post/scripts/example_pre.py deleted file mode 100644 index 6ae0c5fa39..0000000000 --- a/src/Mod/CAM/Path/Post/scripts/example_pre.py +++ /dev/null @@ -1,115 +0,0 @@ -# SPDX-License-Identifier: LGPL-2.1-or-later - -# *************************************************************************** -# * Copyright (c) 2014 Yorik van Havre * -# * * -# * This file is part of the FreeCAD CAx development system. * -# * * -# * This program is free software; you can redistribute it and/or modify * -# * it under the terms of the GNU Lesser General Public License (LGPL) * -# * as published by the Free Software Foundation; either version 2 of * -# * the License, or (at your option) any later version. * -# * for detail see the LICENCE text file. * -# * * -# * FreeCAD is distributed in the hope that it will be useful, * -# * but WITHOUT ANY WARRANTY; without even the implied warranty of * -# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -# * GNU Lesser General Public License for more details. * -# * * -# * You should have received a copy of the GNU Library General Public * -# * License along with FreeCAD; if not, write to the Free Software * -# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * -# * USA * -# * * -# *************************************************************************** - - -""" -This is an example preprocessor file for the Path workbench. Its aim is to -open a gcode file, parse its contents, and create the appropriate objects -in FreeCAD. This preprocessor will not add imported gcode to an existing -job. For a more useful preprocessor, look at the gcode_pre.py file - -Read the Path Workbench documentation to know how to create Path objects -from GCode. -""" - -import FreeCAD -import Path -import os -from builtins import open as pyopen - -# LEVEL = Path.Log.Level.DEBUG -LEVEL = Path.Log.Level.INFO -Path.Log.setLevel(LEVEL, Path.Log.thisModule()) - -if LEVEL == Path.Log.Level.DEBUG: - Path.Log.trackModule(Path.Log.thisModule()) - - -def open(filename): - "called when freecad opens a file." - Path.Log.track(filename) - docname = os.path.splitext(os.path.basename(filename))[0] - doc = FreeCAD.newDocument(docname) - insert(filename, doc.Name) - - -def insert(filename, docname): - "called when freecad imports a file" - Path.Log.track(filename) - gfile = pyopen(filename) - gcode = gfile.read() - gfile.close() - gcode = parse(gcode) - doc = FreeCAD.getDocument(docname) - obj = doc.addObject("Path::Feature", "Path") - path = Path.Path(gcode) - obj.Path = path - - -def parse(inputstring): - "parse(inputstring): returns a parsed output string" - print("preprocessing...") - Path.Log.track(inputstring) - # split the input by line - lines = inputstring.split("\n") - output = [] - lastcommand = None - - for lin in lines: - # remove any leftover trailing and preceding spaces - lin = lin.strip() - if not lin: - # discard empty lines - continue - if lin[0].upper() in ["N"]: - # remove line numbers - lin = lin.split(" ", 1) - if len(lin) >= 1: - lin = lin[1].strip() - else: - continue - - if lin[0] in ["(", "%", "#", ";"]: - # discard comment and other non strictly gcode lines - continue - if lin[0].upper() in ["G", "M"]: - # found a G or M command: we store it - output.append(Path.Command(str(lin))) - last = lin[0].upper() - for c in lin[1:]: - if not c.isdigit(): - break - else: - last += c - lastcommand = last - elif lastcommand: - # no G or M command: we repeat the last one - output.append(Path.Command(str(lastcommand + " " + lin))) - - print("done preprocessing.") - return output - - -print(__name__ + " gcode preprocessor loaded.") diff --git a/src/Mod/CAM/Path/Post/scripts/generic_post.py b/src/Mod/CAM/Path/Post/scripts/generic_post.py index c5f562b67f..521bd1789c 100644 --- a/src/Mod/CAM/Path/Post/scripts/generic_post.py +++ b/src/Mod/CAM/Path/Post/scripts/generic_post.py @@ -21,7 +21,7 @@ # * * # *************************************************************************** -import os +from typing import Any, Dict from Path.Post.Processor import PostProcessor import Path import FreeCAD @@ -30,57 +30,65 @@ Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule()) translate = FreeCAD.Qt.translate -debug = True +debug = False if debug: Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) Path.Log.trackModule(Path.Log.thisModule()) else: Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule()) +Values = Dict[str, Any] + class Generic(PostProcessor): def __init__(self, job): super().__init__( job, tooltip=translate("CAM", "Generic post processor"), - tooltipargs=["arg1", "arg2"], - units="kg", + tooltipargs=[], + units="Metric", ) Path.Log.debug("Generic post processor initialized") - def export(self): - Path.Log.debug("Exporting the job") + def init_values(self, values: Values) -> None: + """Initialize values that are used throughout the postprocessor.""" + # + super().init_values(values) + values["POSTPROCESSOR_FILE_NAME"] = __name__ + values["MACHINE_NAME"] = "Generic" - postables = self._buildPostList() - Path.Log.debug(f"postables count: {len(postables)}") - - g_code_sections = [] - for idx, section in enumerate(postables): - partname, sublist = section - - # here is where the sections are converted to gcode. - g_code_sections.append((idx, partname)) - - return g_code_sections + # Set any values here that need to override the default values set + # in the parent routine. + # + # Any commands in this value will be output after the header and + # safety block at the beginning of the G-code file. + # + values["PREAMBLE"] = """""" + # + # Any commands in this value will be output as the last commands + # in the G-code file. + # + values["POSTAMBLE"] = """""" @property def tooltip(self): tooltip = """ This is a generic post processor. - It doesn't do anything yet because we haven't immplemented it. - - Implementing it would be a good idea + It exposes functionality of the base post processor. """ return tooltip @property def tooltipArgs(self): - argtooltip = """ - --arg1: This is the first argument - --arg2: This is the second argument + argtooltip = super().tooltipArgs - """ + # One could add additional arguments here. + # argtooltip += """ + # --arg1: This is the first argument + # --arg2: This is the second argument + + # """ return argtooltip @property diff --git a/src/Mod/CAM/Path/Post/scripts/grbl_legacy_post.py b/src/Mod/CAM/Path/Post/scripts/grbl_legacy_post.py new file mode 100644 index 0000000000..d3b6b96f25 --- /dev/null +++ b/src/Mod/CAM/Path/Post/scripts/grbl_legacy_post.py @@ -0,0 +1,717 @@ +# *************************************************************************** +# * Copyright (c) 2014 sliptonic * +# * Copyright (c) 2018, 2019 Gauthier Briere * +# * Copyright (c) 2019, 2020 Schildkroet * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * FreeCAD is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Lesser General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with FreeCAD; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +import FreeCAD +from FreeCAD import Units +import Path +import Path.Base.Util as PathUtil +import Path.Post.Utils as PostUtils +import PathScripts.PathUtils as PathUtils +import argparse +import datetime +import shlex +import re +from builtins import open as pyopen + + +TOOLTIP = """ +Generate g-code from a Path that is compatible with the grbl controller. +import grbl_legacy_post +grbl_legacy_post.export(object, "/path/to/file.ncc") +""" + + +# *************************************************************************** +# * Globals set customization preferences +# *************************************************************************** + +# Default values for command line arguments: +OUTPUT_COMMENTS = True # default output of comments in output gCode file +OUTPUT_HEADER = True # default output header in output gCode file +OUTPUT_LINE_NUMBERS = False # default doesn't output line numbers in output gCode file +OUTPUT_BCNC = False # default doesn't add bCNC operation block headers in output gCode file +SHOW_EDITOR = True # default show the resulting file dialog output in GUI +PRECISION = 3 # Default precision for metric (see http://linuxcnc.org/docs/2.7/html/gcode/overview.html#_g_code_best_practices) +TRANSLATE_DRILL_CYCLES = True # If true, G81, G82 & G83 are translated in G0/G1 moves +PREAMBLE = """G17 G90 +""" # default preamble text will appear at the beginning of the gCode output file. +POSTAMBLE = """M5 +G17 G90 +M2 +""" # default postamble text will appear following the last operation. + +SPINDLE_WAIT = 0 # no waiting after M3 / M4 by default +RETURN_TO = None # no movements after end of program + +# Customisation with no command line argument +MODAL = False # if true commands are suppressed if the same as previous line. +LINENR = 100 # line number starting value +LINEINCR = 10 # line number increment +OUTPUT_TOOL_CHANGE = ( + False # default don't output M6 tool changes (comment it) as grbl currently does not handle it +) +DRILL_RETRACT_MODE = ( + "G98" # Default value of drill retractations (CURRENT_Z) other possible value is G99 +) +MOTION_MODE = "G90" # G90 for absolute moves, G91 for relative +UNITS = "G21" # G21 for metric, G20 for us standard +UNIT_FORMAT = "mm" +UNIT_SPEED_FORMAT = "mm/min" +PRE_OPERATION = """""" # Pre operation text will be inserted before every operation +POST_OPERATION = """""" # Post operation text will be inserted after every operation +TOOL_CHANGE = """""" # Tool Change commands will be inserted before a tool change + +# *************************************************************************** +# * End of customization +# *************************************************************************** + +# Parser arguments list & definition +parser = argparse.ArgumentParser(prog="grbl", add_help=False) +parser.add_argument("--comments", action="store_true", help="output comment (default)") +parser.add_argument("--no-comments", action="store_true", help="suppress comment output") +parser.add_argument("--header", action="store_true", help="output headers (default)") +parser.add_argument("--no-header", action="store_true", help="suppress header output") +parser.add_argument("--line-numbers", action="store_true", help="prefix with line numbers") +parser.add_argument( + "--no-line-numbers", + action="store_true", + help="don't prefix with line numbers (default)", +) +parser.add_argument( + "--show-editor", + action="store_true", + help="pop up editor before writing output (default)", +) +parser.add_argument( + "--no-show-editor", + action="store_true", + help="don't pop up editor before writing output", +) +parser.add_argument("--precision", default="3", help="number of digits of precision, default=3") +parser.add_argument( + "--translate_drill", + action="store_true", + help="translate drill cycles G81, G82 & G83 in G0/G1 movements", +) +parser.add_argument( + "--no-translate_drill", + action="store_true", + help="don't translate drill cycles G81, G82 & G83 in G0/G1 movements (default)", +) +parser.add_argument( + "--preamble", + help='set commands to be issued before the first command, default="G17 G90\\n"', +) +parser.add_argument( + "--postamble", + help='set commands to be issued after the last command, default="M5\\nG17 G90\\nM2\\n"', +) +parser.add_argument( + "--inches", action="store_true", help="Convert output for US imperial mode (G20)" +) +parser.add_argument("--tool-change", action="store_true", help="Insert M6 for all tool changes") +parser.add_argument( + "--wait-for-spindle", + type=int, + default=0, + help="Wait for spindle to reach desired speed after M3 / M4, default=0", +) +parser.add_argument( + "--return-to", + default="", + help="Move to the specified coordinates at the end, e.g. --return-to=0,0", +) +parser.add_argument( + "--bcnc", + action="store_true", + help="Add Job operations as bCNC block headers. Consider suppressing existing comments: Add argument --no-comments", +) +parser.add_argument( + "--no-bcnc", action="store_true", help="suppress bCNC block header output (default)" +) +TOOLTIP_ARGS = parser.format_help() + + +# *************************************************************************** +# * Internal global variables +# *************************************************************************** +MOTION_COMMANDS = [ + "G0", + "G00", + "G1", + "G01", + "G2", + "G02", + "G3", + "G03", +] # Motion gCode commands definition +RAPID_MOVES = ["G0", "G00"] # Rapid moves gCode commands definition +SUPPRESS_COMMANDS = [] # These commands are ignored by commenting them out +COMMAND_SPACE = " " +# Global variables storing current position +CURRENT_X = 0 +CURRENT_Y = 0 +CURRENT_Z = 0 + + +def processArguments(argstring): + + global OUTPUT_HEADER + global OUTPUT_COMMENTS + global OUTPUT_LINE_NUMBERS + global SHOW_EDITOR + global PRECISION + global PREAMBLE + global POSTAMBLE + global UNITS + global UNIT_SPEED_FORMAT + global UNIT_FORMAT + global TRANSLATE_DRILL_CYCLES + global OUTPUT_TOOL_CHANGE + global SPINDLE_WAIT + global RETURN_TO + global OUTPUT_BCNC + + try: + args = parser.parse_args(shlex.split(argstring)) + if args.no_header: + OUTPUT_HEADER = False + if args.header: + OUTPUT_HEADER = True + if args.no_comments: + OUTPUT_COMMENTS = False + if args.comments: + OUTPUT_COMMENTS = True + if args.no_line_numbers: + OUTPUT_LINE_NUMBERS = False + if args.line_numbers: + OUTPUT_LINE_NUMBERS = True + if args.no_show_editor: + SHOW_EDITOR = False + if args.show_editor: + SHOW_EDITOR = True + PRECISION = args.precision + if args.preamble is not None: + PREAMBLE = args.preamble.replace("\\n", "\n") + if args.postamble is not None: + POSTAMBLE = args.postamble.replace("\\n", "\n") + if args.no_translate_drill: + TRANSLATE_DRILL_CYCLES = False + if args.translate_drill: + TRANSLATE_DRILL_CYCLES = True + if args.inches: + UNITS = "G20" + UNIT_SPEED_FORMAT = "in/min" + UNIT_FORMAT = "in" + PRECISION = 4 + if args.tool_change: + OUTPUT_TOOL_CHANGE = True + if args.wait_for_spindle > 0: + SPINDLE_WAIT = args.wait_for_spindle + if args.return_to != "": + RETURN_TO = [int(v) for v in args.return_to.split(",")] + if len(RETURN_TO) != 2: + RETURN_TO = None + print("--return-to coordinates must be specified as ,, ignoring") + if args.bcnc: + OUTPUT_BCNC = True + if args.no_bcnc: + OUTPUT_BCNC = False + + except Exception as e: + return False + + return True + + +# For debug... +def dump(obj): + for attr in dir(obj): + print("obj.%s = %s" % (attr, getattr(obj, attr))) + + +def export(objectslist, filename, argstring): + + if not processArguments(argstring): + return None + + global UNITS + global UNIT_FORMAT + global UNIT_SPEED_FORMAT + global MOTION_MODE + global SUPPRESS_COMMANDS + + print("Post Processor: " + __name__ + " postprocessing...") + gcode = "" + + # write header + if OUTPUT_HEADER: + gcode += linenumber() + "(Exported by FreeCAD)\n" + gcode += linenumber() + "(Post Processor: " + __name__ + ")\n" + gcode += linenumber() + "(Output Time:" + str(datetime.datetime.now()) + ")\n" + + # Check canned cycles for drilling + if TRANSLATE_DRILL_CYCLES: + if len(SUPPRESS_COMMANDS) == 0: + SUPPRESS_COMMANDS = ["G99", "G98", "G80"] + else: + SUPPRESS_COMMANDS += ["G99", "G98", "G80"] + + # Write the preamble + if OUTPUT_COMMENTS: + gcode += linenumber() + "(Begin preamble)\n" + for line in PREAMBLE.splitlines(): + gcode += linenumber() + line + "\n" + # verify if PREAMBLE have changed MOTION_MODE or UNITS + if "G90" in PREAMBLE: + MOTION_MODE = "G90" + elif "G91" in PREAMBLE: + MOTION_MODE = "G91" + else: + gcode += linenumber() + MOTION_MODE + "\n" + if "G21" in PREAMBLE: + UNITS = "G21" + UNIT_FORMAT = "mm" + UNIT_SPEED_FORMAT = "mm/min" + elif "G20" in PREAMBLE: + UNITS = "G20" + UNIT_FORMAT = "in" + UNIT_SPEED_FORMAT = "in/min" + else: + gcode += linenumber() + UNITS + "\n" + + for obj in objectslist: + # Debug... + # print("\n" + "*"*70) + # dump(obj) + # print("*"*70 + "\n") + if not hasattr(obj, "Path"): + print( + "The object " + obj.Name + " is not a path. Please select only path and Compounds." + ) + return + + # Skip inactive operations + if not PathUtil.activeForOp(obj): + continue + + # do the pre_op + if OUTPUT_BCNC: + gcode += linenumber() + "(Block-name: " + obj.Label + ")\n" + gcode += linenumber() + "(Block-expand: 0)\n" + gcode += linenumber() + "(Block-enable: 1)\n" + if OUTPUT_COMMENTS: + gcode += linenumber() + "(Begin operation: " + obj.Label + ")\n" + for line in PRE_OPERATION.splitlines(True): + gcode += linenumber() + line + + # get coolant mode + coolantMode = PathUtil.coolantModeForOp(obj) + + # turn coolant on if required + if OUTPUT_COMMENTS: + if not coolantMode == "None": + gcode += linenumber() + "(Coolant On:" + coolantMode + ")\n" + if coolantMode == "Flood": + gcode += linenumber() + "M8" + "\n" + if coolantMode == "Mist": + gcode += linenumber() + "M7" + "\n" + + # Parse the op + gcode += parse(obj) + + # do the post_op + if OUTPUT_COMMENTS: + gcode += linenumber() + "(Finish operation: " + obj.Label + ")\n" + for line in POST_OPERATION.splitlines(True): + gcode += linenumber() + line + + # turn coolant off if required + if not coolantMode == "None": + if OUTPUT_COMMENTS: + gcode += linenumber() + "(Coolant Off:" + coolantMode + ")\n" + gcode += linenumber() + "M9" + "\n" + + if RETURN_TO: + gcode += linenumber() + "G0 X%s Y%s\n" % tuple(RETURN_TO) + + # do the post_amble + if OUTPUT_BCNC: + gcode += linenumber() + "(Block-name: post_amble)\n" + gcode += linenumber() + "(Block-expand: 0)\n" + gcode += linenumber() + "(Block-enable: 1)\n" + if OUTPUT_COMMENTS: + gcode += linenumber() + "(Begin postamble)\n" + for line in POSTAMBLE.splitlines(): + gcode += linenumber() + line + "\n" + + # show the gCode result dialog + if FreeCAD.GuiUp and SHOW_EDITOR: + dia = PostUtils.GCodeEditorDialog() + dia.editor.setText(gcode) + result = dia.exec_() + if result: + final = dia.editor.toPlainText() + else: + final = gcode + else: + final = gcode + + print("Done postprocessing.") + + # write the file + if filename != "-": + with pyopen(filename, "w") as gfile: + gfile.write(final) + + return final + + +def linenumber(): + if not OUTPUT_LINE_NUMBERS: + return "" + global LINENR + global LINEINCR + s = "N" + str(LINENR) + " " + LINENR += LINEINCR + return s + + +def format_outstring(strTable): + global COMMAND_SPACE + # construct the line for the final output + s = "" + for w in strTable: + s += w + COMMAND_SPACE + s = s.strip() + return s + + +def parse(pathobj): + + global DRILL_RETRACT_MODE + global MOTION_MODE + global CURRENT_X + global CURRENT_Y + global CURRENT_Z + + out = "" + lastcommand = None + precision_string = "." + str(PRECISION) + "f" + + params = [ + "X", + "Y", + "Z", + "A", + "B", + "C", + "U", + "V", + "W", + "I", + "J", + "K", + "F", + "S", + "T", + "Q", + "R", + "L", + "P", + ] + + if hasattr(pathobj, "Group"): # We have a compound or project. + if OUTPUT_COMMENTS: + out += linenumber() + "(Compound: " + pathobj.Label + ")\n" + for p in pathobj.Group: + out += parse(p) + return out + + else: # parsing simple path + if not hasattr(pathobj, "Path"): # groups might contain non-path things like stock. + return out + + if OUTPUT_COMMENTS: + out += linenumber() + "(Path: " + pathobj.Label + ")\n" + + for c in PathUtils.getPathWithPlacement(pathobj).Commands: + outstring = [] + command = c.Name + + outstring.append(command) + + # if modal: only print the command if it is not the same as the last one + if MODAL: + if command == lastcommand: + outstring.pop(0) + + # Now add the remaining parameters in order + for param in params: + if param in c.Parameters: + if param == "F": + if command not in RAPID_MOVES: + speed = Units.Quantity(c.Parameters["F"], FreeCAD.Units.Velocity) + if speed.getValueAs(UNIT_SPEED_FORMAT) > 0.0: + outstring.append( + param + + format( + float(speed.getValueAs(UNIT_SPEED_FORMAT)), + precision_string, + ) + ) + elif param in ["T", "H", "S"]: + outstring.append(param + str(int(c.Parameters[param]))) + elif param in ["D", "P", "L"]: + outstring.append(param + str(c.Parameters[param])) + elif param in ["A", "B", "C"]: + outstring.append(param + format(c.Parameters[param], precision_string)) + else: # [X, Y, Z, U, V, W, I, J, K, R, Q] (Conversion eventuelle mm/inches) + pos = Units.Quantity(c.Parameters[param], FreeCAD.Units.Length) + outstring.append( + param + format(float(pos.getValueAs(UNIT_FORMAT)), precision_string) + ) + + # store the latest command + lastcommand = command + + # Memorizes the current position for calculating the related movements and the withdrawal plan + if command in MOTION_COMMANDS: + if "X" in c.Parameters: + CURRENT_X = Units.Quantity(c.Parameters["X"], FreeCAD.Units.Length) + if "Y" in c.Parameters: + CURRENT_Y = Units.Quantity(c.Parameters["Y"], FreeCAD.Units.Length) + if "Z" in c.Parameters: + CURRENT_Z = Units.Quantity(c.Parameters["Z"], FreeCAD.Units.Length) + + if command in ("G98", "G99"): + DRILL_RETRACT_MODE = command + + if command in ("G90", "G91"): + MOTION_MODE = command + + if TRANSLATE_DRILL_CYCLES: + if command in ("G81", "G82", "G83"): + out += drill_translate(outstring, command, c.Parameters) + # Erase the line we just translated + outstring = [] + + if SPINDLE_WAIT > 0: + if command in ("M3", "M03", "M4", "M04"): + out += linenumber() + format_outstring(outstring) + "\n" + out += linenumber() + format_outstring(["G4", "P%s" % SPINDLE_WAIT]) + "\n" + outstring = [] + + # Check for Tool Change: + if command in ("M6", "M06"): + if OUTPUT_COMMENTS: + out += linenumber() + "(Begin toolchange)\n" + if not OUTPUT_TOOL_CHANGE: + outstring.insert(0, "(") + outstring.append(")") + else: + for line in TOOL_CHANGE.splitlines(True): + out += linenumber() + line + + if command == "message": + if OUTPUT_COMMENTS is False: + out = [] + else: + outstring.pop(0) # remove the command + + if command in SUPPRESS_COMMANDS: + outstring.insert(0, "(") + outstring.append(")") + + # prepend a line number and append a newline + if len(outstring) >= 1: + out += linenumber() + format_outstring(outstring) + "\n" + + # Check for comments containing machine-specific commands to pass literally to the controller + m = re.match(r"^\(MC_RUN_COMMAND: ([^)]+)\)$", command) + if m: + raw_command = m.group(1) + out += linenumber() + raw_command + "\n" + + return out + + +def drill_translate(outstring, cmd, params): + global DRILL_RETRACT_MODE + global MOTION_MODE + global CURRENT_X + global CURRENT_Y + global CURRENT_Z + global UNITS + global UNIT_FORMAT + global UNIT_SPEED_FORMAT + + strFormat = "." + str(PRECISION) + "f" + trBuff = "" + + if OUTPUT_COMMENTS: # Comment the original command + outstring[0] = "(" + outstring[0] + outstring[-1] = outstring[-1] + ")" + trBuff += linenumber() + format_outstring(outstring) + "\n" + + # cycle conversion + # currently only cycles in XY are provided (G17) + # other plains ZX (G18) and YZ (G19) are not dealt with : Z drilling only. + param_X = Units.Quantity(params["X"], FreeCAD.Units.Length) + param_Y = Units.Quantity(params["Y"], FreeCAD.Units.Length) + param_Z = Units.Quantity(params["Z"], FreeCAD.Units.Length) + param_R = Units.Quantity(params["R"], FreeCAD.Units.Length) + # R less than Z is error + if param_R < param_Z: + trBuff += linenumber() + "(drill cycle error: R less than Z )\n" + return trBuff + + if MOTION_MODE == "G91": # G91 relative movements, (not generated by CAM WB drilling) + param_X += CURRENT_X + param_Y += CURRENT_Y + param_Z += CURRENT_Z + param_R += param_Z + + # NIST-RS274 + # 3.5.20 Set Canned Cycle Return Level — G98 and G99 + # When the spindle retracts during canned cycles, there is a choice of how far it retracts: (1) retract + # perpendicular to the selected plane to the position indicated by the R word, or (2) retract + # perpendicular to the selected plane to the position that axis was in just before the canned cycle + # started (unless that position is lower than the position indicated by the R word, in which case use + # the R word position). + # To use option (1), program G99. To use option (2), program G98. Remember that the R word has + # different meanings in absolute distance mode and incremental distance mode. + # """ + + if DRILL_RETRACT_MODE == "G99": + clear_Z = param_R + if DRILL_RETRACT_MODE == "G98" and CURRENT_Z >= param_R: + clear_Z = CURRENT_Z + else: + clear_Z = param_R + + strG0_clear_Z = "G0 Z" + format(float(clear_Z.getValueAs(UNIT_FORMAT)), strFormat) + "\n" + strG0_param_R = "G0 Z" + format(float(param_R.getValueAs(UNIT_FORMAT)), strFormat) + "\n" + + # get the other parameters + drill_feedrate = Units.Quantity(params["F"], FreeCAD.Units.Velocity) + strF_Feedrate = " F" + format(float(drill_feedrate.getValueAs(UNIT_SPEED_FORMAT)), ".2f") + "\n" + + if cmd == "G83": + drill_Step = Units.Quantity(params["Q"], FreeCAD.Units.Length) + a_bit = ( + drill_Step * 0.05 + ) # NIST 3.5.16.4 G83 Cycle: "current hole bottom, backed off a bit." + elif cmd == "G82": + drill_DwellTime = params["P"] + + # wrap this block to ensure machine MOTION_MODE is restored in case of error + try: + if MOTION_MODE == "G91": + trBuff += linenumber() + "G90\n" # force absolute coordinates during cycles + + # NIST-RS274 + # 3.5.16.1 Preliminary and In-Between Motion + # At the very beginning of the execution of any of the canned cycles, with the XY-plane selected, if + # the current Z position is below the R position, the Z-axis is traversed to the R position. This + # happens only once, regardless of the value of L. + # In addition, at the beginning of the first cycle and each repeat, the following one or two moves are + # made: + # 1. a straight traverse parallel to the XY-plane to the given XY-position, + # 2. a straight traverse of the Z-axis only to the R position, if it is not already at the R position. + + if CURRENT_Z < param_R: + trBuff += linenumber() + strG0_param_R + trBuff += ( + linenumber() + + "G0 X" + + format(float(param_X.getValueAs(UNIT_FORMAT)), strFormat) + + " Y" + + format(float(param_Y.getValueAs(UNIT_FORMAT)), strFormat) + + "\n" + ) + if CURRENT_Z > param_R: + trBuff += linenumber() + strG0_param_R + + last_Stop_Z = param_R + + # drill moves + if cmd in ("G81", "G82"): + trBuff += ( + linenumber() + + "G1 Z" + + format(float(param_Z.getValueAs(UNIT_FORMAT)), strFormat) + + strF_Feedrate + ) + # pause where applicable + if cmd == "G82": + trBuff += linenumber() + "G4 P" + str(drill_DwellTime) + "\n" + trBuff += linenumber() + strG0_clear_Z + else: # 'G83' + if params["Q"] != 0: + while 1: + if last_Stop_Z != clear_Z: + clearance_depth = ( + last_Stop_Z + a_bit + ) # rapid move to just short of last drilling depth + trBuff += ( + linenumber() + + "G0 Z" + + format( + float(clearance_depth.getValueAs(UNIT_FORMAT)), + strFormat, + ) + + "\n" + ) + next_Stop_Z = last_Stop_Z - drill_Step + if next_Stop_Z > param_Z: + trBuff += ( + linenumber() + + "G1 Z" + + format(float(next_Stop_Z.getValueAs(UNIT_FORMAT)), strFormat) + + strF_Feedrate + ) + trBuff += linenumber() + strG0_clear_Z + last_Stop_Z = next_Stop_Z + else: + trBuff += ( + linenumber() + + "G1 Z" + + format(float(param_Z.getValueAs(UNIT_FORMAT)), strFormat) + + strF_Feedrate + ) + trBuff += linenumber() + strG0_clear_Z + break + + except Exception as e: + pass + + if MOTION_MODE == "G91": + trBuff += linenumber() + "G91" # Restore if changed + + return trBuff + + +# print(__name__ + ": GCode postprocessor loaded.") diff --git a/src/Mod/CAM/Path/Post/scripts/grbl_post.py b/src/Mod/CAM/Path/Post/scripts/grbl_post.py index 84c4009529..d1af303e0b 100644 --- a/src/Mod/CAM/Path/Post/scripts/grbl_post.py +++ b/src/Mod/CAM/Path/Post/scripts/grbl_post.py @@ -2,8 +2,8 @@ # *************************************************************************** # * Copyright (c) 2014 sliptonic * -# * Copyright (c) 2018, 2019 Gauthier Briere * -# * Copyright (c) 2019, 2020 Schildkroet * +# * Copyright (c) 2022 - 2025 Larry Woestman * +# * Copyright (c) 2024 Ondsel * # * * # * This file is part of the FreeCAD CAx development system. * # * * @@ -25,695 +25,165 @@ # * * # *************************************************************************** -import FreeCAD -from FreeCAD import Units -import Path -import Path.Base.Util as PathUtil -import Path.Post.Utils as PostUtils -import PathScripts.PathUtils as PathUtils import argparse -import datetime -import shlex -import re -from builtins import open as pyopen - -TOOLTIP = """ -Generate g-code from a Path that is compatible with the grbl controller. -import grbl_post -grbl_post.export(object, "/path/to/file.ncc") -""" - - -# *************************************************************************** -# * Globals set customization preferences -# *************************************************************************** - -# Default values for command line arguments: -OUTPUT_COMMENTS = True # default output of comments in output gCode file -OUTPUT_HEADER = True # default output header in output gCode file -OUTPUT_LINE_NUMBERS = False # default doesn't output line numbers in output gCode file -OUTPUT_BCNC = False # default doesn't add bCNC operation block headers in output gCode file -SHOW_EDITOR = True # default show the resulting file dialog output in GUI -PRECISION = 3 # Default precision for metric (see http://linuxcnc.org/docs/2.7/html/gcode/overview.html#_g_code_best_practices) -TRANSLATE_DRILL_CYCLES = True # If true, G81, G82 & G83 are translated in G0/G1 moves -PREAMBLE = """G17 G90 -""" # default preamble text will appear at the beginning of the gCode output file. -POSTAMBLE = """M5 -G17 G90 -M2 -""" # default postamble text will appear following the last operation. - -SPINDLE_WAIT = 0 # no waiting after M3 / M4 by default -RETURN_TO = None # no movements after end of program - -# Customisation with no command line argument -MODAL = False # if true commands are suppressed if the same as previous line. -LINENR = 100 # line number starting value -LINEINCR = 10 # line number increment -OUTPUT_TOOL_CHANGE = ( - False # default don't output M6 tool changes (comment it) as grbl currently does not handle it -) -DRILL_RETRACT_MODE = ( - "G98" # Default value of drill retractations (CURRENT_Z) other possible value is G99 -) -MOTION_MODE = "G90" # G90 for absolute moves, G91 for relative -UNITS = "G21" # G21 for metric, G20 for us standard -UNIT_FORMAT = "mm" -UNIT_SPEED_FORMAT = "mm/min" -PRE_OPERATION = """""" # Pre operation text will be inserted before every operation -POST_OPERATION = """""" # Post operation text will be inserted after every operation -TOOL_CHANGE = """""" # Tool Change commands will be inserted before a tool change - -# *************************************************************************** -# * End of customization -# *************************************************************************** - -# Parser arguments list & definition -parser = argparse.ArgumentParser(prog="grbl", add_help=False) -parser.add_argument("--comments", action="store_true", help="output comment (default)") -parser.add_argument("--no-comments", action="store_true", help="suppress comment output") -parser.add_argument("--header", action="store_true", help="output headers (default)") -parser.add_argument("--no-header", action="store_true", help="suppress header output") -parser.add_argument("--line-numbers", action="store_true", help="prefix with line numbers") -parser.add_argument( - "--no-line-numbers", - action="store_true", - help="don't prefix with line numbers (default)", -) -parser.add_argument( - "--show-editor", - action="store_true", - help="pop up editor before writing output (default)", -) -parser.add_argument( - "--no-show-editor", - action="store_true", - help="don't pop up editor before writing output", -) -parser.add_argument("--precision", default="3", help="number of digits of precision, default=3") -parser.add_argument( - "--translate_drill", - action="store_true", - help="translate drill cycles G81, G82 & G83 in G0/G1 movements", -) -parser.add_argument( - "--no-translate_drill", - action="store_true", - help="don't translate drill cycles G81, G82 & G83 in G0/G1 movements (default)", -) -parser.add_argument( - "--preamble", - help='set commands to be issued before the first command, default="G17 G90\\n"', -) -parser.add_argument( - "--postamble", - help='set commands to be issued after the last command, default="M5\\nG17 G90\\nM2\\n"', -) -parser.add_argument( - "--inches", action="store_true", help="Convert output for US imperial mode (G20)" -) -parser.add_argument("--tool-change", action="store_true", help="Insert M6 for all tool changes") -parser.add_argument( - "--wait-for-spindle", - type=int, - default=0, - help="Wait for spindle to reach desired speed after M3 / M4, default=0", -) -parser.add_argument( - "--return-to", - default="", - help="Move to the specified coordinates at the end, e.g. --return-to=0,0", -) -parser.add_argument( - "--bcnc", - action="store_true", - help="Add Job operations as bCNC block headers. Consider suppressing existing comments: Add argument --no-comments", -) -parser.add_argument( - "--no-bcnc", action="store_true", help="suppress bCNC block header output (default)" -) -TOOLTIP_ARGS = parser.format_help() - - -# *************************************************************************** -# * Internal global variables -# *************************************************************************** -MOTION_COMMANDS = [ - "G0", - "G00", - "G1", - "G01", - "G2", - "G02", - "G3", - "G03", -] # Motion gCode commands definition -RAPID_MOVES = ["G0", "G00"] # Rapid moves gCode commands definition -SUPPRESS_COMMANDS = [] # These commands are ignored by commenting them out -COMMAND_SPACE = " " -# Global variables storing current position -CURRENT_X = 0 -CURRENT_Y = 0 -CURRENT_Z = 0 - - -def processArguments(argstring): - - global OUTPUT_HEADER - global OUTPUT_COMMENTS - global OUTPUT_LINE_NUMBERS - global SHOW_EDITOR - global PRECISION - global PREAMBLE - global POSTAMBLE - global UNITS - global UNIT_SPEED_FORMAT - global UNIT_FORMAT - global TRANSLATE_DRILL_CYCLES - global OUTPUT_TOOL_CHANGE - global SPINDLE_WAIT - global RETURN_TO - global OUTPUT_BCNC - - try: - args = parser.parse_args(shlex.split(argstring)) - if args.no_header: - OUTPUT_HEADER = False - if args.header: - OUTPUT_HEADER = True - if args.no_comments: - OUTPUT_COMMENTS = False - if args.comments: - OUTPUT_COMMENTS = True - if args.no_line_numbers: - OUTPUT_LINE_NUMBERS = False - if args.line_numbers: - OUTPUT_LINE_NUMBERS = True - if args.no_show_editor: - SHOW_EDITOR = False - if args.show_editor: - SHOW_EDITOR = True - PRECISION = args.precision - if args.preamble is not None: - PREAMBLE = args.preamble.replace("\\n", "\n") - if args.postamble is not None: - POSTAMBLE = args.postamble.replace("\\n", "\n") - if args.no_translate_drill: - TRANSLATE_DRILL_CYCLES = False - if args.translate_drill: - TRANSLATE_DRILL_CYCLES = True - if args.inches: - UNITS = "G20" - UNIT_SPEED_FORMAT = "in/min" - UNIT_FORMAT = "in" - PRECISION = 4 - if args.tool_change: - OUTPUT_TOOL_CHANGE = True - if args.wait_for_spindle > 0: - SPINDLE_WAIT = args.wait_for_spindle - if args.return_to != "": - RETURN_TO = [int(v) for v in args.return_to.split(",")] - if len(RETURN_TO) != 2: - RETURN_TO = None - print("--return-to coordinates must be specified as ,, ignoring") - if args.bcnc: - OUTPUT_BCNC = True - if args.no_bcnc: - OUTPUT_BCNC = False - - except Exception as e: - return False - - return True - - -# For debug... -def dump(obj): - for attr in dir(obj): - print("obj.%s = %s" % (attr, getattr(obj, attr))) - - -def export(objectslist, filename, argstring): - - if not processArguments(argstring): - return None - - global UNITS - global UNIT_FORMAT - global UNIT_SPEED_FORMAT - global MOTION_MODE - global SUPPRESS_COMMANDS - - print("Post Processor: " + __name__ + " postprocessing...") - gcode = "" - - # write header - if OUTPUT_HEADER: - gcode += linenumber() + "(Exported by FreeCAD)\n" - gcode += linenumber() + "(Post Processor: " + __name__ + ")\n" - gcode += linenumber() + "(Output Time:" + str(datetime.datetime.now()) + ")\n" - - # Check canned cycles for drilling - if TRANSLATE_DRILL_CYCLES: - if len(SUPPRESS_COMMANDS) == 0: - SUPPRESS_COMMANDS = ["G99", "G98", "G80"] - else: - SUPPRESS_COMMANDS += ["G99", "G98", "G80"] - - # Write the preamble - if OUTPUT_COMMENTS: - gcode += linenumber() + "(Begin preamble)\n" - for line in PREAMBLE.splitlines(): - gcode += linenumber() + line + "\n" - # verify if PREAMBLE have changed MOTION_MODE or UNITS - if "G90" in PREAMBLE: - MOTION_MODE = "G90" - elif "G91" in PREAMBLE: - MOTION_MODE = "G91" - else: - gcode += linenumber() + MOTION_MODE + "\n" - if "G21" in PREAMBLE: - UNITS = "G21" - UNIT_FORMAT = "mm" - UNIT_SPEED_FORMAT = "mm/min" - elif "G20" in PREAMBLE: - UNITS = "G20" - UNIT_FORMAT = "in" - UNIT_SPEED_FORMAT = "in/min" - else: - gcode += linenumber() + UNITS + "\n" - - for obj in objectslist: - # Debug... - # print("\n" + "*"*70) - # dump(obj) - # print("*"*70 + "\n") - if not hasattr(obj, "Path"): - print( - "The object " + obj.Name + " is not a path. Please select only path and Compounds." - ) - return - - # Skip inactive operations - if not PathUtil.activeForOp(obj): - continue - - # do the pre_op - if OUTPUT_BCNC: - gcode += linenumber() + "(Block-name: " + obj.Label + ")\n" - gcode += linenumber() + "(Block-expand: 0)\n" - gcode += linenumber() + "(Block-enable: 1)\n" - if OUTPUT_COMMENTS: - gcode += linenumber() + "(Begin operation: " + obj.Label + ")\n" - for line in PRE_OPERATION.splitlines(True): - gcode += linenumber() + line - - # get coolant mode - coolantMode = PathUtil.coolantModeForOp(obj) - - # turn coolant on if required - if OUTPUT_COMMENTS: - if not coolantMode == "None": - gcode += linenumber() + "(Coolant On:" + coolantMode + ")\n" - if coolantMode == "Flood": - gcode += linenumber() + "M8" + "\n" - if coolantMode == "Mist": - gcode += linenumber() + "M7" + "\n" - - # Parse the op - gcode += parse(obj) - - # do the post_op - if OUTPUT_COMMENTS: - gcode += linenumber() + "(Finish operation: " + obj.Label + ")\n" - for line in POST_OPERATION.splitlines(True): - gcode += linenumber() + line - - # turn coolant off if required - if not coolantMode == "None": - if OUTPUT_COMMENTS: - gcode += linenumber() + "(Coolant Off:" + coolantMode + ")\n" - gcode += linenumber() + "M9" + "\n" - - if RETURN_TO: - gcode += linenumber() + "G0 X%s Y%s\n" % tuple(RETURN_TO) - - # do the post_amble - if OUTPUT_BCNC: - gcode += linenumber() + "(Block-name: post_amble)\n" - gcode += linenumber() + "(Block-expand: 0)\n" - gcode += linenumber() + "(Block-enable: 1)\n" - if OUTPUT_COMMENTS: - gcode += linenumber() + "(Begin postamble)\n" - for line in POSTAMBLE.splitlines(): - gcode += linenumber() + line + "\n" - - # show the gCode result dialog - if FreeCAD.GuiUp and SHOW_EDITOR: - dia = PostUtils.GCodeEditorDialog() - dia.editor.setText(gcode) - result = dia.exec_() - if result: - final = dia.editor.toPlainText() - else: - final = gcode - else: - final = gcode - - print("Done postprocessing.") - - # write the file - if filename != "-": - with pyopen(filename, "w") as gfile: - gfile.write(final) - - return final - - -def linenumber(): - if not OUTPUT_LINE_NUMBERS: - return "" - global LINENR - global LINEINCR - s = "N" + str(LINENR) + " " - LINENR += LINEINCR - return s - - -def format_outstring(strTable): - global COMMAND_SPACE - # construct the line for the final output - s = "" - for w in strTable: - s += w + COMMAND_SPACE - s = s.strip() - return s - - -def parse(pathobj): - - global DRILL_RETRACT_MODE - global MOTION_MODE - global CURRENT_X - global CURRENT_Y - global CURRENT_Z - - out = "" - lastcommand = None - precision_string = "." + str(PRECISION) + "f" - - params = [ - "X", - "Y", - "Z", - "A", - "B", - "C", - "U", - "V", - "W", - "I", - "J", - "K", - "F", - "S", - "T", - "Q", - "R", - "L", - "P", - ] - - if hasattr(pathobj, "Group"): # We have a compound or project. - if OUTPUT_COMMENTS: - out += linenumber() + "(Compound: " + pathobj.Label + ")\n" - for p in pathobj.Group: - out += parse(p) - return out - - else: # parsing simple path - if not hasattr(pathobj, "Path"): # groups might contain non-path things like stock. - return out - - if OUTPUT_COMMENTS: - out += linenumber() + "(Path: " + pathobj.Label + ")\n" - - for c in PathUtils.getPathWithPlacement(pathobj).Commands: - outstring = [] - command = c.Name - - outstring.append(command) - - # if modal: only print the command if it is not the same as the last one - if MODAL: - if command == lastcommand: - outstring.pop(0) - - # Now add the remaining parameters in order - for param in params: - if param in c.Parameters: - if param == "F": - if command not in RAPID_MOVES: - speed = Units.Quantity(c.Parameters["F"], FreeCAD.Units.Velocity) - if speed.getValueAs(UNIT_SPEED_FORMAT) > 0.0: - outstring.append( - param - + format( - float(speed.getValueAs(UNIT_SPEED_FORMAT)), - precision_string, - ) - ) - elif param in ["T", "H", "S"]: - outstring.append(param + str(int(c.Parameters[param]))) - elif param in ["D", "P", "L"]: - outstring.append(param + str(c.Parameters[param])) - elif param in ["A", "B", "C"]: - outstring.append(param + format(c.Parameters[param], precision_string)) - else: # [X, Y, Z, U, V, W, I, J, K, R, Q] (Conversion eventuelle mm/inches) - pos = Units.Quantity(c.Parameters[param], FreeCAD.Units.Length) - outstring.append( - param + format(float(pos.getValueAs(UNIT_FORMAT)), precision_string) - ) - - # store the latest command - lastcommand = command - - # Memorizes the current position for calculating the related movements and the withdrawal plan - if command in MOTION_COMMANDS: - if "X" in c.Parameters: - CURRENT_X = Units.Quantity(c.Parameters["X"], FreeCAD.Units.Length) - if "Y" in c.Parameters: - CURRENT_Y = Units.Quantity(c.Parameters["Y"], FreeCAD.Units.Length) - if "Z" in c.Parameters: - CURRENT_Z = Units.Quantity(c.Parameters["Z"], FreeCAD.Units.Length) - - if command in ("G98", "G99"): - DRILL_RETRACT_MODE = command - - if command in ("G90", "G91"): - MOTION_MODE = command - - if TRANSLATE_DRILL_CYCLES: - if command in ("G81", "G82", "G83"): - out += drill_translate(outstring, command, c.Parameters) - # Erase the line we just translated - outstring = [] - - if SPINDLE_WAIT > 0: - if command in ("M3", "M03", "M4", "M04"): - out += linenumber() + format_outstring(outstring) + "\n" - out += linenumber() + format_outstring(["G4", "P%s" % SPINDLE_WAIT]) + "\n" - outstring = [] - - # Check for Tool Change: - if command in ("M6", "M06"): - if OUTPUT_COMMENTS: - out += linenumber() + "(Begin toolchange)\n" - if not OUTPUT_TOOL_CHANGE: - outstring.insert(0, "(") - outstring.append(")") - else: - for line in TOOL_CHANGE.splitlines(True): - out += linenumber() + line - - if command == "message": - if OUTPUT_COMMENTS is False: - out = [] - else: - outstring.pop(0) # remove the command - - if command in SUPPRESS_COMMANDS: - outstring.insert(0, "(") - outstring.append(")") - - # prepend a line number and append a newline - if len(outstring) >= 1: - out += linenumber() + format_outstring(outstring) + "\n" - - # Check for comments containing machine-specific commands to pass literally to the controller - m = re.match(r"^\(MC_RUN_COMMAND: ([^)]+)\)$", command) - if m: - raw_command = m.group(1) - out += linenumber() + raw_command + "\n" - - return out - - -def drill_translate(outstring, cmd, params): - global DRILL_RETRACT_MODE - global MOTION_MODE - global CURRENT_X - global CURRENT_Y - global CURRENT_Z - global UNITS - global UNIT_FORMAT - global UNIT_SPEED_FORMAT - - strFormat = "." + str(PRECISION) + "f" - trBuff = "" - - if OUTPUT_COMMENTS: # Comment the original command - outstring[0] = "(" + outstring[0] - outstring[-1] = outstring[-1] + ")" - trBuff += linenumber() + format_outstring(outstring) + "\n" - - # cycle conversion - # currently only cycles in XY are provided (G17) - # other plains ZX (G18) and YZ (G19) are not dealt with : Z drilling only. - param_X = Units.Quantity(params["X"], FreeCAD.Units.Length) - param_Y = Units.Quantity(params["Y"], FreeCAD.Units.Length) - param_Z = Units.Quantity(params["Z"], FreeCAD.Units.Length) - param_R = Units.Quantity(params["R"], FreeCAD.Units.Length) - # R less than Z is error - if param_R < param_Z: - trBuff += linenumber() + "(drill cycle error: R less than Z )\n" - return trBuff - - if MOTION_MODE == "G91": # G91 relative movements, (not generated by CAM WB drilling) - param_X += CURRENT_X - param_Y += CURRENT_Y - param_Z += CURRENT_Z - param_R += param_Z - - # NIST-RS274 - # 3.5.20 Set Canned Cycle Return Level — G98 and G99 - # When the spindle retracts during canned cycles, there is a choice of how far it retracts: (1) retract - # perpendicular to the selected plane to the position indicated by the R word, or (2) retract - # perpendicular to the selected plane to the position that axis was in just before the canned cycle - # started (unless that position is lower than the position indicated by the R word, in which case use - # the R word position). - # To use option (1), program G99. To use option (2), program G98. Remember that the R word has - # different meanings in absolute distance mode and incremental distance mode. - # """ - - if DRILL_RETRACT_MODE == "G99": - clear_Z = param_R - if DRILL_RETRACT_MODE == "G98" and CURRENT_Z >= param_R: - clear_Z = CURRENT_Z - else: - clear_Z = param_R - - strG0_clear_Z = "G0 Z" + format(float(clear_Z.getValueAs(UNIT_FORMAT)), strFormat) + "\n" - strG0_param_R = "G0 Z" + format(float(param_R.getValueAs(UNIT_FORMAT)), strFormat) + "\n" - - # get the other parameters - drill_feedrate = Units.Quantity(params["F"], FreeCAD.Units.Velocity) - strF_Feedrate = " F" + format(float(drill_feedrate.getValueAs(UNIT_SPEED_FORMAT)), ".2f") + "\n" - - if cmd == "G83": - drill_Step = Units.Quantity(params["Q"], FreeCAD.Units.Length) - a_bit = ( - drill_Step * 0.05 - ) # NIST 3.5.16.4 G83 Cycle: "current hole bottom, backed off a bit." - elif cmd == "G82": - drill_DwellTime = params["P"] - - # wrap this block to ensure machine MOTION_MODE is restored in case of error - try: - if MOTION_MODE == "G91": - trBuff += linenumber() + "G90\n" # force absolute coordinates during cycles - - # NIST-RS274 - # 3.5.16.1 Preliminary and In-Between Motion - # At the very beginning of the execution of any of the canned cycles, with the XY-plane selected, if - # the current Z position is below the R position, the Z-axis is traversed to the R position. This - # happens only once, regardless of the value of L. - # In addition, at the beginning of the first cycle and each repeat, the following one or two moves are - # made: - # 1. a straight traverse parallel to the XY-plane to the given XY-position, - # 2. a straight traverse of the Z-axis only to the R position, if it is not already at the R position. - - if CURRENT_Z < param_R: - trBuff += linenumber() + strG0_param_R - trBuff += ( - linenumber() - + "G0 X" - + format(float(param_X.getValueAs(UNIT_FORMAT)), strFormat) - + " Y" - + format(float(param_Y.getValueAs(UNIT_FORMAT)), strFormat) - + "\n" +from typing import Any, Dict + +from Path.Post.Processor import PostProcessor + +import Path +import FreeCAD + +translate = FreeCAD.Qt.translate + +DEBUG = False +if DEBUG: + Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) + Path.Log.trackModule(Path.Log.thisModule()) +else: + Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule()) + +# +# Define some types that are used throughout this file. +# +Defaults = Dict[str, bool] +Values = Dict[str, Any] +Visible = Dict[str, bool] + + +class Grbl(PostProcessor): + """The Grbl post processor class.""" + + def __init__( + self, + job, + tooltip=translate("CAM", "Grbl post processor"), + tooltipargs=[""], + units="Metric", + ) -> None: + super().__init__( + job=job, + tooltip=tooltip, + tooltipargs=tooltipargs, + units=units, ) - if CURRENT_Z > param_R: - trBuff += linenumber() + strG0_param_R + Path.Log.debug("Grbl post processor initialized.") - last_Stop_Z = param_R + def init_values(self, values: Values) -> None: + """Initialize values that are used throughout the postprocessor.""" + # + super().init_values(values) + # + # Set any values here that need to override the default values set + # in the parent routine. + # + values["ENABLE_COOLANT"] = True + # + # If this is set to True, then commands that are placed in + # comments that look like (MC_RUN_COMMAND: blah) will be output. + # + values["ENABLE_MACHINE_SPECIFIC_COMMANDS"] = True + # + # Used in the argparser code as the "name" of the postprocessor program. + # This would normally show up in the usage message in the TOOLTIP_ARGS. + # + values["MACHINE_NAME"] = "Grbl" + # + # Default to outputting Path labels at the beginning of each Path. + # + values["OUTPUT_PATH_LABELS"] = True + # + # Default to not outputting M6 tool changes (comment it) as grbl + # currently does not handle it. + # + values["OUTPUT_TOOL_CHANGE"] = False + # + # The order of the parameters. + # Arcs may only work on the XY plane (this needs to be verified). + # + values["PARAMETER_ORDER"] = [ + "X", + "Y", + "Z", + "A", + "B", + "C", + "U", + "V", + "W", + "I", + "J", + "F", + "S", + "T", + "Q", + "R", + "L", + "P", + ] + # + # Any commands in this value will be output as the last commands in the G-code file. + # + values[ + "POSTAMBLE" + ] = """M5 +G17 G90 +M2""" + values["POSTPROCESSOR_FILE_NAME"] = __name__ + # + # Any commands in this value will be output after the header and + # safety block at the beginning of the G-code file. + # + values["PREAMBLE"] = """G17 G90""" + # + # Do not show the current machine units just before the PRE_OPERATION. + # + values["SHOW_MACHINE_UNITS"] = False + # + # Default to not outputting a G43 following tool changes + # + values["USE_TLO"] = False - # drill moves - if cmd in ("G81", "G82"): - trBuff += ( - linenumber() - + "G1 Z" - + format(float(param_Z.getValueAs(UNIT_FORMAT)), strFormat) - + strF_Feedrate - ) - # pause where applicable - if cmd == "G82": - trBuff += linenumber() + "G4 P" + str(drill_DwellTime) + "\n" - trBuff += linenumber() + strG0_clear_Z - else: # 'G83' - if params["Q"] != 0: - while 1: - if last_Stop_Z != clear_Z: - clearance_depth = ( - last_Stop_Z + a_bit - ) # rapid move to just short of last drilling depth - trBuff += ( - linenumber() - + "G0 Z" - + format( - float(clearance_depth.getValueAs(UNIT_FORMAT)), - strFormat, - ) - + "\n" - ) - next_Stop_Z = last_Stop_Z - drill_Step - if next_Stop_Z > param_Z: - trBuff += ( - linenumber() - + "G1 Z" - + format(float(next_Stop_Z.getValueAs(UNIT_FORMAT)), strFormat) - + strF_Feedrate - ) - trBuff += linenumber() + strG0_clear_Z - last_Stop_Z = next_Stop_Z - else: - trBuff += ( - linenumber() - + "G1 Z" - + format(float(param_Z.getValueAs(UNIT_FORMAT)), strFormat) - + strF_Feedrate - ) - trBuff += linenumber() + strG0_clear_Z - break + def init_argument_defaults(self, argument_defaults: Defaults) -> None: + """Initialize which arguments (in a pair) are shown as the default argument.""" + super().init_argument_defaults(argument_defaults) + # + # Modify which argument to show as the default in flag-type arguments here. + # If the value is True, the first argument will be shown as the default. + # If the value is False, the second argument will be shown as the default. + # + # For example, if you want to show Metric mode as the default, use: + # argument_defaults["metric_inch"] = True + # + # If you want to show that "Don't pop up editor for writing output" is + # the default, use: + # argument_defaults["show-editor"] = False. + # + # Note: You also need to modify the corresponding entries in the "values" hash + # to actually make the default value(s) change to match. + # + argument_defaults["tlo"] = False + argument_defaults["tool_change"] = False - except Exception as e: - pass + def init_arguments_visible(self, arguments_visible: Visible) -> None: + """Initialize which argument pairs are visible in TOOLTIP_ARGS.""" + super().init_arguments_visible(arguments_visible) + # + # Modify the visibility of any arguments from the defaults here. + # + arguments_visible["bcnc"] = True + arguments_visible["axis-modal"] = False + arguments_visible["return-to"] = True + arguments_visible["tlo"] = False + arguments_visible["tool_change"] = True + arguments_visible["translate_drill"] = True + arguments_visible["wait-for-spindle"] = True - if MOTION_MODE == "G91": - trBuff += linenumber() + "G91" # Restore if changed - - return trBuff - - -# print(__name__ + ": GCode postprocessor loaded.") + @property + def tooltip(self): + tooltip: str = """ + This is a postprocessor file for the CAM workbench. + It is used to take a pseudo-gcode fragment from a CAM object + and output 'real' GCode suitable for a Grbl 3 axis mill. + """ + return tooltip diff --git a/src/Mod/CAM/Path/Post/scripts/linuxcnc_legacy_post.py b/src/Mod/CAM/Path/Post/scripts/linuxcnc_legacy_post.py new file mode 100644 index 0000000000..acd2801c01 --- /dev/null +++ b/src/Mod/CAM/Path/Post/scripts/linuxcnc_legacy_post.py @@ -0,0 +1,461 @@ +# *************************************************************************** +# * Copyright (c) 2014 sliptonic * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * FreeCAD is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Lesser General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with FreeCAD; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +import FreeCAD +from FreeCAD import Units +import Path +import argparse +import datetime +import shlex +import Path.Base.Util as PathUtil +import Path.Post.Utils as PostUtils +import PathScripts.PathUtils as PathUtils +from builtins import open as pyopen + +TOOLTIP = """ +This is a postprocessor file for the Path workbench. It is used to +take a pseudo-G-code fragment outputted by a Path object, and output +real G-code suitable for a linuxcnc 3 axis mill. This postprocessor, once placed +in the appropriate PathScripts folder, can be used directly from inside +FreeCAD, via the GUI importer or via python scripts with: + +import linuxcnc_legacy_post +linuxcnc_legacy_post.export(object,"/path/to/file.ncc","") +""" + +now = datetime.datetime.now() + +parser = argparse.ArgumentParser(prog="linuxcnc", add_help=False) +parser.add_argument("--no-header", action="store_true", help="suppress header output") +parser.add_argument("--no-comments", action="store_true", help="suppress comment output") +parser.add_argument("--line-numbers", action="store_true", help="prefix with line numbers") +parser.add_argument( + "--no-show-editor", + action="store_true", + help="don't pop up editor before writing output", +) +parser.add_argument("--precision", default="3", help="number of digits of precision, default=3") +parser.add_argument( + "--preamble", + help='set commands to be issued before the first command, default="G17 G54 G40 G49 G80 G90\\n"', +) +parser.add_argument( + "--postamble", + help='set commands to be issued after the last command, default="M05\\nG17 G54 G90 G80 G40\\nM2\\n"', +) +parser.add_argument( + "--inches", action="store_true", help="Convert output for US imperial mode (G20)" +) +parser.add_argument( + "--modal", + action="store_true", + help="Output the Same G-command Name USE NonModal Mode", +) +parser.add_argument("--axis-modal", action="store_true", help="Output the Same Axis Value Mode") +parser.add_argument( + "--no-tlo", + action="store_true", + help="suppress tool length offset (G43) following tool changes", +) + +TOOLTIP_ARGS = parser.format_help() + +# These globals set common customization preferences +OUTPUT_COMMENTS = True +OUTPUT_HEADER = True +OUTPUT_LINE_NUMBERS = False +SHOW_EDITOR = True +MODAL = False # if true commands are suppressed if the same as previous line. +USE_TLO = True # if true G43 will be output following tool changes +OUTPUT_DOUBLES = True # if false duplicate axis values are suppressed if the same as previous line. +COMMAND_SPACE = " " +LINENR = 100 # line number starting value + +# These globals will be reflected in the Machine configuration of the project +UNITS = "G21" # G21 for metric, G20 for us standard +UNIT_SPEED_FORMAT = "mm/min" +UNIT_FORMAT = "mm" + +MACHINE_NAME = "LinuxCNC" +CORNER_MIN = {"x": 0, "y": 0, "z": 0} +CORNER_MAX = {"x": 500, "y": 300, "z": 300} +PRECISION = 3 + +# Preamble text will appear at the beginning of the GCODE output file. +PREAMBLE = """G17 G54 G40 G49 G80 G90 +""" + +# Postamble text will appear following the last operation. +POSTAMBLE = """M05 +G17 G54 G90 G80 G40 +M2 +""" + +# Pre operation text will be inserted before every operation +PRE_OPERATION = """""" + +# Post operation text will be inserted after every operation +POST_OPERATION = """""" + +# Tool Change commands will be inserted before a tool change +TOOL_CHANGE = """""" + + +def processArguments(argstring): + global OUTPUT_HEADER + global OUTPUT_COMMENTS + global OUTPUT_LINE_NUMBERS + global SHOW_EDITOR + global PRECISION + global PREAMBLE + global POSTAMBLE + global UNITS + global UNIT_SPEED_FORMAT + global UNIT_FORMAT + global MODAL + global USE_TLO + global OUTPUT_DOUBLES + + try: + args = parser.parse_args(shlex.split(argstring)) + if args.no_header: + OUTPUT_HEADER = False + if args.no_comments: + OUTPUT_COMMENTS = False + if args.line_numbers: + OUTPUT_LINE_NUMBERS = True + if args.no_show_editor: + SHOW_EDITOR = False + print("Show editor = %d" % SHOW_EDITOR) + PRECISION = args.precision + if args.preamble is not None: + PREAMBLE = args.preamble.replace("\\n", "\n") + if args.postamble is not None: + POSTAMBLE = args.postamble.replace("\\n", "\n") + if args.inches: + UNITS = "G20" + UNIT_SPEED_FORMAT = "in/min" + UNIT_FORMAT = "in" + PRECISION = 4 + if args.modal: + MODAL = True + if args.no_tlo: + USE_TLO = False + if args.axis_modal: + print("here") + OUTPUT_DOUBLES = False + + except Exception: + return False + + return True + + +def export(objectslist, filename, argstring): + if not processArguments(argstring): + return None + global UNITS + global UNIT_FORMAT + global UNIT_SPEED_FORMAT + + for obj in objectslist: + if not hasattr(obj, "Path"): + print( + "the object " + obj.Name + " is not a path. Please select only path and Compounds." + ) + return None + + print("postprocessing...") + gcode = "" + + # write header + if OUTPUT_HEADER: + gcode += linenumber() + "(Exported by FreeCAD)\n" + gcode += linenumber() + "(Post Processor: " + __name__ + ")\n" + gcode += linenumber() + "(Output Time:" + str(now) + ")\n" + + # Write the preamble + if OUTPUT_COMMENTS: + gcode += linenumber() + "(begin preamble)\n" + for line in PREAMBLE.splitlines(): + gcode += linenumber() + line + "\n" + gcode += linenumber() + UNITS + "\n" + + for obj in objectslist: + # Skip inactive operations + if not PathUtil.activeForOp(obj): + continue + + # do the pre_op + if OUTPUT_COMMENTS: + gcode += linenumber() + "(begin operation: %s)\n" % obj.Label + gcode += linenumber() + "(machine units: %s)\n" % (UNIT_SPEED_FORMAT) + for line in PRE_OPERATION.splitlines(True): + gcode += linenumber() + line + + # get coolant mode + coolantMode = PathUtil.coolantModeForOp(obj) + + # turn coolant on if required + if OUTPUT_COMMENTS: + if not coolantMode == "None": + gcode += linenumber() + "(Coolant On:" + coolantMode + ")\n" + if coolantMode == "Flood": + gcode += linenumber() + "M8" + "\n" + if coolantMode == "Mist": + gcode += linenumber() + "M7" + "\n" + + # process the operation gcode + gcode += parse(obj) + + # do the post_op + if OUTPUT_COMMENTS: + gcode += linenumber() + "(finish operation: %s)\n" % obj.Label + for line in POST_OPERATION.splitlines(True): + gcode += linenumber() + line + + # turn coolant off if required + if not coolantMode == "None": + if OUTPUT_COMMENTS: + gcode += linenumber() + "(Coolant Off:" + coolantMode + ")\n" + gcode += linenumber() + "M9" + "\n" + + # do the post_amble + if OUTPUT_COMMENTS: + gcode += "(begin postamble)\n" + for line in POSTAMBLE.splitlines(): + gcode += linenumber() + line + "\n" + + if FreeCAD.GuiUp and SHOW_EDITOR: + final = gcode + if len(gcode) > 100000: + print("Skipping editor since output is greater than 100kb") + else: + dia = PostUtils.GCodeEditorDialog() + dia.editor.setText(gcode) + result = dia.exec_() + if result: + final = dia.editor.toPlainText() + else: + final = gcode + + print("done postprocessing.") + + if not filename == "-": + gfile = pyopen(filename, "w") + gfile.write(final) + gfile.close() + + return final + + +def linenumber(): + global LINENR + if OUTPUT_LINE_NUMBERS is True: + LINENR += 10 + return "N" + str(LINENR) + " " + return "" + + +def parse(pathobj): + global PRECISION + global MODAL + global OUTPUT_DOUBLES + global UNIT_FORMAT + global UNIT_SPEED_FORMAT + + out = "" + lastcommand = None + precision_string = "." + str(PRECISION) + "f" + currLocation = {} # keep track for no doubles + + # the order of parameters + # linuxcnc doesn't want K properties on XY plane Arcs need work. + params = [ + "X", + "Y", + "Z", + "A", + "B", + "C", + "I", + "J", + "F", + "S", + "T", + "Q", + "R", + "L", + "H", + "D", + "P", + ] + firstmove = Path.Command("G0", {"X": -1, "Y": -1, "Z": -1, "F": 0.0}) + currLocation.update(firstmove.Parameters) # set First location Parameters + + if hasattr(pathobj, "Group"): # We have a compound or project. + # if OUTPUT_COMMENTS: + # out += linenumber() + "(compound: " + pathobj.Label + ")\n" + for p in pathobj.Group: + out += parse(p) + return out + else: # parsing simple path + # groups might contain non-path things like stock. + if not hasattr(pathobj, "Path"): + return out + + # if OUTPUT_COMMENTS: + # out += linenumber() + "(" + pathobj.Label + ")\n" + + # The following "for" statement was fairly recently added + # but seems to be using the A, B, and C parameters in ways + # that don't appear to be compatible with how the PATH code + # uses the A, B, and C parameters. I have reverted the + # change here until we can figure out what it going on. + # + # for c in PathUtils.getPathWithPlacement(pathobj).Commands: + for c in pathobj.Path.Commands: + outstring = [] + command = c.Name + outstring.append(command) + + # if modal: suppress the command if it is the same as the last one + if MODAL is True: + if command == lastcommand: + outstring.pop(0) + + if c.Name.startswith("(") and not OUTPUT_COMMENTS: # command is a comment + continue + + # Handle G84/G74 tapping cycles + if command in ("G84", "G74") and "F" in c.Parameters: + pitch_mm = float(c.Parameters["F"]) + c.Parameters.pop("F") # Remove F from output, we'll handle it + + # Get spindle speed (from S param or last known value) + spindle_speed = None + if "S" in c.Parameters: + spindle_speed = float(c.Parameters["S"]) + c.Parameters.pop("S") + + # Convert pitch to inches if needed + if UNITS == "G20": # imperial + pitch = pitch_mm / 25.4 + else: + pitch = pitch_mm + + # Calculate feed rate + if spindle_speed is not None: + feed_rate = pitch * spindle_speed + speed = Units.Quantity(feed_rate, UNIT_SPEED_FORMAT) + outstring.append( + "F" + format(float(speed.getValueAs(UNIT_SPEED_FORMAT)), precision_string) + ) + else: + # No spindle speed found, output pitch as F + outstring.append("F" + format(pitch, precision_string)) + + # Now add the remaining parameters in order + for param in params: + if param in c.Parameters: + if param == "F" and ( + currLocation[param] != c.Parameters[param] or OUTPUT_DOUBLES + ): + if c.Name not in [ + "G0", + "G00", + ]: # linuxcnc doesn't use rapid speeds + speed = Units.Quantity(c.Parameters["F"], FreeCAD.Units.Velocity) + if speed.getValueAs(UNIT_SPEED_FORMAT) > 0.0: + outstring.append( + param + + format( + float(speed.getValueAs(UNIT_SPEED_FORMAT)), + precision_string, + ) + ) + else: + continue + elif param == "T": + outstring.append(param + str(int(c.Parameters["T"]))) + elif param == "H": + outstring.append(param + str(int(c.Parameters["H"]))) + elif param == "D": + outstring.append(param + str(int(c.Parameters["D"]))) + elif param == "S": + outstring.append(param + str(int(c.Parameters["S"]))) + else: + if ( + (not OUTPUT_DOUBLES) + and (param in currLocation) + and (currLocation[param] == c.Parameters[param]) + ): + continue + else: + if param in ("A", "B", "C"): + outstring.append( + param + format(float(c.Parameters[param]), precision_string) + ) + else: + pos = Units.Quantity(c.Parameters[param], FreeCAD.Units.Length) + outstring.append( + param + + format(float(pos.getValueAs(UNIT_FORMAT)), precision_string) + ) + + # store the latest command + lastcommand = command + currLocation.update(c.Parameters) + + # Check for Tool Change: + if command == "M6": + # stop the spindle + out += linenumber() + "M5\n" + for line in TOOL_CHANGE.splitlines(True): + out += linenumber() + line + + # add height offset + if USE_TLO: + tool_height = "\nG43 H" + str(int(c.Parameters["T"])) + outstring.append(tool_height) + + if command == "message": + if OUTPUT_COMMENTS is False: + out = [] + else: + outstring.pop(0) # remove the command + + # prepend a line number and append a newline + if len(outstring) >= 1: + if OUTPUT_LINE_NUMBERS: + outstring.insert(0, (linenumber())) + + # append the line to the final output + for w in outstring: + out += w + COMMAND_SPACE + out += "\n" + + return out + + +# print(__name__ + " gcode postprocessor loaded.") diff --git a/src/Mod/CAM/Path/Post/scripts/linuxcnc_post.py b/src/Mod/CAM/Path/Post/scripts/linuxcnc_post.py index 98758d7cad..71e22a9d3e 100644 --- a/src/Mod/CAM/Path/Post/scripts/linuxcnc_post.py +++ b/src/Mod/CAM/Path/Post/scripts/linuxcnc_post.py @@ -2,6 +2,9 @@ # *************************************************************************** # * Copyright (c) 2014 sliptonic * +# * Copyright (c) 2022 - 2025 Larry Woestman * +# * Copyright (c) 2024 Ondsel * +# * Copyright (c) 2024 Carl Slater * # * * # * This file is part of the FreeCAD CAx development system. * # * * @@ -23,517 +26,182 @@ # * * # *************************************************************************** -import FreeCAD -from FreeCAD import Units +from typing import Any, Dict + +from Path.Post.Processor import PostProcessor + import Path -import argparse -import datetime -import shlex -import Path.Base.Util as PathUtil -import Path.Post.Utils as PostUtils -import PathScripts.PathUtils as PathUtils -from builtins import open as pyopen +import FreeCAD -TOOLTIP = """ -This is a postprocessor file for the Path workbench. It is used to -take a pseudo-G-code fragment outputted by a Path object, and output -real G-code suitable for a linuxcnc 3 axis mill. This postprocessor, once placed -in the appropriate PathScripts folder, can be used directly from inside -FreeCAD, via the GUI importer or via python scripts with: +translate = FreeCAD.Qt.translate -import linuxcnc_post -linuxcnc_post.export(object,"/path/to/file.ncc","") -""" +DEBUG = False +if DEBUG: + Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) + Path.Log.trackModule(Path.Log.thisModule()) +else: + Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule()) -now = datetime.datetime.now() - -parser = argparse.ArgumentParser(prog="linuxcnc", add_help=False) -parser.add_argument("--no-header", action="store_true", help="suppress header output") -parser.add_argument("--no-comments", action="store_true", help="suppress comment output") -parser.add_argument("--line-numbers", action="store_true", help="prefix with line numbers") -parser.add_argument( - "--no-show-editor", - action="store_true", - help="don't pop up editor before writing output", -) -parser.add_argument("--precision", default="3", help="number of digits of precision, default=3") -parser.add_argument( - "--preamble", - help='set commands to be issued before the first command, default="G17 G54 G40 G49 G80 G90\\n"', -) -parser.add_argument( - "--postamble", - help='set commands to be issued after the last command, default="M05\\nG17 G54 G90 G80 G40\\nM2\\n"', -) -parser.add_argument( - "--inches", action="store_true", help="Convert output for US imperial mode (G20)" -) -parser.add_argument( - "--modal", - action="store_true", - help="Output the Same G-command Name USE NonModal Mode", -) -parser.add_argument("--axis-modal", action="store_true", help="Output the Same Axis Value Mode") -parser.add_argument( - "--no-tlo", - action="store_true", - help="suppress tool length offset (G43) following tool changes", -) -parser.add_argument("--rigid-tap", action="store_true", help="Enable G33.1 rigid tapping cycle") - -TOOLTIP_ARGS = parser.format_help() - -# These globals set common customization preferences -OUTPUT_COMMENTS = True -OUTPUT_HEADER = True -OUTPUT_LINE_NUMBERS = False -SHOW_EDITOR = True -MODAL = False # if true commands are suppressed if the same as previous line. -USE_TLO = True # if true G43 will be output following tool changes -OUTPUT_DOUBLES = True # if false duplicate axis values are suppressed if the same as previous line. -COMMAND_SPACE = " " -LINENR = 100 # line number starting value - -# These globals will be reflected in the Machine configuration of the project -UNITS = "G21" # G21 for metric, G20 for us standard -UNIT_SPEED_FORMAT = "mm/min" -UNIT_FORMAT = "mm" - -MACHINE_NAME = "LinuxCNC" -CORNER_MIN = {"x": 0, "y": 0, "z": 0} -CORNER_MAX = {"x": 500, "y": 300, "z": 300} -PRECISION = 3 - -RIGID_TAP = False - -# Preamble text will appear at the beginning of the GCODE output file. -PREAMBLE = """G17 G54 G40 G49 G80 G90 -""" - -# Postamble text will appear following the last operation. -POSTAMBLE = """M05 -G17 G54 G90 G80 G40 -M2 -""" - -# Pre operation text will be inserted before every operation -PRE_OPERATION = """""" - -# Post operation text will be inserted after every operation -POST_OPERATION = """""" - -# Tool Change commands will be inserted before a tool change -TOOL_CHANGE = """""" +# +# Define some types that are used throughout this file. +# +Values = Dict[str, Any] -def processArguments(argstring): - global OUTPUT_HEADER - global OUTPUT_COMMENTS - global OUTPUT_LINE_NUMBERS - global SHOW_EDITOR - global PRECISION - global PREAMBLE - global POSTAMBLE - global UNITS - global UNIT_SPEED_FORMAT - global UNIT_FORMAT - global MODAL - global USE_TLO - global OUTPUT_DOUBLES - global RIGID_TAP +class Linuxcnc(PostProcessor): + """ + The LinuxCNC post processor class. + LinuxCNC supports various trajectory control methods (path blending) as + described at https://linuxcnc.org/docs/2.4/html/common_User_Concepts.html#r1_1_2 - try: - args = parser.parse_args(shlex.split(argstring)) - if args.no_header: - OUTPUT_HEADER = False - if args.no_comments: - OUTPUT_COMMENTS = False - if args.line_numbers: - OUTPUT_LINE_NUMBERS = True - if args.no_show_editor: - SHOW_EDITOR = False - print("Show editor = %d" % SHOW_EDITOR) - PRECISION = args.precision - if args.preamble is not None: - PREAMBLE = args.preamble.replace("\\n", "\n") - if args.postamble is not None: - POSTAMBLE = args.postamble.replace("\\n", "\n") - if args.inches: - UNITS = "G20" - UNIT_SPEED_FORMAT = "in/min" - UNIT_FORMAT = "in" - PRECISION = 4 - if args.modal: - MODAL = True - if args.no_tlo: - USE_TLO = False - if args.axis_modal: - print("here") - OUTPUT_DOUBLES = False - if args.rigid_tap: - RIGID_TAP = True - - except Exception: - return False - - return True + This post processor implements the following trajectory control methods: + - Exact Path (G61) + - Exact Stop (G64) + - Blend (G61.1) -def export(objectslist, filename, argstring): - if not processArguments(argstring): - return None - global UNITS - global UNIT_FORMAT - global UNIT_SPEED_FORMAT + """ - for obj in objectslist: - if not hasattr(obj, "Path"): - print( - "the object " + obj.Name + " is not a path. Please select only path and Compounds." - ) - return None + def __init__( + self, + job, + tooltip=translate("CAM", "LinuxCNC post processor"), + tooltipargs=["blend-mode", "blend-tolerance"], + units="Metric", + ) -> None: + super().__init__( + job=job, + tooltip=tooltip, + tooltipargs=tooltipargs, + units=units, + ) + Path.Log.debug("LinuxCNC post processor initialized.") - print("postprocessing...") - gcode = "" - - # write header - if OUTPUT_HEADER: - gcode += linenumber() + "(Exported by FreeCAD)\n" - gcode += linenumber() + "(Post Processor: " + __name__ + ")\n" - gcode += linenumber() + "(Output Time:" + str(now) + ")\n" - - # Write the preamble - if OUTPUT_COMMENTS: - gcode += linenumber() + "(begin preamble)\n" - for line in PREAMBLE.splitlines(): - gcode += linenumber() + line + "\n" - gcode += linenumber() + UNITS + "\n" - - for obj in objectslist: - # Skip inactive operations - if not PathUtil.activeForOp(obj): - continue - - # do the pre_op - if OUTPUT_COMMENTS: - gcode += linenumber() + "(begin operation: %s)\n" % obj.Label - gcode += linenumber() + "(machine units: %s)\n" % (UNIT_SPEED_FORMAT) - for line in PRE_OPERATION.splitlines(True): - gcode += linenumber() + line - - # get coolant mode - coolantMode = PathUtil.coolantModeForOp(obj) - - # turn coolant on if required - if OUTPUT_COMMENTS: - if not coolantMode == "None": - gcode += linenumber() + "(Coolant On:" + coolantMode + ")\n" - if coolantMode == "Flood": - gcode += linenumber() + "M8" + "\n" - if coolantMode == "Mist": - gcode += linenumber() + "M7" + "\n" - - # process the operation gcode - gcode += parse(obj) - - # do the post_op - if OUTPUT_COMMENTS: - gcode += linenumber() + "(finish operation: %s)\n" % obj.Label - for line in POST_OPERATION.splitlines(True): - gcode += linenumber() + line - - # turn coolant off if required - if not coolantMode == "None": - if OUTPUT_COMMENTS: - gcode += linenumber() + "(Coolant Off:" + coolantMode + ")\n" - gcode += linenumber() + "M9" + "\n" - - # do the post_amble - if OUTPUT_COMMENTS: - gcode += "(begin postamble)\n" - for line in POSTAMBLE.splitlines(): - gcode += linenumber() + line + "\n" - - if FreeCAD.GuiUp and SHOW_EDITOR: - final = gcode - if len(gcode) > 100000: - print("Skipping editor since output is greater than 100kb") - else: - dia = PostUtils.GCodeEditorDialog() - dia.editor.setText(gcode) - result = dia.exec_() - if result: - final = dia.editor.toPlainText() - else: - final = gcode - - print("done postprocessing.") - - if not filename == "-": - gfile = pyopen(filename, "w") - gfile.write(final) - gfile.close() - - return final - - -def linenumber(): - global LINENR - if OUTPUT_LINE_NUMBERS is True: - LINENR += 10 - return "N" + str(LINENR) + " " - return "" - - -def parse(pathobj): - global PRECISION - global MODAL - global OUTPUT_DOUBLES - global UNIT_FORMAT - global UNIT_SPEED_FORMAT - - out = "" - lastcommand = None - precision_string = "." + str(PRECISION) + "f" - currLocation = {} # keep track for no doubles - - # the order of parameters - # linuxcnc doesn't want K properties on XY plane Arcs need work. - params = [ - "X", - "Y", - "Z", - "A", - "B", - "C", - "I", - "J", - "F", - "S", - "T", - "Q", - "R", - "L", - "H", - "D", - "P", - ] - firstmove = Path.Command("G0", {"X": -1, "Y": -1, "Z": -1, "F": 0.0}) - currLocation.update(firstmove.Parameters) # set First location Parameters - - if hasattr(pathobj, "Group"): # We have a compound or project. - # if OUTPUT_COMMENTS: - # out += linenumber() + "(compound: " + pathobj.Label + ")\n" - for p in pathobj.Group: - out += parse(p) - return out - else: # parsing simple path - # groups might contain non-path things like stock. - if not hasattr(pathobj, "Path"): - return out - - # if OUTPUT_COMMENTS: - # out += linenumber() + "(" + pathobj.Label + ")\n" - - # The following "for" statement was fairly recently added - # but seems to be using the A, B, and C parameters in ways - # that don't appear to be compatible with how the PATH code - # uses the A, B, and C parameters. I have reverted the - # change here until we can figure out what it going on. + def init_values(self, values: Values) -> None: + """Initialize values that are used throughout the postprocessor.""" # - # for c in PathUtils.getPathWithPlacement(pathobj).Commands: - for c in pathobj.Path.Commands: - outstring = [] - command = c.Name - outstring.append(command) + super().init_values(values) + # + # Set any values here that need to override the default values set + # in the parent routine. + # + values["ENABLE_COOLANT"] = True + # + # The order of parameters. + # + # linuxcnc doesn't want K properties on XY plane; Arcs need work. + # + values["PARAMETER_ORDER"] = [ + "X", + "Y", + "Z", + "A", + "B", + "C", + "I", + "J", + "F", + "S", + "T", + "Q", + "R", + "L", + "H", + "D", + "P", + ] + # + # Used in the argparser code as the "name" of the postprocessor program. + # + values["MACHINE_NAME"] = "LinuxCNC" + # + # Any commands in this value will be output as the last commands + # in the G-code file. + # + values[ + "POSTAMBLE" + ] = """M05 +G17 G54 G90 G80 G40 +M2""" + values["POSTPROCESSOR_FILE_NAME"] = __name__ + # + # Path blending mode configuration (LinuxCNC-specific) + # + values["BLEND_MODE"] = "BLEND" # Options: EXACT_PATH, EXACT_STOP, BLEND + values["BLEND_TOLERANCE"] = 0.0 # P value for BLEND mode (0 = G64, >0 = G64 P-) + # + # Any commands in this value will be output after the header and + # safety block at the beginning of the G-code file. + # + values["PREAMBLE"] = """G17 G54 G40 G49 G80 G90 """ - # if modal: suppress the command if it is the same as the last one - if MODAL is True: - if command == lastcommand: - outstring.pop(0) + def init_arguments(self, values, argument_defaults, arguments_visible): + """Initialize command-line arguments, including LinuxCNC-specific options.""" + parser = super().init_arguments(values, argument_defaults, arguments_visible) - if c.Name.startswith("(") and not OUTPUT_COMMENTS: # command is a comment - continue + # Add LinuxCNC-specific argument group + linuxcnc_group = parser.add_argument_group("LinuxCNC-specific arguments") - # Check for G80, G98, G99 with rigid tapping and annotation - if ( - command in ("G80", "G98", "G99") - and RIGID_TAP - and hasattr(c, "Annotations") - and c.Annotations.get("operation") == "tapping" - ): - continue # Skip this command + linuxcnc_group.add_argument( + "--blend-mode", + choices=["EXACT_PATH", "EXACT_STOP", "BLEND"], + default="BLEND", + help="Path blending mode: EXACT_PATH (G61), EXACT_STOP (G61.1), " + "BLEND (G64/G64 P-) (default: BLEND)", + ) - # Handle G84/G74 tapping cycles - if command in ("G84", "G74") and "F" in c.Parameters: - pitch_mm = float(c.Parameters["F"]) - c.Parameters.pop("F") # Remove F from output, we'll handle it + linuxcnc_group.add_argument( + "--blend-tolerance", + type=float, + default=0.0, + help="Tolerance for BLEND mode (P value): 0 = no tolerance (G64), " + ">0 = tolerance (G64 P-), in current units (default: 0.0)", + ) + return parser - # Get spindle speed (from S param or last known value) - spindle_speed = None - if "S" in c.Parameters: - spindle_speed = float(c.Parameters["S"]) - c.Parameters.pop("S") + def process_arguments(self): + """Process arguments and update values, including blend mode handling.""" + flag, args = super().process_arguments() - # Convert pitch to inches if needed - if UNITS == "G20": # imperial - pitch = pitch_mm / 25.4 - else: - pitch = pitch_mm + if flag and args: + # Update blend mode values from parsed arguments + if hasattr(args, "blend_mode"): + self.values["BLEND_MODE"] = args.blend_mode + if hasattr(args, "blend_tolerance"): + self.values["BLEND_TOLERANCE"] = args.blend_tolerance - # Rigid tapping logic - if RIGID_TAP: - # Output initial tapping command - outstring[0] = "G33.1" - outstring.append("K" + format(pitch, precision_string)) + # Update PREAMBLE with blend command + blend_cmd = self._get_blend_command() + self.values[ + "PREAMBLE" + ] = f"""G17 G54 G40 G49 G80 G90 +{blend_cmd}""" - if "Z" in c.Parameters: - outstring.append("Z" + format(float(c.Parameters["Z"]), precision_string)) + return flag, args - # Output the tapping line - if len(outstring) >= 1: - if OUTPUT_LINE_NUMBERS: - outstring.insert(0, (linenumber())) - for w in outstring: - out += w + COMMAND_SPACE - out += "\n" + def _get_blend_command(self) -> str: + """Generate the path blending G-code command based on current settings.""" + mode = self.values.get("BLEND_MODE", "BLEND") - if "P" in c.Parameters: - # Issue spindle stop - out += linenumber() + "M5\n" - # Issue dwell with P value - out += linenumber() + f"G04 P{c.Parameters['P']}\n" + if mode == "EXACT_PATH": + return "G61" + elif mode == "EXACT_STOP": + return "G61.1" + else: # BLEND + tolerance = self.values.get("BLEND_TOLERANCE", 0.0) + if tolerance > 0: + return f"G64 P{tolerance:.4f}" + else: + return "G64" - # Now handle reverse out and spindle restore - if command == "G84": - # Reverse spindle (M4) with spindle speed - out += linenumber() + "M4\n" - # Repeat tapping command to reverse out, use R for Z - reverse_z = c.Parameters.get("R") - if reverse_z is not None: - pos = Units.Quantity(reverse_z, FreeCAD.Units.Length) - reverse_z = float(pos.getValueAs(UNIT_FORMAT)) - out += ( - linenumber() - + f"G33.1 K{format(pitch, precision_string)} Z{format(float(reverse_z), precision_string)}\n" - ) - else: - out += linenumber() + f"G33.1 K{format(pitch, precision_string)}\n" - # Restore original spindle direction (M3) with spindle speed - out += linenumber() + "M3\n" - elif command == "G74": - # Forward spindle (M3) with spindle speed - out += linenumber() + "M3\n" - # Repeat tapping command to reverse out, use R for Z - reverse_z = c.Parameters.get("R") - if reverse_z is not None: - pos = Units.Quantity(reverse_z, FreeCAD.Units.Length) - reverse_z = float(pos.getValueAs(UNIT_FORMAT)) - out += ( - linenumber() - + f"G33.1 K{format(pitch, precision_string)} Z{format(float(reverse_z), precision_string)}\n" - ) - else: - out += linenumber() + f"G33.1 K{format(pitch, precision_string)}\n" - # Restore original spindle direction (M4) with spindle speed - out += linenumber() + "M4\n" + # tooltipArgs is inherited from base class and automatically includes + # all arguments from init_arguments() via parser.format_help() - continue # Skip the rest of the parameter output for this command - - else: - # Calculate feed rate - if spindle_speed is not None: - feed_rate = pitch * spindle_speed - speed = Units.Quantity(feed_rate, UNIT_SPEED_FORMAT) - outstring.append( - "F" - + format(float(speed.getValueAs(UNIT_SPEED_FORMAT)), precision_string) - ) - else: - # No spindle speed found, output pitch as F - outstring.append("F" + format(pitch, precision_string)) - - # Now add the remaining parameters in order - for param in params: - if param in c.Parameters: - if param == "F" and ( - currLocation[param] != c.Parameters[param] or OUTPUT_DOUBLES - ): - if c.Name not in [ - "G0", - "G00", - ]: # linuxcnc doesn't use rapid speeds - speed = Units.Quantity(c.Parameters["F"], FreeCAD.Units.Velocity) - if speed.getValueAs(UNIT_SPEED_FORMAT) > 0.0: - outstring.append( - param - + format( - float(speed.getValueAs(UNIT_SPEED_FORMAT)), - precision_string, - ) - ) - else: - continue - elif param == "T": - outstring.append(param + str(int(c.Parameters["T"]))) - elif param == "H": - outstring.append(param + str(int(c.Parameters["H"]))) - elif param == "D": - outstring.append(param + str(int(c.Parameters["D"]))) - elif param == "S": - outstring.append(param + str(int(c.Parameters["S"]))) - else: - if ( - (not OUTPUT_DOUBLES) - and (param in currLocation) - and (currLocation[param] == c.Parameters[param]) - ): - continue - else: - if param in ("A", "B", "C"): - outstring.append( - param + format(float(c.Parameters[param]), precision_string) - ) - else: - pos = Units.Quantity(c.Parameters[param], FreeCAD.Units.Length) - outstring.append( - param - + format(float(pos.getValueAs(UNIT_FORMAT)), precision_string) - ) - - # store the latest command - lastcommand = command - currLocation.update(c.Parameters) - - # Check for Tool Change: - if command == "M6": - # stop the spindle - out += linenumber() + "M5\n" - for line in TOOL_CHANGE.splitlines(True): - out += linenumber() + line - - # add height offset - if USE_TLO: - tool_height = "\nG43 H" + str(int(c.Parameters["T"])) - outstring.append(tool_height) - - if command == "message": - if OUTPUT_COMMENTS is False: - out = [] - else: - outstring.pop(0) # remove the command - - # prepend a line number and append a newline - if len(outstring) >= 1: - if OUTPUT_LINE_NUMBERS: - outstring.insert(0, (linenumber())) - - # append the line to the final output - for w in outstring: - out += w + COMMAND_SPACE - out += "\n" - - return out - - -# print(__name__ + " gcode postprocessor loaded.") + @property + def tooltip(self): + tooltip: str = """ + This is a postprocessor file for the CAM workbench. + It is used to take a pseudo-gcode fragment from a CAM object + and output 'real' GCode suitable for a linuxcnc 3 axis mill. + """ + return tooltip diff --git a/src/Mod/CAM/Path/Post/scripts/mach3_mach4_legacy_post.py b/src/Mod/CAM/Path/Post/scripts/mach3_mach4_legacy_post.py new file mode 100644 index 0000000000..0353a1f21d --- /dev/null +++ b/src/Mod/CAM/Path/Post/scripts/mach3_mach4_legacy_post.py @@ -0,0 +1,481 @@ +# *************************************************************************** +# * Copyright (c) 2014 sliptonic * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * FreeCAD is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Lesser General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with FreeCAD; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# ***************************************************************************/ + +import FreeCAD +from FreeCAD import Units +import Path +import argparse +import datetime +import shlex +import Path.Base.Util as PathUtil +import Path.Post.Utils as PostUtils +import PathScripts.PathUtils as PathUtils +from builtins import open as pyopen + +TOOLTIP = """ +This is a postprocessor file for the Path workbench. It is used to +take a pseudo-G-code fragment outputted by a Path object, and output +real G-code suitable for a mach3_4 3 axis mill. This postprocessor, once placed +in the appropriate PathScripts folder, can be used directly from inside +FreeCAD, via the GUI importer or via python scripts with: + +import mach3_4_legacy_post +mach3_4_legacy_post.export(object,"/path/to/file.ncc","") +""" + +now = datetime.datetime.now() + +parser = argparse.ArgumentParser(prog="mach3_4", add_help=False) +parser.add_argument("--no-header", action="store_true", help="suppress header output") +parser.add_argument("--no-comments", action="store_true", help="suppress comment output") +parser.add_argument("--line-numbers", action="store_true", help="prefix with line numbers") +parser.add_argument( + "--no-show-editor", + action="store_true", + help="don't pop up editor before writing output", +) +parser.add_argument("--precision", default="3", help="number of digits of precision, default=3") +parser.add_argument( + "--preamble", + help='set commands to be issued before the first command, default="G17 G54 G40 G49 G80 G90\\n"', +) +parser.add_argument( + "--postamble", + help='set commands to be issued after the last command, default="M05\\nG17 G54 G90 G80 G40\\nM2\\n"', +) +parser.add_argument( + "--inches", action="store_true", help="Convert output for US imperial mode (G20)" +) +parser.add_argument( + "--modal", + action="store_true", + help="Output the Same G-command Name USE NonModal Mode", +) +parser.add_argument("--axis-modal", action="store_true", help="Output the Same Axis Value Mode") +parser.add_argument( + "--no-tlo", + action="store_true", + help="suppress tool length offset (G43) following tool changes", +) + +TOOLTIP_ARGS = parser.format_help() + +# These globals set common customization preferences +OUTPUT_COMMENTS = True +OUTPUT_HEADER = True +OUTPUT_LINE_NUMBERS = False +SHOW_EDITOR = True +MODAL = False # if true commands are suppressed if the same as previous line. +USE_TLO = True # if true G43 will be output following tool changes +OUTPUT_DOUBLES = True # if false duplicate axis values are suppressed if the same as previous line. +COMMAND_SPACE = " " +LINENR = 100 # line number starting value + +# These globals will be reflected in the Machine configuration of the project +UNITS = "G21" # G21 for metric, G20 for us standard +UNIT_SPEED_FORMAT = "mm/min" +UNIT_FORMAT = "mm" + +MACHINE_NAME = "mach3_4" +CORNER_MIN = {"x": 0, "y": 0, "z": 0} +CORNER_MAX = {"x": 500, "y": 300, "z": 300} +PRECISION = 3 + +# Preamble text will appear at the beginning of the GCODE output file. +PREAMBLE = """G17 G54 G40 G49 G80 G90 +""" + +# Postamble text will appear following the last operation. +POSTAMBLE = """M05 +G17 G54 G90 G80 G40 +M2 +""" + +# Pre operation text will be inserted before every operation +PRE_OPERATION = """""" + +# Post operation text will be inserted after every operation +POST_OPERATION = """""" + +# Tool Change commands will be inserted before a tool change +TOOL_CHANGE = """""" + + +def processArguments(argstring): + global OUTPUT_HEADER + global OUTPUT_COMMENTS + global OUTPUT_LINE_NUMBERS + global SHOW_EDITOR + global PRECISION + global PREAMBLE + global POSTAMBLE + global UNITS + global UNIT_SPEED_FORMAT + global UNIT_FORMAT + global MODAL + global USE_TLO + global OUTPUT_DOUBLES + + try: + args = parser.parse_args(shlex.split(argstring)) + if args.no_header: + OUTPUT_HEADER = False + if args.no_comments: + OUTPUT_COMMENTS = False + if args.line_numbers: + OUTPUT_LINE_NUMBERS = True + if args.no_show_editor: + SHOW_EDITOR = False + print("Show editor = %d" % SHOW_EDITOR) + PRECISION = args.precision + if args.preamble is not None: + PREAMBLE = args.preamble.replace("\\n", "\n") + if args.postamble is not None: + POSTAMBLE = args.postamble.replace("\\n", "\n") + if args.inches: + UNITS = "G20" + UNIT_SPEED_FORMAT = "in/min" + UNIT_FORMAT = "in" + PRECISION = 4 + if args.modal: + MODAL = True + if args.no_tlo: + USE_TLO = False + if args.axis_modal: + print("here") + OUTPUT_DOUBLES = False + + except Exception: + return False + + return True + + +def export(objectslist, filename, argstring): + if not processArguments(argstring): + return None + global UNITS + global UNIT_FORMAT + global UNIT_SPEED_FORMAT + + for obj in objectslist: + if not hasattr(obj, "Path"): + print( + "the object " + obj.Name + " is not a path. Please select only path and Compounds." + ) + return None + + print("postprocessing...") + gcode = "" + + # write header + if OUTPUT_HEADER: + gcode += linenumber() + "(Exported by FreeCAD)\n" + gcode += linenumber() + "(Post Processor: " + __name__ + ")\n" + gcode += linenumber() + "(Output Time:" + str(now) + ")\n" + + # Write the preamble + if OUTPUT_COMMENTS: + gcode += linenumber() + "(begin preamble)\n" + for line in PREAMBLE.splitlines(): + gcode += linenumber() + line + "\n" + gcode += linenumber() + UNITS + "\n" + + for obj in objectslist: + + # Skip inactive operations + if not PathUtil.activeForOp(obj): + continue + + # do the pre_op + if OUTPUT_COMMENTS: + gcode += linenumber() + "(begin operation: %s)\n" % obj.Label + gcode += linenumber() + "(machine: %s, %s)\n" % ( + MACHINE_NAME, + UNIT_SPEED_FORMAT, + ) + for line in PRE_OPERATION.splitlines(True): + gcode += linenumber() + line + + # get coolant mode + coolantMode = PathUtil.coolantModeForOp(obj) + + # turn coolant on if required + if OUTPUT_COMMENTS: + if not coolantMode == "None": + gcode += linenumber() + "(Coolant On:" + coolantMode + ")\n" + if coolantMode == "Flood": + gcode += linenumber() + "M8" + "\n" + if coolantMode == "Mist": + gcode += linenumber() + "M7" + "\n" + + # process the operation gcode + gcode += parse(obj) + + # do the post_op + if OUTPUT_COMMENTS: + gcode += linenumber() + "(finish operation: %s)\n" % obj.Label + for line in POST_OPERATION.splitlines(True): + gcode += linenumber() + line + + # turn coolant off if required + if not coolantMode == "None": + if OUTPUT_COMMENTS: + gcode += linenumber() + "(Coolant Off:" + coolantMode + ")\n" + gcode += linenumber() + "M9" + "\n" + + # do the post_amble + if OUTPUT_COMMENTS: + gcode += "(begin postamble)\n" + for line in POSTAMBLE.splitlines(): + gcode += linenumber() + line + "\n" + + if FreeCAD.GuiUp and SHOW_EDITOR: + dia = PostUtils.GCodeEditorDialog() + dia.editor.setText(gcode) + result = dia.exec_() + if result: + final = dia.editor.toPlainText() + else: + final = gcode + else: + final = gcode + + print("done postprocessing.") + + if not filename == "-": + gfile = pyopen(filename, "w") + gfile.write(final) + gfile.close() + + return final + + +def linenumber(): + global LINENR + if OUTPUT_LINE_NUMBERS is True: + LINENR += 10 + return "N" + str(LINENR) + " " + return "" + + +def parse(pathobj): + global PRECISION + global MODAL + global OUTPUT_DOUBLES + global UNIT_FORMAT + global UNIT_SPEED_FORMAT + + out = "" + lastcommand = None + precision_string = "." + str(PRECISION) + "f" + currLocation = {} # keep track for no doubles + + # the order of parameters + # mach3_4 doesn't want K properties on XY plane Arcs need work. + params = [ + "X", + "Y", + "Z", + "A", + "B", + "C", + "I", + "J", + "F", + "S", + "T", + "Q", + "R", + "L", + "H", + "D", + "P", + ] + firstmove = Path.Command("G0", {"X": -1, "Y": -1, "Z": -1, "F": 0.0}) + currLocation.update(firstmove.Parameters) # set First location Parameters + + if hasattr(pathobj, "Group"): # We have a compound or project. + # if OUTPUT_COMMENTS: + # out += linenumber() + "(compound: " + pathobj.Label + ")\n" + for p in pathobj.Group: + out += parse(p) + return out + else: # parsing simple path + + # groups might contain non-path things like stock. + if not hasattr(pathobj, "Path"): + return out + + # if OUTPUT_COMMENTS: + # out += linenumber() + "(" + pathobj.Label + ")\n" + + adaptiveOp = False + opHorizRapid = 0 + opVertRapid = 0 + + if "Adaptive" in pathobj.Name: + adaptiveOp = True + if hasattr(pathobj, "ToolController"): + if ( + hasattr(pathobj.ToolController, "HorizRapid") + and pathobj.ToolController.HorizRapid > 0 + ): + opHorizRapid = Units.Quantity( + pathobj.ToolController.HorizRapid, FreeCAD.Units.Velocity + ) + else: + FreeCAD.Console.PrintWarning( + "Tool Controller Horizontal Rapid Values are unset" + "\n" + ) + + if ( + hasattr(pathobj.ToolController, "VertRapid") + and pathobj.ToolController.VertRapid > 0 + ): + opVertRapid = Units.Quantity( + pathobj.ToolController.VertRapid, FreeCAD.Units.Velocity + ) + else: + FreeCAD.Console.PrintWarning( + "Tool Controller Vertical Rapid Values are unset" + "\n" + ) + + for c in PathUtils.getPathWithPlacement(pathobj).Commands: + + outstring = [] + command = c.Name + + if adaptiveOp and c.Name in ["G0", "G00"]: + if opHorizRapid and opVertRapid: + command = "G1" + else: + outstring.append("(Tool Controller Rapid Values are unset)" + "\n") + + outstring.append(command) + + # if modal: suppress the command if it is the same as the last one + if MODAL is True: + if command == lastcommand: + outstring.pop(0) + + if c.Name.startswith("(") and not OUTPUT_COMMENTS: # command is a comment + continue + + # Now add the remaining parameters in order + for param in params: + if param in c.Parameters: + if param == "F" and ( + currLocation[param] != c.Parameters[param] or OUTPUT_DOUBLES + ): + if c.Name not in [ + "G0", + "G00", + ]: # mach3_4 doesn't use rapid speeds + speed = Units.Quantity(c.Parameters["F"], FreeCAD.Units.Velocity) + if speed.getValueAs(UNIT_SPEED_FORMAT) > 0.0: + outstring.append( + param + + format( + float(speed.getValueAs(UNIT_SPEED_FORMAT)), + precision_string, + ) + ) + else: + continue + elif param == "T": + outstring.append(param + str(int(c.Parameters["T"]))) + elif param == "H": + outstring.append(param + str(int(c.Parameters["H"]))) + elif param == "D": + outstring.append(param + str(int(c.Parameters["D"]))) + elif param == "S": + outstring.append(param + str(int(c.Parameters["S"]))) + else: + 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) + outstring.append( + param + format(float(pos.getValueAs(UNIT_FORMAT)), precision_string) + ) + + if adaptiveOp and c.Name in ["G0", "G00"]: + if opHorizRapid and opVertRapid: + if "Z" not in c.Parameters: + outstring.append( + "F" + + format( + float(opHorizRapid.getValueAs(UNIT_SPEED_FORMAT)), + precision_string, + ) + ) + else: + outstring.append( + "F" + + format( + float(opVertRapid.getValueAs(UNIT_SPEED_FORMAT)), + precision_string, + ) + ) + + # store the latest command + lastcommand = command + currLocation.update(c.Parameters) + + # Check for Tool Change: + if command == "M6": + # stop the spindle + out += linenumber() + "M5\n" + for line in TOOL_CHANGE.splitlines(True): + out += linenumber() + line + + # add height offset + if USE_TLO: + tool_height = "\nG43 H" + str(int(c.Parameters["T"])) + outstring.append(tool_height) + + if command == "message": + if OUTPUT_COMMENTS is False: + out = [] + else: + outstring.pop(0) # remove the command + + # prepend a line number and append a newline + if len(outstring) >= 1: + if OUTPUT_LINE_NUMBERS: + outstring.insert(0, (linenumber())) + + # append the line to the final output + for w in outstring: + out += w + COMMAND_SPACE + out = out.strip() + "\n" + + return out + + +# print(__name__ + " gcode postprocessor loaded.") diff --git a/src/Mod/CAM/Path/Post/scripts/mach3_mach4_post.py b/src/Mod/CAM/Path/Post/scripts/mach3_mach4_post.py index 43decaaa6b..b9bf80475f 100644 --- a/src/Mod/CAM/Path/Post/scripts/mach3_mach4_post.py +++ b/src/Mod/CAM/Path/Post/scripts/mach3_mach4_post.py @@ -2,6 +2,8 @@ # *************************************************************************** # * Copyright (c) 2014 sliptonic * +# * Copyright (c) 2022 - 2025 Larry Woestman * +# * Copyright (c) 2024 Ondsel * # * * # * This file is part of the FreeCAD CAx development system. * # * * @@ -21,463 +23,128 @@ # * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * # * USA * # * * -# ***************************************************************************/ +# *************************************************************************** + +from typing import Any, Dict + +from Path.Post.Processor import PostProcessor -import FreeCAD -from FreeCAD import Units import Path -import argparse -import datetime -import shlex -import Path.Base.Util as PathUtil -import Path.Post.Utils as PostUtils -import PathScripts.PathUtils as PathUtils -from builtins import open as pyopen +import FreeCAD -TOOLTIP = """ -This is a postprocessor file for the Path workbench. It is used to -take a pseudo-G-code fragment outputted by a Path object, and output -real G-code suitable for a mach3_4 3 axis mill. This postprocessor, once placed -in the appropriate PathScripts folder, can be used directly from inside -FreeCAD, via the GUI importer or via python scripts with: +translate = FreeCAD.Qt.translate -import mach3_4_post -mach3_4_post.export(object,"/path/to/file.ncc","") -""" +DEBUG = False +if DEBUG: + Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) + Path.Log.trackModule(Path.Log.thisModule()) +else: + Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule()) -now = datetime.datetime.now() +# +# Define some types that are used throughout this file. +# +Values = Dict[str, Any] +Visible = Dict[str, bool] -parser = argparse.ArgumentParser(prog="mach3_4", add_help=False) -parser.add_argument("--no-header", action="store_true", help="suppress header output") -parser.add_argument("--no-comments", action="store_true", help="suppress comment output") -parser.add_argument("--line-numbers", action="store_true", help="prefix with line numbers") -parser.add_argument( - "--no-show-editor", - action="store_true", - help="don't pop up editor before writing output", -) -parser.add_argument("--precision", default="3", help="number of digits of precision, default=3") -parser.add_argument( - "--preamble", - help='set commands to be issued before the first command, default="G17 G54 G40 G49 G80 G90\\n"', -) -parser.add_argument( - "--postamble", - help='set commands to be issued after the last command, default="M05\\nG17 G54 G90 G80 G40\\nM2\\n"', -) -parser.add_argument( - "--inches", action="store_true", help="Convert output for US imperial mode (G20)" -) -parser.add_argument( - "--modal", - action="store_true", - help="Output the Same G-command Name USE NonModal Mode", -) -parser.add_argument("--axis-modal", action="store_true", help="Output the Same Axis Value Mode") -parser.add_argument( - "--no-tlo", - action="store_true", - help="suppress tool length offset (G43) following tool changes", -) -TOOLTIP_ARGS = parser.format_help() +class Mach3_Mach4(PostProcessor): + """The Mach3_Mach4 post processor class.""" -# These globals set common customization preferences -OUTPUT_COMMENTS = True -OUTPUT_HEADER = True -OUTPUT_LINE_NUMBERS = False -SHOW_EDITOR = True -MODAL = False # if true commands are suppressed if the same as previous line. -USE_TLO = True # if true G43 will be output following tool changes -OUTPUT_DOUBLES = True # if false duplicate axis values are suppressed if the same as previous line. -COMMAND_SPACE = " " -LINENR = 100 # line number starting value + def __init__( + self, + job, + tooltip=translate("CAM", "Mach3_Mach4 post processor"), + tooltipargs=[""], + units="Metric", + ) -> None: + super().__init__( + job=job, + tooltip=tooltip, + tooltipargs=tooltipargs, + units=units, + ) + Path.Log.debug("Mach3_Mach4 post processor initialized.") -# These globals will be reflected in the Machine configuration of the project -UNITS = "G21" # G21 for metric, G20 for us standard -UNIT_SPEED_FORMAT = "mm/min" -UNIT_FORMAT = "mm" - -MACHINE_NAME = "mach3_4" -CORNER_MIN = {"x": 0, "y": 0, "z": 0} -CORNER_MAX = {"x": 500, "y": 300, "z": 300} -PRECISION = 3 - -# Preamble text will appear at the beginning of the GCODE output file. -PREAMBLE = """G17 G54 G40 G49 G80 G90 -""" - -# Postamble text will appear following the last operation. -POSTAMBLE = """M05 + def init_values(self, values: Values) -> None: + """Initialize values that are used throughout the postprocessor.""" + # + super().init_values(values) + # + # Set any values here that need to override the default values set + # in the parent routine. + # + values["ENABLE_COOLANT"] = True + # + # Used in the argparser code as the "name" of the postprocessor program. + # This would normally show up in the usage message in the TOOLTIP_ARGS. + # + values["MACHINE_NAME"] = "mach3_4" + # + # Enable special processing for operations with "Adaptive" in the name. + # + values["OUTPUT_ADAPTIVE"] = True + # + # Output the machine name for mach3_mach4 instead of the machine units alone. + # + values["OUTPUT_MACHINE_NAME"] = True + # + # The order of parameters. + # + # mach3_mach4 doesn't want K properties on XY plane; Arcs need work. + # + values["PARAMETER_ORDER"] = [ + "X", + "Y", + "Z", + "A", + "B", + "C", + "I", + "J", + "F", + "S", + "T", + "Q", + "R", + "L", + "H", + "D", + "P", + ] + # + # Any commands in this value will be output as the last commands + # in the G-code file. + # + values[ + "POSTAMBLE" + ] = """M05 G17 G54 G90 G80 G40 -M2 -""" +M2""" + values["POSTPROCESSOR_FILE_NAME"] = __name__ + # + # Any commands in this value will be output after the header and + # safety block at the beginning of the G-code file. + # + values["PREAMBLE"] = """G17 G54 G40 G49 G80 G90""" + # + # Output the machine name for mach3_mach4 instead of the machine units alone. + # + values["SHOW_MACHINE_UNITS"] = False -# Pre operation text will be inserted before every operation -PRE_OPERATION = """""" + def init_arguments_visible(self, arguments_visible: Visible) -> None: + """Initialize which argument pairs are visible in TOOLTIP_ARGS.""" + super().init_arguments_visible(arguments_visible) + # + # Modify the visibility of any arguments from the defaults here. + # + arguments_visible["axis-modal"] = True -# Post operation text will be inserted after every operation -POST_OPERATION = """""" - -# Tool Change commands will be inserted before a tool change -TOOL_CHANGE = """""" - - -def processArguments(argstring): - global OUTPUT_HEADER - global OUTPUT_COMMENTS - global OUTPUT_LINE_NUMBERS - global SHOW_EDITOR - global PRECISION - global PREAMBLE - global POSTAMBLE - global UNITS - global UNIT_SPEED_FORMAT - global UNIT_FORMAT - global MODAL - global USE_TLO - global OUTPUT_DOUBLES - - try: - args = parser.parse_args(shlex.split(argstring)) - if args.no_header: - OUTPUT_HEADER = False - if args.no_comments: - OUTPUT_COMMENTS = False - if args.line_numbers: - OUTPUT_LINE_NUMBERS = True - if args.no_show_editor: - SHOW_EDITOR = False - print("Show editor = %d" % SHOW_EDITOR) - PRECISION = args.precision - if args.preamble is not None: - PREAMBLE = args.preamble.replace("\\n", "\n") - if args.postamble is not None: - POSTAMBLE = args.postamble.replace("\\n", "\n") - if args.inches: - UNITS = "G20" - UNIT_SPEED_FORMAT = "in/min" - UNIT_FORMAT = "in" - PRECISION = 4 - if args.modal: - MODAL = True - if args.no_tlo: - USE_TLO = False - if args.axis_modal: - print("here") - OUTPUT_DOUBLES = False - - except Exception: - return False - - return True - - -def export(objectslist, filename, argstring): - if not processArguments(argstring): - return None - global UNITS - global UNIT_FORMAT - global UNIT_SPEED_FORMAT - - for obj in objectslist: - if not hasattr(obj, "Path"): - print( - "the object " + obj.Name + " is not a path. Please select only path and Compounds." - ) - return None - - print("postprocessing...") - gcode = "" - - # write header - if OUTPUT_HEADER: - gcode += linenumber() + "(Exported by FreeCAD)\n" - gcode += linenumber() + "(Post Processor: " + __name__ + ")\n" - gcode += linenumber() + "(Output Time:" + str(now) + ")\n" - - # Write the preamble - if OUTPUT_COMMENTS: - gcode += linenumber() + "(begin preamble)\n" - for line in PREAMBLE.splitlines(): - gcode += linenumber() + line + "\n" - gcode += linenumber() + UNITS + "\n" - - for obj in objectslist: - - # Skip inactive operations - if not PathUtil.activeForOp(obj): - continue - - # do the pre_op - if OUTPUT_COMMENTS: - gcode += linenumber() + "(begin operation: %s)\n" % obj.Label - gcode += linenumber() + "(machine: %s, %s)\n" % ( - MACHINE_NAME, - UNIT_SPEED_FORMAT, - ) - for line in PRE_OPERATION.splitlines(True): - gcode += linenumber() + line - - # get coolant mode - coolantMode = PathUtil.coolantModeForOp(obj) - - # turn coolant on if required - if OUTPUT_COMMENTS: - if not coolantMode == "None": - gcode += linenumber() + "(Coolant On:" + coolantMode + ")\n" - if coolantMode == "Flood": - gcode += linenumber() + "M8" + "\n" - if coolantMode == "Mist": - gcode += linenumber() + "M7" + "\n" - - # process the operation gcode - gcode += parse(obj) - - # do the post_op - if OUTPUT_COMMENTS: - gcode += linenumber() + "(finish operation: %s)\n" % obj.Label - for line in POST_OPERATION.splitlines(True): - gcode += linenumber() + line - - # turn coolant off if required - if not coolantMode == "None": - if OUTPUT_COMMENTS: - gcode += linenumber() + "(Coolant Off:" + coolantMode + ")\n" - gcode += linenumber() + "M9" + "\n" - - # do the post_amble - if OUTPUT_COMMENTS: - gcode += "(begin postamble)\n" - for line in POSTAMBLE.splitlines(): - gcode += linenumber() + line + "\n" - - if FreeCAD.GuiUp and SHOW_EDITOR: - dia = PostUtils.GCodeEditorDialog() - dia.editor.setText(gcode) - result = dia.exec_() - if result: - final = dia.editor.toPlainText() - else: - final = gcode - else: - final = gcode - - print("done postprocessing.") - - if not filename == "-": - gfile = pyopen(filename, "w") - gfile.write(final) - gfile.close() - - return final - - -def linenumber(): - global LINENR - if OUTPUT_LINE_NUMBERS is True: - LINENR += 10 - return "N" + str(LINENR) + " " - return "" - - -def parse(pathobj): - global PRECISION - global MODAL - global OUTPUT_DOUBLES - global UNIT_FORMAT - global UNIT_SPEED_FORMAT - - out = "" - lastcommand = None - precision_string = "." + str(PRECISION) + "f" - currLocation = {} # keep track for no doubles - - # the order of parameters - # mach3_4 doesn't want K properties on XY plane Arcs need work. - params = [ - "X", - "Y", - "Z", - "A", - "B", - "C", - "I", - "J", - "F", - "S", - "T", - "Q", - "R", - "L", - "H", - "D", - "P", - ] - firstmove = Path.Command("G0", {"X": -1, "Y": -1, "Z": -1, "F": 0.0}) - currLocation.update(firstmove.Parameters) # set First location Parameters - - if hasattr(pathobj, "Group"): # We have a compound or project. - # if OUTPUT_COMMENTS: - # out += linenumber() + "(compound: " + pathobj.Label + ")\n" - for p in pathobj.Group: - out += parse(p) - return out - else: # parsing simple path - - # groups might contain non-path things like stock. - if not hasattr(pathobj, "Path"): - return out - - # if OUTPUT_COMMENTS: - # out += linenumber() + "(" + pathobj.Label + ")\n" - - adaptiveOp = False - opHorizRapid = 0 - opVertRapid = 0 - - if "Adaptive" in pathobj.Name: - adaptiveOp = True - if hasattr(pathobj, "ToolController"): - if ( - hasattr(pathobj.ToolController, "HorizRapid") - and pathobj.ToolController.HorizRapid > 0 - ): - opHorizRapid = Units.Quantity( - pathobj.ToolController.HorizRapid, FreeCAD.Units.Velocity - ) - else: - FreeCAD.Console.PrintWarning( - "Tool Controller Horizontal Rapid Values are unset" + "\n" - ) - - if ( - hasattr(pathobj.ToolController, "VertRapid") - and pathobj.ToolController.VertRapid > 0 - ): - opVertRapid = Units.Quantity( - pathobj.ToolController.VertRapid, FreeCAD.Units.Velocity - ) - else: - FreeCAD.Console.PrintWarning( - "Tool Controller Vertical Rapid Values are unset" + "\n" - ) - - for c in PathUtils.getPathWithPlacement(pathobj).Commands: - - outstring = [] - command = c.Name - - if adaptiveOp and c.Name in ["G0", "G00"]: - if opHorizRapid and opVertRapid: - command = "G1" - else: - outstring.append("(Tool Controller Rapid Values are unset)" + "\n") - - outstring.append(command) - - # if modal: suppress the command if it is the same as the last one - if MODAL is True: - if command == lastcommand: - outstring.pop(0) - - if c.Name.startswith("(") and not OUTPUT_COMMENTS: # command is a comment - continue - - # Now add the remaining parameters in order - for param in params: - if param in c.Parameters: - if param == "F" and ( - currLocation[param] != c.Parameters[param] or OUTPUT_DOUBLES - ): - if c.Name not in [ - "G0", - "G00", - ]: # mach3_4 doesn't use rapid speeds - speed = Units.Quantity(c.Parameters["F"], FreeCAD.Units.Velocity) - if speed.getValueAs(UNIT_SPEED_FORMAT) > 0.0: - outstring.append( - param - + format( - float(speed.getValueAs(UNIT_SPEED_FORMAT)), - precision_string, - ) - ) - else: - continue - elif param == "T": - outstring.append(param + str(int(c.Parameters["T"]))) - elif param == "H": - outstring.append(param + str(int(c.Parameters["H"]))) - elif param == "D": - outstring.append(param + str(int(c.Parameters["D"]))) - elif param == "S": - outstring.append(param + str(int(c.Parameters["S"]))) - else: - 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) - outstring.append( - param + format(float(pos.getValueAs(UNIT_FORMAT)), precision_string) - ) - - if adaptiveOp and c.Name in ["G0", "G00"]: - if opHorizRapid and opVertRapid: - if "Z" not in c.Parameters: - outstring.append( - "F" - + format( - float(opHorizRapid.getValueAs(UNIT_SPEED_FORMAT)), - precision_string, - ) - ) - else: - outstring.append( - "F" - + format( - float(opVertRapid.getValueAs(UNIT_SPEED_FORMAT)), - precision_string, - ) - ) - - # store the latest command - lastcommand = command - currLocation.update(c.Parameters) - - # Check for Tool Change: - if command == "M6": - # stop the spindle - out += linenumber() + "M5\n" - for line in TOOL_CHANGE.splitlines(True): - out += linenumber() + line - - # add height offset - if USE_TLO: - tool_height = "\nG43 H" + str(int(c.Parameters["T"])) - outstring.append(tool_height) - - if command == "message": - if OUTPUT_COMMENTS is False: - out = [] - else: - outstring.pop(0) # remove the command - - # prepend a line number and append a newline - if len(outstring) >= 1: - if OUTPUT_LINE_NUMBERS: - outstring.insert(0, (linenumber())) - - # append the line to the final output - for w in outstring: - out += w + COMMAND_SPACE - out = out.strip() + "\n" - - return out - - -# print(__name__ + " gcode postprocessor loaded.") + @property + def tooltip(self): + tooltip: str = """ + This is a postprocessor file for the CAM workbench. + It is used to take a pseudo-gcode fragment from a CAM object + and output 'real' GCode suitable for a Mach3_4 3 axis mill. + """ + return tooltip diff --git a/src/Mod/CAM/Path/Post/scripts/refactored_masso_g3_post.py b/src/Mod/CAM/Path/Post/scripts/masso_g3_post.py similarity index 80% rename from src/Mod/CAM/Path/Post/scripts/refactored_masso_g3_post.py rename to src/Mod/CAM/Path/Post/scripts/masso_g3_post.py index dfb08accaa..2463f4edc6 100644 --- a/src/Mod/CAM/Path/Post/scripts/refactored_masso_g3_post.py +++ b/src/Mod/CAM/Path/Post/scripts/masso_g3_post.py @@ -28,7 +28,7 @@ from typing import Any, Dict -from Path.Post.scripts.refactored_linuxcnc_post import Refactored_Linuxcnc +from Path.Post.Processor import PostProcessor import Path import FreeCAD @@ -48,13 +48,13 @@ else: Values = Dict[str, Any] -class Refactored_Masso_G3(Refactored_Linuxcnc): - """The Refactored Masso G3 post processor class.""" +class Masso_G3(PostProcessor): + """The Masso G3 post processor class.""" def __init__( self, job, - tooltip=translate("CAM", "Refactored Masso G3 post processor"), + tooltip=translate("CAM", "Masso G3 post processor"), tooltipargs=[""], units="Metric", ) -> None: @@ -64,25 +64,39 @@ class Refactored_Masso_G3(Refactored_Linuxcnc): tooltipargs=tooltipargs, units=units, ) - Path.Log.debug("Refactored Masso G3 post processor initialized.") + Path.Log.debug("Masso G3 post processor initialized.") def init_values(self, values: Values) -> None: """Initialize values that are used throughout the postprocessor.""" - # super().init_values(values) - # - # Set any values here that need to override the default values set - # in the parent routine. - # - # - # Used in the argparser code as the "name" of the postprocessor program. - # + values["ENABLE_COOLANT"] = True + values["PARAMETER_ORDER"] = [ + "X", + "Y", + "Z", + "A", + "B", + "C", + "I", + "J", + "F", + "S", + "T", + "Q", + "R", + "L", + "H", + "D", + "P", + ] + values[ + "POSTAMBLE" + ] = """M05 +G17 G54 G90 G80 G40 +M2""" + values["PREAMBLE"] = """G17 G54 G40 G49 G80 G90""" values["MACHINE_NAME"] = "Masso G3" values["POSTPROCESSOR_FILE_NAME"] = __name__ - # - # setting TOOL_BEFORE_CHANGE to True will output T# M6 before each tool change - # rather than M6 T#. - # values["TOOL_BEFORE_CHANGE"] = True @property diff --git a/src/Mod/CAM/Path/Post/scripts/refactored_centroid_post.py b/src/Mod/CAM/Path/Post/scripts/refactored_centroid_post.py deleted file mode 100644 index 3db3944fec..0000000000 --- a/src/Mod/CAM/Path/Post/scripts/refactored_centroid_post.py +++ /dev/null @@ -1,196 +0,0 @@ -# SPDX-License-Identifier: LGPL-2.1-or-later - -# *************************************************************************** -# * Copyright (c) 2014 sliptonic * -# * Copyright (c) 2022 - 2025 Larry Woestman * -# * Copyright (c) 2024 Ondsel * -# * * -# * This file is part of the FreeCAD CAx development system. * -# * * -# * This program is free software; you can redistribute it and/or modify * -# * it under the terms of the GNU Lesser General Public License (LGPL) * -# * as published by the Free Software Foundation; either version 2 of * -# * the License, or (at your option) any later version. * -# * for detail see the LICENCE text file. * -# * * -# * FreeCAD is distributed in the hope that it will be useful, * -# * but WITHOUT ANY WARRANTY; without even the implied warranty of * -# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -# * GNU Lesser General Public License for more details. * -# * * -# * You should have received a copy of the GNU Library General Public * -# * License along with FreeCAD; if not, write to the Free Software * -# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * -# * USA * -# * * -# *************************************************************************** - -from typing import Any, Dict - -from Path.Post.Processor import PostProcessor - -import Path -import FreeCAD - -translate = FreeCAD.Qt.translate - -DEBUG = False -if DEBUG: - Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) - Path.Log.trackModule(Path.Log.thisModule()) -else: - Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule()) - -# -# Define some types that are used throughout this file. -# -Values = Dict[str, Any] -Visible = Dict[str, bool] - - -class Refactored_Centroid(PostProcessor): - """The Refactored Centroid post processor class.""" - - def __init__( - self, - job, - tooltip=translate("CAM", "Refactored Centroid post processor"), - tooltipargs=[""], - units="Metric", - ) -> None: - super().__init__( - job=job, - tooltip=tooltip, - tooltipargs=tooltipargs, - units=units, - ) - Path.Log.debug("Refactored Centroid post processor initialized.") - - def init_values(self, values: Values) -> None: - """Initialize values that are used throughout the postprocessor.""" - # - super().init_values(values) - # - # Set any values here that need to override the default values set - # in the parent routine. - # - # Use 4 digits for axis precision by default. - # - values["AXIS_PRECISION"] = 4 - values["DEFAULT_AXIS_PRECISION"] = 4 - values["DEFAULT_INCH_AXIS_PRECISION"] = 4 - # - # Use ";" as the comment symbol - # - values["COMMENT_SYMBOL"] = ";" - # - # Use 1 digit for feed precision by default. - # - values["FEED_PRECISION"] = 1 - values["DEFAULT_FEED_PRECISION"] = 1 - values["DEFAULT_INCH_FEED_PRECISION"] = 1 - # - # This value usually shows up in the post_op comment as "Finish operation:". - # Change it to "End" to produce "End operation:". - # - values["FINISH_LABEL"] = "End" - # - # If this value is True, then a list of tool numbers - # with their labels are output just before the preamble. - # - values["LIST_TOOLS_IN_PREAMBLE"] = True - # - # Used in the argparser code as the "name" of the postprocessor program. - # This would normally show up in the usage message in the TOOLTIP_ARGS. - # - values["MACHINE_NAME"] = "Centroid" - # - # This list controls the order of parameters in a line during output. - # centroid doesn't want K properties on XY plane; Arcs need work. - # - values["PARAMETER_ORDER"] = [ - "X", - "Y", - "Z", - "A", - "B", - "I", - "J", - "F", - "S", - "T", - "Q", - "R", - "L", - "H", - ] - # - # Any commands in this value will be output as the last commands - # in the G-code file. - # - values["POSTAMBLE"] = """M99""" - values["POSTPROCESSOR_FILE_NAME"] = __name__ - # - # Any commands in this value will be output after the header and - # safety block at the beginning of the G-code file. - # - values["PREAMBLE"] = """G53 G00 G17""" - # - # Output any messages. - # - values["REMOVE_MESSAGES"] = False - # - # Any commands in this value are output after the header but before the preamble, - # then again after the TOOLRETURN but before the POSTAMBLE. - # - values["SAFETYBLOCK"] = """G90 G80 G40 G49""" - # - # Do not show the current machine units just before the PRE_OPERATION. - # - values["SHOW_MACHINE_UNITS"] = False - # - # Do not show the current operation label just before the PRE_OPERATION. - # - values["SHOW_OPERATION_LABELS"] = False - # - # Do not output an M5 command to stop the spindle for tool changes. - # - values["STOP_SPINDLE_FOR_TOOL_CHANGE"] = False - # - # spindle off, height offset canceled, spindle retracted - # (M25 is a centroid command to retract spindle) - # - values[ - "TOOLRETURN" - ] = """M5 -M25 -G49 H0""" - # - # Default to not outputting a G43 following tool changes - # - values["USE_TLO"] = False - # - # This was in the original centroid postprocessor file - # but does not appear to be used anywhere. - # - # ZAXISRETURN = """G91 G28 X0 Z0 G90""" - # - - def init_arguments_visible(self, arguments_visible: Visible) -> None: - """Initialize which argument pairs are visible in TOOLTIP_ARGS.""" - super().init_arguments_visible(arguments_visible) - # - # Modify the visibility of any arguments from the defaults here. - # - arguments_visible["axis-modal"] = False - arguments_visible["precision"] = False - arguments_visible["tlo"] = False - - @property - def tooltip(self): - tooltip: str = """ - This is a postprocessor file for the CAM workbench. - It is used to take a pseudo-gcode fragment from a CAM object - and output 'real' GCode suitable for a centroid 3 axis mill. - """ - return tooltip diff --git a/src/Mod/CAM/Path/Post/scripts/refactored_grbl_post.py b/src/Mod/CAM/Path/Post/scripts/refactored_grbl_post.py deleted file mode 100644 index 6304710fc2..0000000000 --- a/src/Mod/CAM/Path/Post/scripts/refactored_grbl_post.py +++ /dev/null @@ -1,190 +0,0 @@ -# SPDX-License-Identifier: LGPL-2.1-or-later - -# *************************************************************************** -# * Copyright (c) 2014 sliptonic * -# * Copyright (c) 2022 - 2025 Larry Woestman * -# * Copyright (c) 2024 Ondsel * -# * * -# * This file is part of the FreeCAD CAx development system. * -# * * -# * This program is free software; you can redistribute it and/or modify * -# * it under the terms of the GNU Lesser General Public License (LGPL) * -# * as published by the Free Software Foundation; either version 2 of * -# * the License, or (at your option) any later version. * -# * for detail see the LICENCE text file. * -# * * -# * FreeCAD is distributed in the hope that it will be useful, * -# * but WITHOUT ANY WARRANTY; without even the implied warranty of * -# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -# * GNU Lesser General Public License for more details. * -# * * -# * You should have received a copy of the GNU Library General Public * -# * License along with FreeCAD; if not, write to the Free Software * -# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * -# * USA * -# * * -# *************************************************************************** - -import argparse - -from typing import Any, Dict - -from Path.Post.Processor import PostProcessor - -import Path -import FreeCAD - -translate = FreeCAD.Qt.translate - -DEBUG = False -if DEBUG: - Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) - Path.Log.trackModule(Path.Log.thisModule()) -else: - Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule()) - -# -# Define some types that are used throughout this file. -# -Defaults = Dict[str, bool] -Values = Dict[str, Any] -Visible = Dict[str, bool] - - -class Refactored_Grbl(PostProcessor): - """The Refactored Grbl post processor class.""" - - def __init__( - self, - job, - tooltip=translate("CAM", "Refactored Grbl post processor"), - tooltipargs=[""], - units="Metric", - ) -> None: - super().__init__( - job=job, - tooltip=tooltip, - tooltipargs=tooltipargs, - units=units, - ) - Path.Log.debug("Refactored Grbl post processor initialized.") - - def init_values(self, values: Values) -> None: - """Initialize values that are used throughout the postprocessor.""" - # - super().init_values(values) - # - # Set any values here that need to override the default values set - # in the parent routine. - # - values["ENABLE_COOLANT"] = True - # - # If this is set to True, then commands that are placed in - # comments that look like (MC_RUN_COMMAND: blah) will be output. - # - values["ENABLE_MACHINE_SPECIFIC_COMMANDS"] = True - # - # Used in the argparser code as the "name" of the postprocessor program. - # This would normally show up in the usage message in the TOOLTIP_ARGS. - # - values["MACHINE_NAME"] = "Grbl" - # - # Default to outputting Path labels at the beginning of each Path. - # - values["OUTPUT_PATH_LABELS"] = True - # - # Default to not outputting M6 tool changes (comment it) as grbl - # currently does not handle it. - # - values["OUTPUT_TOOL_CHANGE"] = False - # - # The order of the parameters. - # Arcs may only work on the XY plane (this needs to be verified). - # - values["PARAMETER_ORDER"] = [ - "X", - "Y", - "Z", - "A", - "B", - "C", - "U", - "V", - "W", - "I", - "J", - "K", - "F", - "S", - "T", - "Q", - "R", - "L", - "P", - ] - # - # Any commands in this value will be output as the last commands in the G-code file. - # - values[ - "POSTAMBLE" - ] = """M5 -G17 G90 -M2""" - values["POSTPROCESSOR_FILE_NAME"] = __name__ - # - # Any commands in this value will be output after the header and - # safety block at the beginning of the G-code file. - # - values["PREAMBLE"] = """G17 G90""" - # - # Do not show the current machine units just before the PRE_OPERATION. - # - values["SHOW_MACHINE_UNITS"] = False - # - # Default to not outputting a G43 following tool changes - # - values["USE_TLO"] = False - - def init_argument_defaults(self, argument_defaults: Defaults) -> None: - """Initialize which arguments (in a pair) are shown as the default argument.""" - super().init_argument_defaults(argument_defaults) - # - # Modify which argument to show as the default in flag-type arguments here. - # If the value is True, the first argument will be shown as the default. - # If the value is False, the second argument will be shown as the default. - # - # For example, if you want to show Metric mode as the default, use: - # argument_defaults["metric_inch"] = True - # - # If you want to show that "Don't pop up editor for writing output" is - # the default, use: - # argument_defaults["show-editor"] = False. - # - # Note: You also need to modify the corresponding entries in the "values" hash - # to actually make the default value(s) change to match. - # - argument_defaults["tlo"] = False - argument_defaults["tool_change"] = False - - def init_arguments_visible(self, arguments_visible: Visible) -> None: - """Initialize which argument pairs are visible in TOOLTIP_ARGS.""" - super().init_arguments_visible(arguments_visible) - # - # Modify the visibility of any arguments from the defaults here. - # - arguments_visible["bcnc"] = True - arguments_visible["axis-modal"] = False - arguments_visible["return-to"] = True - arguments_visible["tlo"] = False - arguments_visible["tool_change"] = True - arguments_visible["translate_drill"] = True - arguments_visible["wait-for-spindle"] = True - - @property - def tooltip(self): - tooltip: str = """ - This is a postprocessor file for the CAM workbench. - It is used to take a pseudo-gcode fragment from a CAM object - and output 'real' GCode suitable for a Grbl 3 axis mill. - """ - return tooltip diff --git a/src/Mod/CAM/Path/Post/scripts/refactored_linuxcnc_post.py b/src/Mod/CAM/Path/Post/scripts/refactored_linuxcnc_post.py deleted file mode 100644 index 0cfecba7c0..0000000000 --- a/src/Mod/CAM/Path/Post/scripts/refactored_linuxcnc_post.py +++ /dev/null @@ -1,129 +0,0 @@ -# SPDX-License-Identifier: LGPL-2.1-or-later - -# *************************************************************************** -# * Copyright (c) 2014 sliptonic * -# * Copyright (c) 2022 - 2025 Larry Woestman * -# * Copyright (c) 2024 Ondsel * -# * Copyright (c) 2024 Carl Slater * -# * * -# * This file is part of the FreeCAD CAx development system. * -# * * -# * This program is free software; you can redistribute it and/or modify * -# * it under the terms of the GNU Lesser General Public License (LGPL) * -# * as published by the Free Software Foundation; either version 2 of * -# * the License, or (at your option) any later version. * -# * for detail see the LICENCE text file. * -# * * -# * FreeCAD is distributed in the hope that it will be useful, * -# * but WITHOUT ANY WARRANTY; without even the implied warranty of * -# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -# * GNU Lesser General Public License for more details. * -# * * -# * You should have received a copy of the GNU Library General Public * -# * License along with FreeCAD; if not, write to the Free Software * -# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * -# * USA * -# * * -# *************************************************************************** - -from typing import Any, Dict - -from Path.Post.Processor import PostProcessor - -import Path -import FreeCAD - -translate = FreeCAD.Qt.translate - -DEBUG = False -if DEBUG: - Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) - Path.Log.trackModule(Path.Log.thisModule()) -else: - Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule()) - -# -# Define some types that are used throughout this file. -# -Values = Dict[str, Any] - - -class Refactored_Linuxcnc(PostProcessor): - """The Refactored LinuxCNC post processor class.""" - - def __init__( - self, - job, - tooltip=translate("CAM", "Refactored LinuxCNC post processor"), - tooltipargs=[""], - units="Metric", - ) -> None: - super().__init__( - job=job, - tooltip=tooltip, - tooltipargs=tooltipargs, - units=units, - ) - Path.Log.debug("Refactored LinuxCNC post processor initialized.") - - def init_values(self, values: Values) -> None: - """Initialize values that are used throughout the postprocessor.""" - # - super().init_values(values) - # - # Set any values here that need to override the default values set - # in the parent routine. - # - values["ENABLE_COOLANT"] = True - # - # The order of parameters. - # - # linuxcnc doesn't want K properties on XY plane; Arcs need work. - # - values["PARAMETER_ORDER"] = [ - "X", - "Y", - "Z", - "A", - "B", - "C", - "I", - "J", - "F", - "S", - "T", - "Q", - "R", - "L", - "H", - "D", - "P", - ] - # - # Used in the argparser code as the "name" of the postprocessor program. - # - values["MACHINE_NAME"] = "LinuxCNC" - # - # Any commands in this value will be output as the last commands - # in the G-code file. - # - values[ - "POSTAMBLE" - ] = """M05 -G17 G54 G90 G80 G40 -M2""" - values["POSTPROCESSOR_FILE_NAME"] = __name__ - # - # Any commands in this value will be output after the header and - # safety block at the beginning of the G-code file. - # - values["PREAMBLE"] = """G17 G54 G40 G49 G80 G90""" - - @property - def tooltip(self): - tooltip: str = """ - This is a postprocessor file for the CAM workbench. - It is used to take a pseudo-gcode fragment from a CAM object - and output 'real' GCode suitable for a linuxcnc 3 axis mill. - """ - return tooltip diff --git a/src/Mod/CAM/Path/Post/scripts/refactored_mach3_mach4_post.py b/src/Mod/CAM/Path/Post/scripts/refactored_mach3_mach4_post.py deleted file mode 100644 index fddd3a66ed..0000000000 --- a/src/Mod/CAM/Path/Post/scripts/refactored_mach3_mach4_post.py +++ /dev/null @@ -1,150 +0,0 @@ -# SPDX-License-Identifier: LGPL-2.1-or-later - -# *************************************************************************** -# * Copyright (c) 2014 sliptonic * -# * Copyright (c) 2022 - 2025 Larry Woestman * -# * Copyright (c) 2024 Ondsel * -# * * -# * This file is part of the FreeCAD CAx development system. * -# * * -# * This program is free software; you can redistribute it and/or modify * -# * it under the terms of the GNU Lesser General Public License (LGPL) * -# * as published by the Free Software Foundation; either version 2 of * -# * the License, or (at your option) any later version. * -# * for detail see the LICENCE text file. * -# * * -# * FreeCAD is distributed in the hope that it will be useful, * -# * but WITHOUT ANY WARRANTY; without even the implied warranty of * -# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -# * GNU Lesser General Public License for more details. * -# * * -# * You should have received a copy of the GNU Library General Public * -# * License along with FreeCAD; if not, write to the Free Software * -# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * -# * USA * -# * * -# *************************************************************************** - -from typing import Any, Dict - -from Path.Post.Processor import PostProcessor - -import Path -import FreeCAD - -translate = FreeCAD.Qt.translate - -DEBUG = False -if DEBUG: - Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) - Path.Log.trackModule(Path.Log.thisModule()) -else: - Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule()) - -# -# Define some types that are used throughout this file. -# -Values = Dict[str, Any] -Visible = Dict[str, bool] - - -class Refactored_Mach3_Mach4(PostProcessor): - """The Refactored Mach3_Mach4 post processor class.""" - - def __init__( - self, - job, - tooltip=translate("CAM", "Refactored Mach3_Mach4 post processor"), - tooltipargs=[""], - units="Metric", - ) -> None: - super().__init__( - job=job, - tooltip=tooltip, - tooltipargs=tooltipargs, - units=units, - ) - Path.Log.debug("Refactored Mach3_Mach4 post processor initialized.") - - def init_values(self, values: Values) -> None: - """Initialize values that are used throughout the postprocessor.""" - # - super().init_values(values) - # - # Set any values here that need to override the default values set - # in the parent routine. - # - values["ENABLE_COOLANT"] = True - # - # Used in the argparser code as the "name" of the postprocessor program. - # This would normally show up in the usage message in the TOOLTIP_ARGS. - # - values["MACHINE_NAME"] = "mach3_4" - # - # Enable special processing for operations with "Adaptive" in the name. - # - values["OUTPUT_ADAPTIVE"] = True - # - # Output the machine name for mach3_mach4 instead of the machine units alone. - # - values["OUTPUT_MACHINE_NAME"] = True - # - # The order of parameters. - # - # mach3_mach4 doesn't want K properties on XY plane; Arcs need work. - # - values["PARAMETER_ORDER"] = [ - "X", - "Y", - "Z", - "A", - "B", - "C", - "I", - "J", - "F", - "S", - "T", - "Q", - "R", - "L", - "H", - "D", - "P", - ] - # - # Any commands in this value will be output as the last commands - # in the G-code file. - # - values[ - "POSTAMBLE" - ] = """M05 -G17 G54 G90 G80 G40 -M2""" - values["POSTPROCESSOR_FILE_NAME"] = __name__ - # - # Any commands in this value will be output after the header and - # safety block at the beginning of the G-code file. - # - values["PREAMBLE"] = """G17 G54 G40 G49 G80 G90""" - # - # Output the machine name for mach3_mach4 instead of the machine units alone. - # - values["SHOW_MACHINE_UNITS"] = False - - def init_arguments_visible(self, arguments_visible: Visible) -> None: - """Initialize which argument pairs are visible in TOOLTIP_ARGS.""" - super().init_arguments_visible(arguments_visible) - # - # Modify the visibility of any arguments from the defaults here. - # - arguments_visible["axis-modal"] = True - - @property - def tooltip(self): - tooltip: str = """ - This is a postprocessor file for the CAM workbench. - It is used to take a pseudo-gcode fragment from a CAM object - and output 'real' GCode suitable for a Mach3_4 3 axis mill. - """ - return tooltip diff --git a/src/Mod/CAM/Path/Post/scripts/smoothie_post.py b/src/Mod/CAM/Path/Post/scripts/smoothie_post.py index b0476015d3..17d6f4ebc1 100644 --- a/src/Mod/CAM/Path/Post/scripts/smoothie_post.py +++ b/src/Mod/CAM/Path/Post/scripts/smoothie_post.py @@ -23,441 +23,265 @@ # * * # *************************************************************************** +import os +import socket +import sys +from typing import Any, Dict, Optional -import argparse -import datetime -import Path.Post.Utils as PostUtils -import PathScripts.PathUtils as PathUtils +from Path.Post.Processor import PostProcessor + +import Path import FreeCAD -from FreeCAD import Units -import shlex -from builtins import open as pyopen -TOOLTIP = """ -This is a postprocessor file for the Path workbench. It is used to -take a pseudo-G-code fragment outputted by a Path object, and output -real G-code suitable for a smoothieboard. This postprocessor, once placed -in the appropriate PathScripts folder, can be used directly from inside -FreeCAD, via the GUI importer or via python scripts with: +translate = FreeCAD.Qt.translate -import smoothie_post -smoothie_post.export(object,"/path/to/file.ncc","") -""" - -now = datetime.datetime.now() - -parser = argparse.ArgumentParser(prog="linuxcnc", add_help=False) -parser.add_argument("--header", action="store_true", help="output headers (default)") -parser.add_argument("--no-header", action="store_true", help="suppress header output") -parser.add_argument("--comments", action="store_true", help="output comment (default)") -parser.add_argument("--no-comments", action="store_true", help="suppress comment output") -parser.add_argument("--line-numbers", action="store_true", help="prefix with line numbers") -parser.add_argument( - "--no-line-numbers", - action="store_true", - help="don't prefix with line numbers (default)", -) -parser.add_argument( - "--show-editor", - action="store_true", - help="pop up editor before writing output (default)", -) -parser.add_argument( - "--no-show-editor", - action="store_true", - help="don't pop up editor before writing output", -) -parser.add_argument("--precision", default="4", help="number of digits of precision, default=4") -parser.add_argument( - "--preamble", - help='set commands to be issued before the first command, default="G17\\nG90\\n"', -) -parser.add_argument( - "--postamble", - help='set commands to be issued after the last command, default="M05\\nG17 G90\\nM2\\n"', -) -parser.add_argument("--IP_ADDR", help="IP Address for machine target machine") -parser.add_argument( - "--verbose", - action="store_true", - help='verbose output for debugging, default="False"', -) -parser.add_argument( - "--inches", action="store_true", help="Convert output for US imperial mode (G20)" -) - -TOOLTIP_ARGS = parser.format_help() - -# These globals set common customization preferences -OUTPUT_COMMENTS = True -OUTPUT_HEADER = True -OUTPUT_LINE_NUMBERS = False -IP_ADDR = None -VERBOSE = False - -SPINDLE_SPEED = 0.0 - -if FreeCAD.GuiUp: - SHOW_EDITOR = True +DEBUG = False +if DEBUG: + Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) + Path.Log.trackModule(Path.Log.thisModule()) else: - SHOW_EDITOR = False -MODAL = False # if true commands are suppressed if the same as previous line. -COMMAND_SPACE = " " -LINENR = 100 # line number starting value + Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule()) -# These globals will be reflected in the Machine configuration of the project -UNITS = "G21" # G21 for metric, G20 for us standard -UNIT_SPEED_FORMAT = "mm/min" -UNIT_FORMAT = "mm" +# +# Define some types that are used throughout this file. +# +Values = Dict[str, Any] -MACHINE_NAME = "SmoothieBoard" -CORNER_MIN = {"x": 0, "y": 0, "z": 0} -CORNER_MAX = {"x": 500, "y": 300, "z": 300} -# Preamble text will appear at the beginning of the GCODE output file. -PREAMBLE = """G17 G90 -""" +class Smoothie(PostProcessor): + """ + The SmoothieBoard post processor class. -# Postamble text will appear following the last operation. -POSTAMBLE = """M05 + This postprocessor outputs G-code suitable for SmoothieBoard controllers. + It supports direct network upload to the SmoothieBoard via TCP/IP. + """ + + def __init__( + self, + job, + tooltip=translate("CAM", "Refactored SmoothieBoard post processor"), + tooltipargs=["ip-addr", "verbose"], + units="Metric", + ) -> None: + super().__init__( + job=job, + tooltip=tooltip, + tooltipargs=tooltipargs, + units=units, + ) + Path.Log.debug("Refactored SmoothieBoard post processor initialized.") + self.ip_addr: Optional[str] = None + self.verbose: bool = False + + def init_values(self, values: Values) -> None: + """Initialize values that are used throughout the postprocessor.""" + # + super().init_values(values) + # + # Set any values here that need to override the default values set + # in the parent routine. + # + # The order of parameters. + # SmoothieBoard doesn't want K properties on XY plane (like LinuxCNC). + # + values["PARAMETER_ORDER"] = [ + "X", + "Y", + "Z", + "A", + "B", + "I", + "J", + "F", + "S", + "T", + "Q", + "R", + "L", + ] + # + # Used in the argparser code as the "name" of the postprocessor program. + # + values["MACHINE_NAME"] = "SmoothieBoard" + # + # Any commands in this value will be output as the last commands + # in the G-code file. + # + values[ + "POSTAMBLE" + ] = """M05 G17 G90 -M2 -""" +M2""" + values["POSTPROCESSOR_FILE_NAME"] = __name__ + # + # Any commands in this value will be output after the header and + # safety block at the beginning of the G-code file. + # + values["PREAMBLE"] = """G17 G90""" + def init_arguments(self, values, argument_defaults, arguments_visible): + """Initialize command-line arguments, including SmoothieBoard-specific options.""" + parser = super().init_arguments(values, argument_defaults, arguments_visible) -# Pre operation text will be inserted before every operation -PRE_OPERATION = """""" + # Add SmoothieBoard-specific argument group + smoothie_group = parser.add_argument_group("SmoothieBoard-specific arguments") -# Post operation text will be inserted after every operation -POST_OPERATION = """""" + smoothie_group.add_argument( + "--ip-addr", help="IP address for direct upload to SmoothieBoard (e.g., 192.168.1.100)" + ) -# Tool Change commands will be inserted before a tool change -TOOL_CHANGE = """""" + smoothie_group.add_argument( + "--verbose", + action="store_true", + help="Enable verbose output for network transfer debugging", + ) -# Number of digits after the decimal point -PRECISION = 5 + return parser + def process_arguments(self): + """Process arguments and update values, including SmoothieBoard-specific settings.""" + flag, args = super().process_arguments() -def processArguments(argstring): - global OUTPUT_HEADER - global OUTPUT_COMMENTS - global OUTPUT_LINE_NUMBERS - global SHOW_EDITOR - global IP_ADDR - global VERBOSE - global PRECISION - global PREAMBLE - global POSTAMBLE - global UNITS - global UNIT_SPEED_FORMAT - global UNIT_FORMAT + if flag and args: + # Update SmoothieBoard-specific values from parsed arguments + if hasattr(args, "ip_addr") and args.ip_addr: + self.ip_addr = args.ip_addr + Path.Log.info(f"SmoothieBoard IP address set to: {self.ip_addr}") - try: - args = parser.parse_args(shlex.split(argstring)) + if hasattr(args, "verbose"): + self.verbose = args.verbose + if self.verbose: + Path.Log.info("Verbose mode enabled") - if args.no_header: - OUTPUT_HEADER = False - if args.header: - OUTPUT_HEADER = True - if args.no_comments: - OUTPUT_COMMENTS = False - if args.comments: - OUTPUT_COMMENTS = True - if args.no_line_numbers: - OUTPUT_LINE_NUMBERS = False - if args.line_numbers: - OUTPUT_LINE_NUMBERS = True - if args.no_show_editor: - SHOW_EDITOR = False - if args.show_editor: - SHOW_EDITOR = True - print("Show editor = %d" % SHOW_EDITOR) - PRECISION = args.precision - if args.preamble is not None: - PREAMBLE = args.preamble.replace("\\n", "\n") - if args.postamble is not None: - POSTAMBLE = args.postamble.replace("\\n", "\n") - if args.inches: - UNITS = "G20" - UNIT_SPEED_FORMAT = "in/min" - UNIT_FORMAT = "in" + return flag, args - IP_ADDR = args.IP_ADDR - VERBOSE = args.verbose + def export(self): + """Override export to handle network upload to SmoothieBoard.""" + # First, do the standard export processing + gcode_sections = super().export() - except Exception: - return False + if gcode_sections is None: + return None - return True + # If IP address is specified, send to SmoothieBoard instead of writing to file + if self.ip_addr: + # Combine all G-code sections + gcode = "" + for section_name, section_gcode in gcode_sections: + if section_gcode: + gcode += section_gcode + # Get the output filename from the job + filename = self._job.PostProcessorOutputFile + if not filename or filename == "-": + filename = "output.nc" -def export(objectslist, filename, argstring): - processArguments(argstring) - global UNITS - for obj in objectslist: - if not hasattr(obj, "Path"): + self._send_to_smoothie(self.ip_addr, gcode, filename) + + # Return the gcode for display/editor + return gcode_sections + + # Normal file-based export + return gcode_sections + + def _send_to_smoothie(self, ip: str, gcode: str, fname: str) -> None: + """ + Send G-code directly to SmoothieBoard via network. + + Args: + ip: IP address of the SmoothieBoard + gcode: G-code string to send + fname: Filename to use on the SmoothieBoard SD card + """ + fname = os.path.basename(fname) + FreeCAD.Console.PrintMessage(f"Sending to SmoothieBoard: {fname}\n") + + gcode = gcode.rstrip() + filesize = len(gcode) + + try: + # Make connection to SmoothieBoard SFTP server (port 115) + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(4.0) + s.connect((ip, 115)) + tn = s.makefile(mode="rw") + + # Read startup prompt + ln = tn.readline() + if not ln.startswith("+"): + FreeCAD.Console.PrintError(f"Failed to connect with SFTP: {ln}\n") + return + + if self.verbose: + print("RSP: " + ln.strip()) + + # Issue initial store command + tn.write(f"STOR OLD /sd/{fname}\n") + tn.flush() + + ln = tn.readline() + if not ln.startswith("+"): + FreeCAD.Console.PrintError(f"Failed to create file: {ln}\n") + return + + if self.verbose: + print("RSP: " + ln.strip()) + + # Send size of file + tn.write(f"SIZE {filesize}\n") + tn.flush() + + ln = tn.readline() + if not ln.startswith("+"): + FreeCAD.Console.PrintError(f"Failed: {ln}\n") + return + + if self.verbose: + print("RSP: " + ln.strip()) + + # Now send file + cnt = 0 + for line in gcode.splitlines(True): + tn.write(line) + if self.verbose: + cnt += len(line) + print("SND: " + line.strip()) + print(f"{cnt}/{filesize}\r", end="") + + tn.flush() + + ln = tn.readline() + if not ln.startswith("+"): + FreeCAD.Console.PrintError(f"Failed to save file: {ln}\n") + return + + if self.verbose: + print("RSP: " + ln.strip()) + + # Exit + tn.write("DONE\n") + tn.flush() + tn.close() + + FreeCAD.Console.PrintMessage("Upload complete\n") + + except socket.timeout: + FreeCAD.Console.PrintError(f"Connection timeout while connecting to {ip}:115\n") + except ConnectionRefusedError: FreeCAD.Console.PrintError( - "the object " - + obj.Name - + " is not a path. Please select only path and Compounds.\n" + f"Connection refused by {ip}:115. Is the SmoothieBoard running?\n" ) - return + except Exception as e: + FreeCAD.Console.PrintError(f"Error sending to SmoothieBoard: {str(e)}\n") - FreeCAD.Console.PrintMessage("postprocessing...\n") - gcode = "" + @property + def tooltip(self): + tooltip: str = """ + This is a postprocessor file for the CAM workbench. + It is used to take a pseudo-gcode fragment from a CAM object + and output 'real' GCode suitable for a SmoothieBoard controller. - # Find the machine. - # The user my have overridden post processor defaults in the GUI. Make - # sure we're using the current values in the Machine Def. - myMachine = None - for pathobj in objectslist: - if hasattr(pathobj, "MachineName"): - myMachine = pathobj.MachineName - if hasattr(pathobj, "MachineUnits"): - if pathobj.MachineUnits == "Metric": - UNITS = "G21" - else: - UNITS = "G20" - if myMachine is None: - FreeCAD.Console.PrintWarning("No machine found in this selection\n") - - # write header - if OUTPUT_HEADER: - gcode += linenumber() + "(Exported by FreeCAD)\n" - gcode += linenumber() + "(Post Processor: " + __name__ + ")\n" - gcode += linenumber() + "(Output Time:" + str(now) + ")\n" - - # Write the preamble - if OUTPUT_COMMENTS: - gcode += linenumber() + "(begin preamble)\n" - for line in PREAMBLE.splitlines(): - gcode += linenumber() + line + "\n" - gcode += linenumber() + UNITS + "\n" - - for obj in objectslist: - - # do the pre_op - if OUTPUT_COMMENTS: - gcode += linenumber() + "(begin operation: " + obj.Label + ")\n" - for line in PRE_OPERATION.splitlines(True): - gcode += linenumber() + line - - gcode += parse(obj) - - # do the post_op - if OUTPUT_COMMENTS: - gcode += linenumber() + "(finish operation: " + obj.Label + ")\n" - for line in POST_OPERATION.splitlines(True): - gcode += linenumber() + line - - # do the post_amble - - if OUTPUT_COMMENTS: - gcode += "(begin postamble)\n" - for line in POSTAMBLE.splitlines(): - gcode += linenumber() + line + "\n" - - if SHOW_EDITOR: - dia = PostUtils.GCodeEditorDialog() - dia.editor.setText(gcode) - result = dia.exec_() - if result: - final = dia.editor.toPlainText() - else: - final = gcode - else: - final = gcode - - if IP_ADDR is not None: - sendToSmoothie(IP_ADDR, final, filename) - else: - - if not filename == "-": - gfile = pyopen(filename, "w") - gfile.write(final) - gfile.close() - - FreeCAD.Console.PrintMessage("done postprocessing.\n") - return final - - -def sendToSmoothie(ip, GCODE, fname): - import sys - import socket - import os - - fname = os.path.basename(fname) - FreeCAD.Console.PrintMessage("sending to smoothie: {}\n".format(fname)) - - f = GCODE.rstrip() - filesize = len(f) - # make connection to sftp server - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.settimeout(4.0) - s.connect((ip, 115)) - tn = s.makefile(mode="rw") - - # read startup prompt - ln = tn.readline() - if not ln.startswith("+"): - FreeCAD.Console.PrintMessage("Failed to connect with sftp: {}\n".format(ln)) - sys.exit() - - if VERBOSE: - print("RSP: " + ln.strip()) - - # Issue initial store command - tn.write("STOR OLD /sd/" + fname + "\n") - tn.flush() - - ln = tn.readline() - if not ln.startswith("+"): - FreeCAD.Console.PrintError("Failed to create file: {}\n".format(ln)) - sys.exit() - - if VERBOSE: - print("RSP: " + ln.strip()) - - # send size of file - tn.write("SIZE " + str(filesize) + "\n") - tn.flush() - - ln = tn.readline() - if not ln.startswith("+"): - FreeCAD.Console.PrintError("Failed: {}\n".format(ln)) - sys.exit() - - if VERBOSE: - print("RSP: " + ln.strip()) - - cnt = 0 - # now send file - for line in f.splitlines(1): - tn.write(line) - if VERBOSE: - cnt += len(line) - print("SND: " + line.strip()) - print(str(cnt) + "/" + str(filesize) + "\r", end="") - - tn.flush() - - ln = tn.readline() - if not ln.startswith("+"): - FreeCAD.Console.PrintError("Failed to save file: {}\n".format(ln)) - sys.exit() - - if VERBOSE: - print("RSP: " + ln.strip()) - - # exit - tn.write("DONE\n") - tn.flush() - tn.close() - - FreeCAD.Console.PrintMessage("Upload complete\n") - - -def linenumber(): - global LINENR - if OUTPUT_LINE_NUMBERS is True: - LINENR += 10 - return "N" + str(LINENR) + " " - return "" - - -def parse(pathobj): - global SPINDLE_SPEED - - out = "" - lastcommand = None - precision_string = "." + str(PRECISION) + "f" - - # params = ['X','Y','Z','A','B','I','J','K','F','S'] #This list control - # the order of parameters - # linuxcnc doesn't want K properties on XY plane Arcs need work. - params = ["X", "Y", "Z", "A", "B", "I", "J", "F", "S", "T", "Q", "R", "L"] - - if hasattr(pathobj, "Group"): # We have a compound or project. - # if OUTPUT_COMMENTS: - # out += linenumber() + "(compound: " + pathobj.Label + ")\n" - for p in pathobj.Group: - out += parse(p) - return out - else: # parsing simple path - - # groups might contain non-path things like stock. - if not hasattr(pathobj, "Path"): - return out - - # if OUTPUT_COMMENTS: - # out += linenumber() + "(" + pathobj.Label + ")\n" - - for c in PathUtils.getPathWithPlacement(pathobj).Commands: - outstring = [] - command = c.Name - outstring.append(command) - # if modal: only print the command if it is not the same as the - # last one - if MODAL is True: - if command == lastcommand: - outstring.pop(0) - - # Now add the remaining parameters in order - for param in params: - if param in c.Parameters: - if param == "F": - if c.Name not in [ - "G0", - "G00", - ]: # linuxcnc doesn't use rapid speeds - speed = Units.Quantity(c.Parameters["F"], FreeCAD.Units.Velocity) - outstring.append( - param - + format( - float(speed.getValueAs(UNIT_SPEED_FORMAT)), - precision_string, - ) - ) - elif param == "T": - outstring.append(param + str(c.Parameters["T"])) - elif param == "S": - outstring.append(param + str(c.Parameters["S"])) - SPINDLE_SPEED = c.Parameters["S"] - else: - pos = Units.Quantity(c.Parameters[param], FreeCAD.Units.Length) - outstring.append( - param + format(float(pos.getValueAs(UNIT_FORMAT)), precision_string) - ) - if command in ["G1", "G01", "G2", "G02", "G3", "G03"]: - outstring.append("S" + str(SPINDLE_SPEED)) - - # store the latest command - lastcommand = command - - # Check for Tool Change: - if command == "M6": - # if OUTPUT_COMMENTS: - # out += linenumber() + "(begin toolchange)\n" - for line in TOOL_CHANGE.splitlines(True): - out += linenumber() + line - - if command == "message": - if OUTPUT_COMMENTS is False: - out = [] - else: - outstring.pop(0) # remove the command - - # prepend a line number and append a newline - if len(outstring) >= 1: - if OUTPUT_LINE_NUMBERS: - outstring.insert(0, (linenumber())) - - # append the line to the final output - for w in outstring: - out += w + COMMAND_SPACE - out = out.strip() + "\n" - - return out - - -# print(__name__ + " gcode postprocessor loaded.") + This postprocessor supports direct network upload to SmoothieBoard + via the --ip-addr argument. + """ + return tooltip diff --git a/src/Mod/CAM/Path/Post/scripts/refactored_test_post.py b/src/Mod/CAM/Path/Post/scripts/test_post.py similarity index 95% rename from src/Mod/CAM/Path/Post/scripts/refactored_test_post.py rename to src/Mod/CAM/Path/Post/scripts/test_post.py index 69ebe34d81..621c76a346 100644 --- a/src/Mod/CAM/Path/Post/scripts/refactored_test_post.py +++ b/src/Mod/CAM/Path/Post/scripts/test_post.py @@ -48,13 +48,13 @@ Values = Dict[str, Any] Visible = Dict[str, bool] -class Refactored_Test(PostProcessor): - """The Refactored Test post processor class.""" +class Test(PostProcessor): + """The Test post processor class.""" def __init__( self, job, - tooltip=translate("CAM", "Refactored Test post processor"), + tooltip=translate("CAM", "Test post processor"), tooltipargs=[""], units="Metric", ) -> None: @@ -64,7 +64,7 @@ class Refactored_Test(PostProcessor): tooltipargs=tooltipargs, units=units, ) - Path.Log.debug("Refactored Test post processor initialized") + Path.Log.debug("Test post processor initialized") def init_values(self, values: Values) -> None: """Initialize values that are used throughout the postprocessor.""" diff --git a/src/Mod/CAM/TestCAMApp.py b/src/Mod/CAM/TestCAMApp.py index 9fb88a13c7..80a0c6f744 100644 --- a/src/Mod/CAM/TestCAMApp.py +++ b/src/Mod/CAM/TestCAMApp.py @@ -97,17 +97,20 @@ from CAMTests.TestPathUtil import TestPathUtil from CAMTests.TestPathVcarve import TestPathVcarve from CAMTests.TestPathVoronoi import TestPathVoronoi -from CAMTests.TestCentroidPost import TestCentroidPost -from CAMTests.TestGrblPost import TestGrblPost +from CAMTests.TestGenericPost import TestGenericPost from CAMTests.TestLinuxCNCPost import TestLinuxCNCPost +from CAMTests.TestGrblPost import TestGrblPost +from CAMTests.TestMassoG3Post import TestMassoG3Post +from CAMTests.TestCentroidPost import TestCentroidPost from CAMTests.TestMach3Mach4Post import TestMach3Mach4Post -from CAMTests.TestRefactoredCentroidPost import TestRefactoredCentroidPost -from CAMTests.TestRefactoredGrblPost import TestRefactoredGrblPost -from CAMTests.TestRefactoredLinuxCNCPost import TestRefactoredLinuxCNCPost -from CAMTests.TestRefactoredMassoG3Post import TestRefactoredMassoG3Post -from CAMTests.TestRefactoredMach3Mach4Post import TestRefactoredMach3Mach4Post -from CAMTests.TestRefactoredTestDressupPost import TestRefactoredTestDressupPost -from CAMTests.TestRefactoredTestPost import TestRefactoredTestPost -from CAMTests.TestRefactoredTestPostGCodes import TestRefactoredTestPostGCodes -from CAMTests.TestRefactoredTestPostMCodes import TestRefactoredTestPostMCodes +from CAMTests.TestTestPost import TestTestPost +from CAMTests.TestPostGCodes import TestPostGCodes +from CAMTests.TestPostMCodes import TestPostMCodes +from CAMTests.TestDressupPost import TestDressupPost + +from CAMTests.TestLinuxCNCLegacyPost import TestLinuxCNCLegacyPost +from CAMTests.TestGrblLegacyPost import TestGrblLegacyPost +from CAMTests.TestCentroidLegacyPost import TestCentroidLegacyPost +from CAMTests.TestMach3Mach4LegacyPost import TestMach3Mach4LegacyPost + from CAMTests.TestSnapmakerPost import TestSnapmakerPost