From 1e87d8e6681b755b9757f94b1201e50eb84b28a2 Mon Sep 17 00:00:00 2001 From: jffmichi <> Date: Sun, 4 May 2025 21:22:20 +0200 Subject: [PATCH 1/9] CAM: fix handling of Active state and CoolantMode with nested dressups --- src/Mod/CAM/Path/Base/Util.py | 20 ++++++++++++++++--- src/Mod/CAM/Path/Post/UtilsExport.py | 16 +++------------ .../CAM/Path/Post/scripts/snapmaker_post.py | 7 +++---- 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/src/Mod/CAM/Path/Base/Util.py b/src/Mod/CAM/Path/Base/Util.py index 3de8266d5f..999da0a6c7 100644 --- a/src/Mod/CAM/Path/Base/Util.py +++ b/src/Mod/CAM/Path/Base/Util.py @@ -118,13 +118,13 @@ def isSolid(obj): return not shape.isNull() and shape.Volume and shape.isClosed() -def opProperty(op, prop): +def opProperty(op, prop, default=None): """opProperty(op, prop) ... return the value of property prop of the underlying operation (or None if prop does not exist)""" if hasattr(op, prop): return getattr(op, prop) if hasattr(op, "Base"): - return opProperty(op.Base, prop) - return None + return opProperty(op.Base, prop, default) + return default def toolControllerForOp(op): @@ -134,6 +134,20 @@ def toolControllerForOp(op): return opProperty(op, "ToolController") +def coolantModeForOp(op): + """coolantModeForOp(op) ... return the coolant mode used by the op. + If the op doesn't have its own coolant mode but has a Base object, return its coolant mode. + Otherwise return "None".""" + return opProperty(op, "CoolantMode", "None") + + +def activeForOp(op): + """activeForOp(op) ... return the active property used by the op. + If the op doesn't have its own active property but has a Base object, return its active property. + Otherwise return True.""" + return opProperty(op, "Active", True) + + def getPublicObject(obj): """getPublicObject(obj) ... returns the object which should be used to reference a feature of the given object.""" if hasattr(obj, "getParentGeoFeatureGroup"): diff --git a/src/Mod/CAM/Path/Post/UtilsExport.py b/src/Mod/CAM/Path/Post/UtilsExport.py index a9529c80b1..3baefca34b 100644 --- a/src/Mod/CAM/Path/Post/UtilsExport.py +++ b/src/Mod/CAM/Path/Post/UtilsExport.py @@ -32,6 +32,7 @@ import os from typing import Any, Dict, List import FreeCAD +import Path.Base.Util as PathUtil import Path.Post.Utils as PostUtils import Path.Post.UtilsParse as PostUtilsParse import Path.Tool.Controller as PathToolController @@ -50,15 +51,6 @@ def check_canned_cycles(values: Values) -> None: values["SUPPRESS_COMMANDS"] += ["G99", "G98", "G80"] -def determine_coolant_mode(obj) -> str: - """Determine the coolant mode.""" - if hasattr(obj, "CoolantMode") or hasattr(obj, "Base") and hasattr(obj.Base, "CoolantMode"): - if hasattr(obj, "CoolantMode"): - return obj.CoolantMode - return obj.Base.CoolantMode - return "None" - - def output_coolant_off(values: Values, gcode: Gcode, coolant_mode: str) -> None: """Output the commands to turn coolant off if necessary.""" comment: str @@ -314,11 +306,9 @@ def export_common(values: Values, objectslist, filename: str) -> str: for obj in objectslist: # Skip inactive operations - if hasattr(obj, "Active") and not obj.Active: + if not PathUtil.activeForOp(obj): continue - if hasattr(obj, "Base") and hasattr(obj.Base, "Active") and not obj.Base.Active: - continue - coolant_mode = determine_coolant_mode(obj) + coolant_mode = PathUtil.coolantModeForOp(obj) output_start_bcnc(values, gcode, obj) output_preop(values, gcode, obj) output_coolant_on(values, gcode, coolant_mode) diff --git a/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py b/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py index 925f406bba..c5bf8bf79f 100644 --- a/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py +++ b/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py @@ -31,6 +31,7 @@ from typing import Any, List, Tuple import FreeCAD import Path +import Path.Base.Util as PathUtil import Path.Post.Processor import Path.Post.UtilsArguments import Path.Post.UtilsExport @@ -773,11 +774,9 @@ class Snapmaker(Path.Post.Processor.PostProcessor): for obj in objects: # Skip inactive operations - if hasattr(obj, "Active") and not obj.Active: + if not PathUtil.activeForOp(obj): continue - if hasattr(obj, "Base") and hasattr(obj.Base, "Active") and not obj.Base.Active: - continue - coolant_mode = Path.Post.UtilsExport.determine_coolant_mode(obj) + coolant_mode = PathUtil.coolantModeForOp(obj) Path.Post.UtilsExport.output_start_bcnc(self.values, gcode, obj) Path.Post.UtilsExport.output_preop(self.values, gcode, obj) Path.Post.UtilsExport.output_coolant_on(self.values, gcode, coolant_mode) From f4d853e5dcc982d70d4e5c7828bf25e198403191 Mon Sep 17 00:00:00 2001 From: jffmichi <> Date: Wed, 7 May 2025 04:55:03 +0200 Subject: [PATCH 2/9] CAM: respect SHOW_OPERATION_LABELS in post-operation information --- .../CAMTests/TestRefactoredCentroidPost.py | 6 +-- .../CAM/CAMTests/TestRefactoredTestPost.py | 42 ++++++++-------- .../CAMTests/TestRefactoredTestPostGCodes.py | 48 +++++++++---------- src/Mod/CAM/Path/Post/UtilsArguments.py | 3 +- src/Mod/CAM/Path/Post/UtilsExport.py | 9 ++-- 5 files changed, 56 insertions(+), 52 deletions(-) diff --git a/src/Mod/CAM/CAMTests/TestRefactoredCentroidPost.py b/src/Mod/CAM/CAMTests/TestRefactoredCentroidPost.py index 0d172d24b3..a545fb2966 100644 --- a/src/Mod/CAM/CAMTests/TestRefactoredCentroidPost.py +++ b/src/Mod/CAM/CAMTests/TestRefactoredCentroidPost.py @@ -141,14 +141,14 @@ G53 G00 G17 G21 ;Begin operation G54 -;End operation: Fixture +;End operation ;Begin operation ;TC: Default Tool ;Begin toolchange M6 T1 -;End operation: TC: Default Tool +;End operation ;Begin operation -;End operation: Profile +;End operation ;Begin postamble M5 M25 diff --git a/src/Mod/CAM/CAMTests/TestRefactoredTestPost.py b/src/Mod/CAM/CAMTests/TestRefactoredTestPost.py index 58b05f44a6..d58ad8a0a9 100644 --- a/src/Mod/CAM/CAMTests/TestRefactoredTestPost.py +++ b/src/Mod/CAM/CAMTests/TestRefactoredTestPost.py @@ -306,15 +306,15 @@ G90 G21 (Begin operation) G54 -(Finish operation: Fixture) +(Finish operation) (Begin operation) (TC: Default Tool) (Begin toolchange) (M6 T1) -(Finish operation: TC: Default Tool) +(Finish operation) (Begin operation) (comment with spaces) -(Finish operation: Profile) +(Finish operation) (Begin postamble) """, "--command_space=' ' --comments", @@ -326,15 +326,15 @@ G90 G21 (Begin operation) G54 -(Finish operation: Fixture) +(Finish operation) (Begin operation) (TC: Default Tool) (Begin toolchange) (M6T1) -(Finish operation: TC: Default Tool) +(Finish operation) (Begin operation) (comment with spaces) -(Finish operation: Profile) +(Finish operation) (Begin postamble) """, "--command_space='' --comments", @@ -352,15 +352,15 @@ G90 G21 (Begin operation) G54 -(Finish operation: Fixture) +(Finish operation) (Begin operation) (TC: Default Tool) (Begin toolchange) (M6 T1) -(Finish operation: TC: Default Tool) +(Finish operation) (Begin operation) (comment with spaces) -(Finish operation: Profile) +(Finish operation) (Begin postamble) """, "--comments", @@ -372,15 +372,15 @@ G90 G21 ;Begin operation G54 -;Finish operation: Fixture +;Finish operation ;Begin operation ;TC: Default Tool ;Begin toolchange ;M6 T1 -;Finish operation: TC: Default Tool +;Finish operation ;Begin operation ;comment with spaces -;Finish operation: Profile +;Finish operation ;Begin postamble """, "--comment_symbol=';' --comments", @@ -392,15 +392,15 @@ G90 G21 !Begin operation G54 -!Finish operation: Fixture +!Finish operation !Begin operation !TC: Default Tool !Begin toolchange !M6 T1 -!Finish operation: TC: Default Tool +!Finish operation !Begin operation !comment with spaces -!Finish operation: Profile +!Finish operation !Begin postamble """, "--comment_symbol='!' --comments", @@ -471,14 +471,14 @@ G54 self.assertEqual(split_gcode[6], "G21") self.assertEqual(split_gcode[7], "(Begin operation)") self.assertEqual(split_gcode[8], "G54") - self.assertEqual(split_gcode[9], "(Finish operation: Fixture)") + self.assertEqual(split_gcode[9], "(Finish operation)") self.assertEqual(split_gcode[10], "(Begin operation)") self.assertEqual(split_gcode[11], "(TC: Default Tool)") self.assertEqual(split_gcode[12], "(Begin toolchange)") self.assertEqual(split_gcode[13], "(M6 T1)") - self.assertEqual(split_gcode[14], "(Finish operation: TC: Default Tool)") + self.assertEqual(split_gcode[14], "(Finish operation)") self.assertEqual(split_gcode[15], "(Begin operation)") - self.assertEqual(split_gcode[16], "(Finish operation: Profile)") + self.assertEqual(split_gcode[16], "(Finish operation)") self.assertEqual(split_gcode[17], "(Begin postamble)") # Test with comments without header. @@ -487,14 +487,14 @@ G90 G21 (Begin operation) G54 -(Finish operation: Fixture) +(Finish operation) (Begin operation) (TC: Default Tool) (Begin toolchange) (M6 T1) -(Finish operation: TC: Default Tool) +(Finish operation) (Begin operation) -(Finish operation: Profile) +(Finish operation) (Begin postamble) """ self.job.PostProcessorArgs = "--comments --no-header" diff --git a/src/Mod/CAM/CAMTests/TestRefactoredTestPostGCodes.py b/src/Mod/CAM/CAMTests/TestRefactoredTestPostGCodes.py index aff6c073de..93788b8737 100644 --- a/src/Mod/CAM/CAMTests/TestRefactoredTestPostGCodes.py +++ b/src/Mod/CAM/CAMTests/TestRefactoredTestPostGCodes.py @@ -768,12 +768,12 @@ G90 G21 (Begin operation) G54 -(Finish operation: Fixture) +(Finish operation) (Begin operation) (TC: Default Tool) (Begin toolchange) (M6 T1) -(Finish operation: TC: Default Tool) +(Finish operation) (Begin operation) G0 X1.000 Y2.000 G0 Z8.000 @@ -795,7 +795,7 @@ G1 Z0.000 F7380.000 G0 Z5.000 (G80) G90 -(Finish operation: Profile) +(Finish operation) (Begin postamble) """, "--comments --translate_drill", @@ -865,12 +865,12 @@ G90 G21 (Begin operation) G54 -(Finish operation: Fixture) +(Finish operation) (Begin operation) (TC: Default Tool) (Begin toolchange) (M6 T1) -(Finish operation: TC: Default Tool) +(Finish operation) (Begin operation) G0 X1.000 Y2.000 G0 Z8.000 @@ -894,7 +894,7 @@ G0 Z13.000 G91 (G80) G90 -(Finish operation: Profile) +(Finish operation) (Begin postamble) """, "--comments --translate_drill", @@ -951,12 +951,12 @@ G90 G21 (Begin operation) G54 -(Finish operation: Fixture) +(Finish operation) (Begin operation) (TC: Default Tool) (Begin toolchange) (M6 T1) -(Finish operation: TC: Default Tool) +(Finish operation) (Begin operation) G0 X1.000 Y2.000 G0 Z8.000 @@ -969,7 +969,7 @@ G1 Z0.000 F7380.000 G0 Z5.000 (G80) G90 -(Finish operation: Profile) +(Finish operation) (Begin postamble) """, "--comments --translate_drill", @@ -1030,12 +1030,12 @@ G90 G21 (Begin operation) G54 -(Finish operation: Fixture) +(Finish operation) (Begin operation) (TC: Default Tool) (Begin toolchange) (M6 T1) -(Finish operation: TC: Default Tool) +(Finish operation) (Begin operation) G0 X1.000 Y2.000 G0 Z8.000 @@ -1050,7 +1050,7 @@ G0 Z13.000 G91 (G80) G90 -(Finish operation: Profile) +(Finish operation) (Begin postamble) """, "--comments --translate_drill", @@ -1108,12 +1108,12 @@ G90 G21 (Begin operation) G54 -(Finish operation: Fixture) +(Finish operation) (Begin operation) (TC: Default Tool) (Begin toolchange) (M6 T1) -(Finish operation: TC: Default Tool) +(Finish operation) (Begin operation) G0 X1.000 Y2.000 G0 Z8.000 @@ -1127,7 +1127,7 @@ G4 P1.23456 G0 Z5.000 (G80) G90 -(Finish operation: Profile) +(Finish operation) (Begin postamble) """, "--comments --translate_drill", @@ -1189,12 +1189,12 @@ G90 G21 (Begin operation) G54 -(Finish operation: Fixture) +(Finish operation) (Begin operation) (TC: Default Tool) (Begin toolchange) (M6 T1) -(Finish operation: TC: Default Tool) +(Finish operation) (Begin operation) G0 X1.000 Y2.000 G0 Z8.000 @@ -1210,7 +1210,7 @@ G0 Z13.000 G91 (G80) G90 -(Finish operation: Profile) +(Finish operation) (Begin postamble) """, "--comments --translate_drill", @@ -1276,12 +1276,12 @@ G90 G21 (Begin operation) G54 -(Finish operation: Fixture) +(Finish operation) (Begin operation) (TC: Default Tool) (Begin toolchange) (M6 T1) -(Finish operation: TC: Default Tool) +(Finish operation) (Begin operation) G0 X1.000 Y2.000 G0 Z8.000 @@ -1303,7 +1303,7 @@ G1 Z0.000 F7380.000 G0 Z5.000 (G80) G90 -(Finish operation: Profile) +(Finish operation) (Begin postamble) """, "--comments --translate_drill", @@ -1373,12 +1373,12 @@ G90 G21 (Begin operation) G54 -(Finish operation: Fixture) +(Finish operation) (Begin operation) (TC: Default Tool) (Begin toolchange) (M6 T1) -(Finish operation: TC: Default Tool) +(Finish operation) (Begin operation) G0 X1.000 Y2.000 G0 Z8.000 @@ -1402,7 +1402,7 @@ G0 Z13.000 G91 (G80) G90 -(Finish operation: Profile) +(Finish operation) (Begin postamble) """, "--comments --translate_drill", diff --git a/src/Mod/CAM/Path/Post/UtilsArguments.py b/src/Mod/CAM/Path/Post/UtilsArguments.py index 7896d0a16d..94314bc38e 100644 --- a/src/Mod/CAM/Path/Post/UtilsArguments.py +++ b/src/Mod/CAM/Path/Post/UtilsArguments.py @@ -587,7 +587,8 @@ def init_shared_values(values: Values) -> None: # values["SHOW_MACHINE_UNITS"] = True # - # If True then the current operation label is output just before the PRE_OPERATION. + # If True then the current operation label is output just before the PRE_OPERATION + # and just before the POST_OPERATION. # values["SHOW_OPERATION_LABELS"] = True # diff --git a/src/Mod/CAM/Path/Post/UtilsExport.py b/src/Mod/CAM/Path/Post/UtilsExport.py index 3baefca34b..71e929a010 100644 --- a/src/Mod/CAM/Path/Post/UtilsExport.py +++ b/src/Mod/CAM/Path/Post/UtilsExport.py @@ -154,9 +154,12 @@ def output_postop(values: Values, gcode: Gcode, obj) -> None: nl: str = "\n" if values["OUTPUT_COMMENTS"]: - comment = PostUtilsParse.create_comment( - values, f'{values["FINISH_LABEL"]} operation: {obj.Label}' - ) + if values["SHOW_OPERATION_LABELS"]: + comment = PostUtilsParse.create_comment( + values, f'{values["FINISH_LABEL"]} operation: {obj.Label}' + ) + else: + comment = PostUtilsParse.create_comment(values, f'{values["FINISH_LABEL"]} operation') gcode.append(f"{PostUtilsParse.linenumber(values)}{comment}{nl}") for line in values["POST_OPERATION"].splitlines(False): gcode.append(f"{PostUtilsParse.linenumber(values)}{line}{nl}") From 3bcc702de955d2d7b6763f49cdc704b13b7f967d Mon Sep 17 00:00:00 2001 From: jffmichi <> Date: Wed, 7 May 2025 00:57:42 +0200 Subject: [PATCH 3/9] CAM: add test for refactored postprocessor using dressups --- .../CAMTests/TestRefactoredTestDressupPost.py | 155 ++++++++++++++++++ src/Mod/CAM/CAMTests/dressuptest.FCStd | Bin 0 -> 41788 bytes src/Mod/CAM/CMakeLists.txt | 2 + src/Mod/CAM/TestCAMApp.py | 2 + 4 files changed, 159 insertions(+) create mode 100644 src/Mod/CAM/CAMTests/TestRefactoredTestDressupPost.py create mode 100644 src/Mod/CAM/CAMTests/dressuptest.FCStd diff --git a/src/Mod/CAM/CAMTests/TestRefactoredTestDressupPost.py b/src/Mod/CAM/CAMTests/TestRefactoredTestDressupPost.py new file mode 100644 index 0000000000..6b57bbea1c --- /dev/null +++ b/src/Mod/CAM/CAMTests/TestRefactoredTestDressupPost.py @@ -0,0 +1,155 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * 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 +import Path.Dressup.Utils as PathDressup + + +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.""" + + @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/dressuptest.FCStd") + cls.job = cls.doc.getObject("Job") + cls.post = PostProcessorFactory.get_post_processor(cls.job, "refactored_test") + + # there are 4 operations in dressuptest.FCStd + # every operation uses a different ToolController + # each operation uses one more dressup to check nested dressups also work correctly + + cls.ops = cls.job.Operations.Group + + @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 test001(self): + # test handling of Active state in postprocessor and nested dressups + + self.post.values["OUTPUT_COMMENTS"] = True + self.post.values["SHOW_OPERATION_LABELS"] = True + + # set all operations to Active = False + + for op in self.ops: + PathDressup.baseOp(op).Active = False + + # check that none of the operations is in the output + + gcode = self.post.export()[0][1].splitlines() + + for op in self.ops: + expected = "(Begin operation: " + op.Label + ")" + self.assertNotIn(expected, gcode) + + # set all operations back to Active = True + + for op in self.ops: + PathDressup.baseOp(op).Active = True + + # check that all operations are in the output + + gcode = self.post.export()[0][1].splitlines() + + for op in self.ops: + expected = "(Begin operation: " + op.Label + ")" + self.assertIn(expected, gcode) + + def test002(self): + # test handling of ToolController in postprocessor and nested dressups + + self.post.values["OUTPUT_TOOL_CHANGE"] = True + + gcode = self.post.export()[0][1].splitlines() + + self.assertIn("M6 T1", gcode) + self.assertIn("M6 T2", gcode) + self.assertIn("M6 T3", gcode) + self.assertIn("M6 T4", gcode) + + def test003(self): + # test handling of CoolantMode in postprocessor and nested dressups + + self.post.values["ENABLE_COOLANT"] = True + + for op in self.ops: + base = PathDressup.baseOp(op) + base.CoolantMode = "Mist" + + gcode = self.post.export()[0][1].splitlines() + + self.assertIn("M7", gcode) + self.assertIn("M9", gcode) + + base.CoolantMode = "None" diff --git a/src/Mod/CAM/CAMTests/dressuptest.FCStd b/src/Mod/CAM/CAMTests/dressuptest.FCStd new file mode 100644 index 0000000000000000000000000000000000000000..f41fc81bc783970a6ebf24e7a06d00c49972ce1a GIT binary patch literal 41788 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 Date: Wed, 14 May 2025 05:10:05 +0200 Subject: [PATCH 4/9] CAM: replace opProperty with more specific activeForOp --- src/Mod/CAM/Path/Post/Processor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Mod/CAM/Path/Post/Processor.py b/src/Mod/CAM/Path/Post/Processor.py index 9dbecca733..1c153c309a 100644 --- a/src/Mod/CAM/Path/Post/Processor.py +++ b/src/Mod/CAM/Path/Post/Processor.py @@ -195,7 +195,7 @@ class PostProcessor: # Now generate the gcode for obj in self._job.Operations.Group: tc = PathUtil.toolControllerForOp(obj) - if tc is not None and PathUtil.opProperty(obj, "Active"): + if tc is not None and PathUtil.activeForOp(obj): if tc.ToolNumber != currTool: sublist.append(tc) Path.Log.debug(f"Appending TC: {tc.Name}") @@ -227,7 +227,7 @@ class PostProcessor: Path.Log.track(obj.Label) # check if the operation is active - if not getattr(obj, "Active", True): + if not PathUtil.activeForOp(obj): Path.Log.track() continue @@ -276,7 +276,7 @@ class PostProcessor: for obj in self._job.Operations.Group: # check if the operation is active - if not getattr(obj, "Active", True): + if not PathUtil.activeForOp(obj): continue sublist = [] From 747afab6e5ad03fc94df810d330ceca127cdeab3 Mon Sep 17 00:00:00 2001 From: jffmichi <> Date: Wed, 14 May 2025 21:43:31 +0200 Subject: [PATCH 5/9] CAM: fix handling of Active state and CoolantMode with nested dressups for some non-refactored postprocessors --- .../Path/Post/scripts/dynapath_4060_post.py | 17 +++------ src/Mod/CAM/Path/Post/scripts/estlcam_post.py | 9 +---- .../CAM/Path/Post/scripts/fangling_post.py | 9 ++--- src/Mod/CAM/Path/Post/scripts/fanuc_post.py | 16 ++------ src/Mod/CAM/Path/Post/scripts/grbl_post.py | 9 +---- .../CAM/Path/Post/scripts/linuxcnc_post.py | 16 ++------ .../CAM/Path/Post/scripts/mach3_mach4_post.py | 16 ++------ src/Mod/CAM/Path/Post/scripts/marlin_post.py | 9 +---- src/Mod/CAM/Path/Post/scripts/rrf_post.py | 9 +---- src/Mod/CAM/Path/Post/scripts/uccnc_post.py | 37 +++++++++---------- src/Mod/CAM/Path/Post/scripts/wedm_post.py | 16 ++------ 11 files changed, 50 insertions(+), 113 deletions(-) diff --git a/src/Mod/CAM/Path/Post/scripts/dynapath_4060_post.py b/src/Mod/CAM/Path/Post/scripts/dynapath_4060_post.py index f7861567f3..edf4951ddd 100644 --- a/src/Mod/CAM/Path/Post/scripts/dynapath_4060_post.py +++ b/src/Mod/CAM/Path/Post/scripts/dynapath_4060_post.py @@ -31,6 +31,7 @@ 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 @@ -232,12 +233,9 @@ def export(objectslist, filename, argstring): for obj in objectslist: # Skip inactive operations - if hasattr(obj, "Active"): - if not obj.Active: - continue - if hasattr(obj, "Base") and hasattr(obj.Base, "Active"): - if not obj.Base.Active: - continue + if not PathUtil.activeForOp(obj): + continue + if hasattr(obj, "ClearanceHeight"): clearanceHeight = obj.ClearanceHeight.Value @@ -257,12 +255,7 @@ def export(objectslist, filename, argstring): ) # get coolant mode - coolantMode = "None" - if hasattr(obj, "CoolantMode") or hasattr(obj, "Base") and hasattr(obj.Base, "CoolantMode"): - if hasattr(obj, "CoolantMode"): - coolantMode = obj.CoolantMode - else: - coolantMode = obj.Base.CoolantMode + coolantMode = PathUtil.coolantModeForOp(obj) # turn coolant on if required if OUTPUT_COMMENTS: diff --git a/src/Mod/CAM/Path/Post/scripts/estlcam_post.py b/src/Mod/CAM/Path/Post/scripts/estlcam_post.py index 2a571766a1..070bfa64fb 100644 --- a/src/Mod/CAM/Path/Post/scripts/estlcam_post.py +++ b/src/Mod/CAM/Path/Post/scripts/estlcam_post.py @@ -263,7 +263,7 @@ def export(objectslist, filename, argstring): continue # Skip inactive operations - if PathUtil.opProperty(obj, "Active") is False: + if not PathUtil.activeForOp(obj): continue # do the pre_op @@ -273,12 +273,7 @@ def export(objectslist, filename, argstring): gcode += linenumber() + line # get coolant mode - coolantMode = "None" - if hasattr(obj, "CoolantMode") or hasattr(obj, "Base") and hasattr(obj.Base, "CoolantMode"): - if hasattr(obj, "CoolantMode"): - coolantMode = obj.CoolantMode - else: - coolantMode = obj.Base.CoolantMode + coolantMode = PathUtil.coolantModeForOp(obj) # turn coolant on if required if OUTPUT_COMMENTS: diff --git a/src/Mod/CAM/Path/Post/scripts/fangling_post.py b/src/Mod/CAM/Path/Post/scripts/fangling_post.py index c6938c66b5..636bd2bc8b 100644 --- a/src/Mod/CAM/Path/Post/scripts/fangling_post.py +++ b/src/Mod/CAM/Path/Post/scripts/fangling_post.py @@ -38,6 +38,7 @@ import datetime import shlex # from PathScripts import PostUtils +import Path.Base.Util as PathUtil import Path.Post.Utils as PostUtils from PathScripts import PathUtils from builtins import open as pyopen @@ -216,12 +217,8 @@ def export(objectslist, filename, argstring): for obj in objectslist: # Skip inactive operations - if hasattr(obj, "Active"): - if not obj.Active: - continue - if hasattr(obj, "Base") and hasattr(obj.Base, "Active"): - if not obj.Base.Active: - continue + if not PathUtil.activeForOp(obj): + continue # fetch machine details job = PathUtils.findParentJob(obj) diff --git a/src/Mod/CAM/Path/Post/scripts/fanuc_post.py b/src/Mod/CAM/Path/Post/scripts/fanuc_post.py index 4487f70457..b708d51a9b 100644 --- a/src/Mod/CAM/Path/Post/scripts/fanuc_post.py +++ b/src/Mod/CAM/Path/Post/scripts/fanuc_post.py @@ -29,6 +29,7 @@ import argparse import datetime import shlex import os.path +import Path.Base.Util as PathUtil import Path.Post.Utils as PostUtils import PathScripts.PathUtils as PathUtils from builtins import open as pyopen @@ -227,12 +228,8 @@ def export(objectslist, filename, argstring): for obj in objectslist: # Skip inactive operations - if hasattr(obj, "Active"): - if not obj.Active: - continue - if hasattr(obj, "Base") and hasattr(obj.Base, "Active"): - if not obj.Base.Active: - continue + if not PathUtil.activeForOp(obj): + continue # do the pre_op if OUTPUT_COMMENTS: @@ -242,12 +239,7 @@ def export(objectslist, filename, argstring): gcode += linenumber() + line # get coolant mode - coolantMode = "None" - if hasattr(obj, "CoolantMode") or hasattr(obj, "Base") and hasattr(obj.Base, "CoolantMode"): - if hasattr(obj, "CoolantMode"): - coolantMode = obj.CoolantMode - else: - coolantMode = obj.Base.CoolantMode + coolantMode = PathUtil.coolantModeForOp(obj) # turn coolant on if required if OUTPUT_COMMENTS: diff --git a/src/Mod/CAM/Path/Post/scripts/grbl_post.py b/src/Mod/CAM/Path/Post/scripts/grbl_post.py index 3dd7f267af..497da87a57 100644 --- a/src/Mod/CAM/Path/Post/scripts/grbl_post.py +++ b/src/Mod/CAM/Path/Post/scripts/grbl_post.py @@ -315,7 +315,7 @@ def export(objectslist, filename, argstring): return # Skip inactive operations - if PathUtil.opProperty(obj, "Active") is False: + if not PathUtil.activeForOp(obj): continue # do the pre_op @@ -329,12 +329,7 @@ def export(objectslist, filename, argstring): gcode += linenumber() + line # get coolant mode - coolantMode = "None" - if hasattr(obj, "CoolantMode") or hasattr(obj, "Base") and hasattr(obj.Base, "CoolantMode"): - if hasattr(obj, "CoolantMode"): - coolantMode = obj.CoolantMode - else: - coolantMode = obj.Base.CoolantMode + coolantMode = PathUtil.coolantModeForOp(obj) # turn coolant on if required if OUTPUT_COMMENTS: diff --git a/src/Mod/CAM/Path/Post/scripts/linuxcnc_post.py b/src/Mod/CAM/Path/Post/scripts/linuxcnc_post.py index 5a0e676936..358eefe4bf 100644 --- a/src/Mod/CAM/Path/Post/scripts/linuxcnc_post.py +++ b/src/Mod/CAM/Path/Post/scripts/linuxcnc_post.py @@ -27,6 +27,7 @@ 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 @@ -203,12 +204,8 @@ def export(objectslist, filename, argstring): for obj in objectslist: # Skip inactive operations - if hasattr(obj, "Active"): - if not obj.Active: - continue - if hasattr(obj, "Base") and hasattr(obj.Base, "Active"): - if not obj.Base.Active: - continue + if not PathUtil.activeForOp(obj): + continue # do the pre_op if OUTPUT_COMMENTS: @@ -218,12 +215,7 @@ def export(objectslist, filename, argstring): gcode += linenumber() + line # get coolant mode - coolantMode = "None" - if hasattr(obj, "CoolantMode") or hasattr(obj, "Base") and hasattr(obj.Base, "CoolantMode"): - if hasattr(obj, "CoolantMode"): - coolantMode = obj.CoolantMode - else: - coolantMode = obj.Base.CoolantMode + coolantMode = PathUtil.coolantModeForOp(obj) # turn coolant on if required if OUTPUT_COMMENTS: 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 a450895333..c0eb947fd3 100644 --- a/src/Mod/CAM/Path/Post/scripts/mach3_mach4_post.py +++ b/src/Mod/CAM/Path/Post/scripts/mach3_mach4_post.py @@ -27,6 +27,7 @@ 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 @@ -203,12 +204,8 @@ def export(objectslist, filename, argstring): for obj in objectslist: # Skip inactive operations - if hasattr(obj, "Active"): - if not obj.Active: - continue - if hasattr(obj, "Base") and hasattr(obj.Base, "Active"): - if not obj.Base.Active: - continue + if not PathUtil.activeForOp(obj): + continue # do the pre_op if OUTPUT_COMMENTS: @@ -221,12 +218,7 @@ def export(objectslist, filename, argstring): gcode += linenumber() + line # get coolant mode - coolantMode = "None" - if hasattr(obj, "CoolantMode") or hasattr(obj, "Base") and hasattr(obj.Base, "CoolantMode"): - if hasattr(obj, "CoolantMode"): - coolantMode = obj.CoolantMode - else: - coolantMode = obj.Base.CoolantMode + coolantMode = PathUtil.coolantModeForOp(obj) # turn coolant on if required if OUTPUT_COMMENTS: diff --git a/src/Mod/CAM/Path/Post/scripts/marlin_post.py b/src/Mod/CAM/Path/Post/scripts/marlin_post.py index 6705a84797..23d8188389 100644 --- a/src/Mod/CAM/Path/Post/scripts/marlin_post.py +++ b/src/Mod/CAM/Path/Post/scripts/marlin_post.py @@ -357,7 +357,7 @@ def export(objectslist, filename, argstring): return # Skip inactive operations: - if PathUtil.opProperty(obj, "Active") is False: + if not PathUtil.activeForOp(obj): continue # Do the pre_op: @@ -371,12 +371,7 @@ def export(objectslist, filename, argstring): gcode += linenumber() + line # Get coolant mode: - coolantMode = "None" # None is the word returned from the operation - if hasattr(obj, "CoolantMode") or hasattr(obj, "Base") and hasattr(obj.Base, "CoolantMode"): - if hasattr(obj, "CoolantMode"): - coolantMode = obj.CoolantMode - else: - coolantMode = obj.Base.CoolantMode + coolantMode = PathUtil.coolantModeForOp(obj) # Turn coolant on if required: if OUTPUT_COMMENTS: diff --git a/src/Mod/CAM/Path/Post/scripts/rrf_post.py b/src/Mod/CAM/Path/Post/scripts/rrf_post.py index d578a9b17f..b72cec3c69 100644 --- a/src/Mod/CAM/Path/Post/scripts/rrf_post.py +++ b/src/Mod/CAM/Path/Post/scripts/rrf_post.py @@ -355,7 +355,7 @@ def export(objectslist, filename, argstring): return # Skip inactive operations: - if PathUtil.opProperty(obj, "Active") is False: + if not PathUtil.activeForOp(obj): continue # Do the pre_op: @@ -369,12 +369,7 @@ def export(objectslist, filename, argstring): gcode += linenumber() + line # Get coolant mode: - coolantMode = "None" # None is the word returned from the operation - if hasattr(obj, "CoolantMode") or hasattr(obj, "Base") and hasattr(obj.Base, "CoolantMode"): - if hasattr(obj, "CoolantMode"): - coolantMode = obj.CoolantMode - else: - coolantMode = obj.Base.CoolantMode + coolantMode = PathUtil.coolantModeForOp(obj) # Turn coolant on if required: if OUTPUT_COMMENTS: diff --git a/src/Mod/CAM/Path/Post/scripts/uccnc_post.py b/src/Mod/CAM/Path/Post/scripts/uccnc_post.py index b44a77c506..4634ff0390 100644 --- a/src/Mod/CAM/Path/Post/scripts/uccnc_post.py +++ b/src/Mod/CAM/Path/Post/scripts/uccnc_post.py @@ -33,6 +33,7 @@ import FreeCAD from FreeCAD import Units import Path +import Path.Base.Util as PathUtil import PathScripts.PathUtils as PathUtils import argparse import datetime @@ -458,18 +459,17 @@ def export(objectslist, filename, argstring): gcode += append(line) # turn coolant on if required - if hasattr(obj, "CoolantMode"): - coolantMode = obj.CoolantMode - if coolantMode == "Mist": - if OUTPUT_COMMENTS: - gcode += append("M7 (coolant: mist on)\n") - else: - gcode += append("M7\n") - if coolantMode == "Flood": - if OUTPUT_COMMENTS: - gcode += append("M8 (coolant: flood on)\n") - else: - gcode += append("M8\n") + coolantMode = PathUtil.coolantModeForOp(obj) + if coolantMode == "Mist": + if OUTPUT_COMMENTS: + gcode += append("M7 (coolant: mist on)\n") + else: + gcode += append("M7\n") + if coolantMode == "Flood": + if OUTPUT_COMMENTS: + gcode += append("M8 (coolant: flood on)\n") + else: + gcode += append("M8\n") # process the operation gcode if OUTPUT_COMMENTS: @@ -483,13 +483,12 @@ def export(objectslist, filename, argstring): gcode += append(line) # turn coolant off if required - if hasattr(obj, "CoolantMode"): - coolantMode = obj.CoolantMode - if not coolantMode == "None": - if OUTPUT_COMMENTS: - gcode += append("M9 (coolant: off)\n") - else: - gcode += append("M9\n") + if not coolantMode == "None": + if OUTPUT_COMMENTS: + gcode += append("M9 (coolant: off)\n") + else: + gcode += append("M9\n") + if OUTPUT_COMMENTS: gcode += append("(operation finalised: %s)\n" % obj.Label) diff --git a/src/Mod/CAM/Path/Post/scripts/wedm_post.py b/src/Mod/CAM/Path/Post/scripts/wedm_post.py index 1b9396f8b9..fc745ba8e1 100644 --- a/src/Mod/CAM/Path/Post/scripts/wedm_post.py +++ b/src/Mod/CAM/Path/Post/scripts/wedm_post.py @@ -30,6 +30,7 @@ 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 @@ -279,12 +280,8 @@ def export(objectslist, filename, argstring): for obj in objectslist: # Skip inactive operations - if hasattr(obj, "Active"): - if not obj.Active: - continue - if hasattr(obj, "Base") and hasattr(obj.Base, "Active"): - if not obj.Base.Active: - continue + if not PathUtil.activeForOp(obj): + continue # fetch machine details job = PathUtils.findParentJob(obj) @@ -319,12 +316,7 @@ def export(objectslist, filename, argstring): gcode += linenumber() + line # get coolant mode - coolantMode = "None" - if hasattr(obj, "CoolantMode") or hasattr(obj, "Base") and hasattr(obj.Base, "CoolantMode"): - if hasattr(obj, "CoolantMode"): - coolantMode = obj.CoolantMode - else: - coolantMode = obj.Base.CoolantMode + coolantMode = PathUtil.coolantModeForOp(obj) # turn coolant on if required if OUTPUT_COMMENTS: From 9d72b917b6ee5336333abf97b4dba7111a8e947a Mon Sep 17 00:00:00 2001 From: jffmichi <> Date: Wed, 14 May 2025 21:44:48 +0200 Subject: [PATCH 6/9] CAM: fix uccnc postprocessor not checking the Active state of operations --- src/Mod/CAM/Path/Post/scripts/uccnc_post.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Mod/CAM/Path/Post/scripts/uccnc_post.py b/src/Mod/CAM/Path/Post/scripts/uccnc_post.py index 4634ff0390..bad70ad181 100644 --- a/src/Mod/CAM/Path/Post/scripts/uccnc_post.py +++ b/src/Mod/CAM/Path/Post/scripts/uccnc_post.py @@ -452,6 +452,10 @@ def export(objectslist, filename, argstring): # write the code body for obj in objectslist: + # Skip inactive operations + if not PathUtil.activeForOp(obj): + continue + # pre_op if OUTPUT_COMMENTS: gcode += append("(operation initialise: %s)\n" % obj.Label) From 82a473ee599bbcfd0506c55fb641a2343c366e0e Mon Sep 17 00:00:00 2001 From: jffmichi <> Date: Wed, 14 May 2025 21:50:04 +0200 Subject: [PATCH 7/9] CAM: fix crash in dynapath_4060 postprocessor due to fmt function expecting precision as integer --- src/Mod/CAM/Path/Post/scripts/dynapath_4060_post.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Mod/CAM/Path/Post/scripts/dynapath_4060_post.py b/src/Mod/CAM/Path/Post/scripts/dynapath_4060_post.py index edf4951ddd..84704c7611 100644 --- a/src/Mod/CAM/Path/Post/scripts/dynapath_4060_post.py +++ b/src/Mod/CAM/Path/Post/scripts/dynapath_4060_post.py @@ -172,7 +172,7 @@ def processArguments(argstring): SHOW_EDITOR = False print("Show editor = %r" % (SHOW_EDITOR)) if args.precision is not None: - PRECISION = args.precision + PRECISION = int(args.precision) if args.preamble is not None: PREAMBLE = args.preamble.replace("\\n", "\n") if args.postamble is not None: From bb409986f74186a898a6a45fe1501e38b22e9e38 Mon Sep 17 00:00:00 2001 From: jffmichi <> Date: Wed, 14 May 2025 21:55:49 +0200 Subject: [PATCH 8/9] CAM: prevent marlin postprocessor from always writing to "-" --- src/Mod/CAM/Path/Post/scripts/marlin_post.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Mod/CAM/Path/Post/scripts/marlin_post.py b/src/Mod/CAM/Path/Post/scripts/marlin_post.py index 23d8188389..eee6f05b5c 100644 --- a/src/Mod/CAM/Path/Post/scripts/marlin_post.py +++ b/src/Mod/CAM/Path/Post/scripts/marlin_post.py @@ -36,6 +36,7 @@ import Path import Path.Base.Util as PathUtil import Path.Post.Utils as PostUtils import PathScripts.PathUtils as PathUtils +from builtins import open as pyopen Revised = "2020-11-03" # Revision date for this file. @@ -450,8 +451,10 @@ def export(objectslist, filename, argstring): print("Done postprocessing.") # Write the file: - with open(filename, "w") as fp: - fp.write(final) + if not filename == "-": + gfile = pyopen(filename, "w") + gfile.write(final) + gfile.close() return final From 1809c6cf01cfe28e4f2abe1c31c0bf6c19e1ef76 Mon Sep 17 00:00:00 2001 From: jffmichi <> Date: Wed, 14 May 2025 22:00:11 +0200 Subject: [PATCH 9/9] CAM: fix rrf postprocessor always writing to "-" instead of specified file --- src/Mod/CAM/Path/Post/scripts/rrf_post.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Mod/CAM/Path/Post/scripts/rrf_post.py b/src/Mod/CAM/Path/Post/scripts/rrf_post.py index b72cec3c69..9023aaafb4 100644 --- a/src/Mod/CAM/Path/Post/scripts/rrf_post.py +++ b/src/Mod/CAM/Path/Post/scripts/rrf_post.py @@ -35,6 +35,7 @@ import Path import Path.Base.Util as PathUtil import Path.Post.Utils as PostUtils import PathScripts.PathUtils as PathUtils +from builtins import open as pyopen Revised = "2021-10-21" # Revision date for this file. @@ -443,8 +444,12 @@ def export(objectslist, filename, argstring): print("Done postprocessing.") # Write the file: - with open(filename, "w") as fp: - fp.write(final) + if not filename == "-": + gfile = pyopen(filename, "w") + gfile.write(final) + gfile.close() + + return final def linenumber():