| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | from Path.Post.Command import DlgSelectPostProcessor |
| | from Path.Post.Processor import PostProcessor, PostProcessorFactory |
| | from unittest.mock import patch, MagicMock |
| | import FreeCAD |
| | import Path |
| | import Path.Post.Command as PathCommand |
| | import Path.Post.Processor as PathPost |
| | import Path.Post.Utils as PostUtils |
| | import Path.Post.UtilsExport as PostUtilsExport |
| | import Path.Main.Job as PathJob |
| | import Path.Tool.Controller as PathToolController |
| | import difflib |
| | import os |
| | import unittest |
| |
|
| | from .FilePathTestUtils import assertFilePathsEqual |
| |
|
| | PathCommand.LOG_MODULE = Path.Log.thisModule() |
| | Path.Log.setLevel(Path.Log.Level.INFO, PathCommand.LOG_MODULE) |
| |
|
| |
|
| | class TestFileNameGenerator(unittest.TestCase): |
| | r""" |
| | String substitution allows the following: |
| | %D ... directory of the active document |
| | %d ... name of the active document (with extension) |
| | %M ... user macro directory |
| | %j ... name of the active Job object |
| | |
| | |
| | The Following can be used if output is being split. If Output is not split |
| | these will be ignored. |
| | |
| | %S ... Sequence Number (default) |
| | |
| | Either: |
| | %T ... Tool Number |
| | %t ... Tool Controller label |
| | |
| | %W ... Work Coordinate System |
| | %O ... Operation Label |
| | |
| | |split on| use | Ignore | |
| | |-----------|-------|--------| |
| | |fixture | %W | %O %T %t | |
| | |Operation| %O | %T %t %W | |
| | |Tool| **Either %T or %t** | %O %W | |
| | |
| | The confusing bit is that for split on tool, it will use EITHER the tool number or the tool label. |
| | If you include both, the second one overrides the first. |
| | And for split on operation, where including the tool should be possible, it ignores it altogether. |
| | |
| | self.job.Fixtures = ["G54"] |
| | self.job.SplitOutput = False |
| | self.job.OrderOutputBy = "Fixture" |
| | |
| | Assume: |
| | active document: self.assertTrue(filename, f"{home}/testdoc.fcstd |
| | user macro: ~/.local/share/FreeCAD/Macro |
| | Job: MainJob |
| | Operations: |
| | OutsideProfile |
| | DrillAllHoles |
| | TC: 7/16" two flute (5) |
| | TC: Drill (2) |
| | Fixtures: (G54, G55) |
| | |
| | Strings should be sanitized like this to ensure valid filenames |
| | # import re |
| | # filename="TC: 7/16" two flute" |
| | # >>> re.sub(r"[^\w\d-]","_",filename) |
| | # "TC__7_16__two_flute" |
| | |
| | """ |
| |
|
| | @classmethod |
| | def setUpClass(cls): |
| | FreeCAD.ConfigSet("SuppressRecomputeRequiredDialog", "True") |
| |
|
| | |
| | cls.doc = FreeCAD.newDocument("TestFileNaming") |
| | cls.testfilename = cls.doc.Name |
| | cls.testfilepath = os.getcwd() |
| | cls.macro = FreeCAD.getUserMacroDir() |
| |
|
| | |
| | import Part |
| |
|
| | box = cls.doc.addObject("Part::Box", "TestBox") |
| | box.Length = 100 |
| | box.Width = 100 |
| | box.Height = 20 |
| |
|
| | |
| | cls.job = PathJob.Create("MainJob", [box], None) |
| | cls.job.PostProcessor = "linuxcnc" |
| | cls.job.PostProcessorOutputFile = "" |
| | cls.job.SplitOutput = False |
| | cls.job.OrderOutputBy = "Operation" |
| | cls.job.Fixtures = ["G54", "G55"] |
| |
|
| | |
| | from Path.Tool.toolbit import ToolBit |
| |
|
| | tool_attrs = { |
| | "name": "TestTool", |
| | "shape": "endmill.fcstd", |
| | "parameter": {"Diameter": 6.0}, |
| | "attribute": {}, |
| | } |
| | toolbit = ToolBit.from_dict(tool_attrs) |
| | tool = toolbit.attach_to_doc(doc=cls.doc) |
| | tool.Label = "6mm_Endmill" |
| |
|
| | tc = PathToolController.Create("TC_Test_Tool", tool, 5) |
| | tc.Label = "TC: 6mm Endmill" |
| | cls.job.addObject(tc) |
| |
|
| | |
| | profile_op = cls.doc.addObject("Path::FeaturePython", "TestProfile") |
| | profile_op.Label = "OutsideProfile" |
| | |
| | profile_op.Path = Path.Path() |
| | cls.job.Operations.addObject(profile_op) |
| |
|
| | cls.doc.recompute() |
| |
|
| | @classmethod |
| | def tearDownClass(cls): |
| | FreeCAD.closeDocument(cls.doc.Name) |
| | FreeCAD.ConfigSet("SuppressRecomputeRequiredDialog", "") |
| |
|
| | def test000(self): |
| | |
| | FreeCAD.setActiveDocument(self.doc.Label) |
| | teststring = "" |
| | self.job.PostProcessorOutputFile = teststring |
| | Path.Preferences.setOutputFileDefaults(teststring, "Append Unique ID on conflict") |
| |
|
| | generator = PostUtils.FilenameGenerator(job=self.job) |
| | filename_generator = generator.generate_filenames() |
| | filename = next(filename_generator) |
| | Path.Log.debug(filename) |
| | assertFilePathsEqual( |
| | self, filename, os.path.join(self.testfilepath, f"{self.testfilename}.nc") |
| | ) |
| |
|
| | def test010(self): |
| | |
| | teststring = "%D/testfile.nc" |
| | self.job.PostProcessorOutputFile = teststring |
| | Path.Preferences.setOutputFileDefaults(teststring, "Append Unique ID on conflict") |
| |
|
| | generator = PostUtils.FilenameGenerator(job=self.job) |
| | filename_generator = generator.generate_filenames() |
| | filename = next(filename_generator) |
| |
|
| | print(os.path.normpath(filename)) |
| | assertFilePathsEqual(self, filename, f"{self.testfilepath}/testfile.nc") |
| |
|
| | def test015(self): |
| | |
| | teststring = "~/Desktop/%j.nc" |
| | self.job.PostProcessorOutputFile = teststring |
| | Path.Preferences.setOutputFileDefaults(teststring, "Append Unique ID on conflict") |
| |
|
| | generator = PostUtils.FilenameGenerator(job=self.job) |
| | filename_generator = generator.generate_filenames() |
| | filename = next(filename_generator) |
| |
|
| | assertFilePathsEqual(self, filename, "~/Desktop/MainJob.nc") |
| |
|
| | def test020(self): |
| | teststring = "%d.nc" |
| | self.job.PostProcessorOutputFile = teststring |
| | Path.Preferences.setOutputFileDefaults(teststring, "Append Unique ID on conflict") |
| |
|
| | generator = PostUtils.FilenameGenerator(job=self.job) |
| | filename_generator = generator.generate_filenames() |
| | filename = next(filename_generator) |
| |
|
| | expected = os.path.join(self.testfilepath, f"{self.testfilename}.nc") |
| |
|
| | assertFilePathsEqual(self, filename, expected) |
| |
|
| | def test030(self): |
| | teststring = "%M/outfile.nc" |
| | self.job.PostProcessorOutputFile = teststring |
| | Path.Preferences.setOutputFileDefaults(teststring, "Append Unique ID on conflict") |
| |
|
| | generator = PostUtils.FilenameGenerator(job=self.job) |
| | filename_generator = generator.generate_filenames() |
| | filename = next(filename_generator) |
| |
|
| | assertFilePathsEqual(self, filename, f"{self.macro}outfile.nc") |
| |
|
| | def test040(self): |
| | |
| | teststring = "%d%T%t%W%O/testdoc.nc" |
| | self.job.PostProcessorOutputFile = teststring |
| | Path.Preferences.setOutputFileDefaults(teststring, "Append Unique ID on conflict") |
| |
|
| | generator = PostUtils.FilenameGenerator(job=self.job) |
| | filename_generator = generator.generate_filenames() |
| | filename = next(filename_generator) |
| |
|
| | assertFilePathsEqual(self, filename, f"{self.testfilename}/testdoc.nc") |
| |
|
| | def test045(self): |
| | """Testing the sequence number substitution""" |
| | generator = PostUtils.FilenameGenerator(job=self.job) |
| | filename_generator = generator.generate_filenames() |
| | expected_filenames = [f"TestFileNaming{os.sep}testdoc.nc"] + [ |
| | f"TestFileNaming{os.sep}testdoc-{i}.nc" for i in range(1, 5) |
| | ] |
| | for expected_filename in expected_filenames: |
| | filename = next(filename_generator) |
| | assertFilePathsEqual(self, filename, expected_filename) |
| |
|
| | def test046(self): |
| | """Testing the sequence number substitution""" |
| | teststring = "%S-%d.nc" |
| | self.job.PostProcessorOutputFile = teststring |
| | generator = PostUtils.FilenameGenerator(job=self.job) |
| | filename_generator = generator.generate_filenames() |
| | expected_filenames = [ |
| | os.path.join(self.testfilepath, f"{i}-TestFileNaming.nc") for i in range(5) |
| | ] |
| | for expected_filename in expected_filenames: |
| | filename = next(filename_generator) |
| | assertFilePathsEqual(self, filename, expected_filename) |
| |
|
| | def test050(self): |
| | |
| | teststring = "%S-%d.nc" |
| | self.job.PostProcessorOutputFile = teststring |
| | Path.Preferences.setOutputFileDefaults(teststring, "Append Unique ID on conflict") |
| |
|
| | generator = PostUtils.FilenameGenerator(job=self.job) |
| | filename_generator = generator.generate_filenames() |
| | filename = next(filename_generator) |
| |
|
| | assertFilePathsEqual(self, filename, os.path.join(self.testfilepath, "0-TestFileNaming.nc")) |
| |
|
| | def test060(self): |
| | """Test subpart naming""" |
| | teststring = "%M/outfile.nc" |
| | self.job.PostProcessorOutputFile = teststring |
| | Path.Preferences.setOutputFileDefaults(teststring, "Append Unique ID on conflict") |
| |
|
| | generator = PostUtils.FilenameGenerator(job=self.job) |
| | generator.set_subpartname("Tool") |
| | filename_generator = generator.generate_filenames() |
| | filename = next(filename_generator) |
| |
|
| | assertFilePathsEqual(self, filename, f"{self.macro}outfile-Tool.nc") |
| |
|
| | def test070(self): |
| | """Test %T substitution (tool number) with actual tool controller""" |
| | teststring = "%T.nc" |
| | self.job.PostProcessorOutputFile = teststring |
| |
|
| | generator = PostUtils.FilenameGenerator(job=self.job) |
| | generator.set_subpartname("5") |
| | filename_generator = generator.generate_filenames() |
| | filename = next(filename_generator) |
| |
|
| | assertFilePathsEqual(self, filename, os.path.join(self.testfilepath, "5.nc")) |
| |
|
| | def test071(self): |
| | """Test %t substitution (tool description) with actual tool controller""" |
| | teststring = "%t.nc" |
| | self.job.PostProcessorOutputFile = teststring |
| |
|
| | generator = PostUtils.FilenameGenerator(job=self.job) |
| | generator.set_subpartname("TC__6mm_Endmill") |
| | filename_generator = generator.generate_filenames() |
| | filename = next(filename_generator) |
| |
|
| | assertFilePathsEqual(self, filename, os.path.join(self.testfilepath, "TC__6mm_Endmill.nc")) |
| |
|
| | def test072(self): |
| | """Test %W substitution (work coordinate system/fixture)""" |
| | teststring = "%W.nc" |
| | self.job.PostProcessorOutputFile = teststring |
| |
|
| | generator = PostUtils.FilenameGenerator(job=self.job) |
| | generator.set_subpartname("G54") |
| | filename_generator = generator.generate_filenames() |
| | filename = next(filename_generator) |
| |
|
| | assertFilePathsEqual(self, filename, os.path.join(self.testfilepath, "G54.nc")) |
| |
|
| | def test073(self): |
| | """Test %O substitution (operation label)""" |
| | teststring = "%O.nc" |
| | self.job.PostProcessorOutputFile = teststring |
| |
|
| | generator = PostUtils.FilenameGenerator(job=self.job) |
| | generator.set_subpartname("OutsideProfile") |
| | filename_generator = generator.generate_filenames() |
| | filename = next(filename_generator) |
| |
|
| | assertFilePathsEqual(self, filename, os.path.join(self.testfilepath, "OutsideProfile.nc")) |
| |
|
| | def test075(self): |
| | """Test path and filename substitutions together""" |
| | teststring = "%D/%j_%S.nc" |
| | self.job.PostProcessorOutputFile = teststring |
| |
|
| | generator = PostUtils.FilenameGenerator(job=self.job) |
| | filename_generator = generator.generate_filenames() |
| | filename = next(filename_generator) |
| |
|
| | |
| | |
| | |
| | assertFilePathsEqual(self, filename, os.path.join(".", "MainJob_0.nc")) |
| |
|
| | def test076(self): |
| | """Test invalid substitution characters are ignored""" |
| | teststring = "%X%Y%Z/invalid_%Q.nc" |
| | self.job.PostProcessorOutputFile = teststring |
| |
|
| | generator = PostUtils.FilenameGenerator(job=self.job) |
| | filename_generator = generator.generate_filenames() |
| | filename = next(filename_generator) |
| |
|
| | |
| | assertFilePathsEqual(self, filename, os.path.join(self.testfilepath, "invalid_.nc")) |
| |
|
| |
|
| | class TestResolvingPostProcessorName(unittest.TestCase): |
| | @classmethod |
| | def setUpClass(cls): |
| | FreeCAD.ConfigSet("SuppressRecomputeRequiredDialog", "True") |
| | |
| | cls.doc = FreeCAD.newDocument("boxtest") |
| |
|
| | |
| | import Part |
| |
|
| | box = cls.doc.addObject("Part::Box", "TestBox") |
| | box.Length = 100 |
| | box.Width = 100 |
| | box.Height = 20 |
| |
|
| | |
| | cls.job = PathJob.Create("MainJob", [box], None) |
| | cls.job.PostProcessorOutputFile = "" |
| | cls.job.SplitOutput = False |
| | cls.job.OrderOutputBy = "Operation" |
| | cls.job.Fixtures = ["G54", "G55"] |
| |
|
| | @classmethod |
| | def tearDownClass(cls): |
| | FreeCAD.closeDocument(cls.doc.Name) |
| | FreeCAD.ConfigSet("SuppressRecomputeRequiredDialog", "") |
| |
|
| | def setUp(self): |
| | pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/CAM") |
| | pref.SetString("PostProcessorDefault", "") |
| |
|
| | def tearDown(self): |
| | pass |
| |
|
| | def test010(self): |
| | |
| | self.job.PostProcessor = "linuxcnc" |
| | with patch("Path.Post.Processor.PostProcessor.exists", return_value=True): |
| | postname = PathCommand._resolve_post_processor_name(self.job) |
| | self.assertEqual(postname, "linuxcnc") |
| |
|
| | def test020(self): |
| | |
| | with patch("Path.Post.Processor.PostProcessor.exists", return_value=False): |
| | with self.assertRaises(ValueError): |
| | PathCommand._resolve_post_processor_name(self.job) |
| |
|
| | def test030(self): |
| | |
| | self.job.PostProcessor = "" |
| | pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/CAM") |
| | pref.SetString("PostProcessorDefault", "grbl") |
| |
|
| | with patch("Path.Post.Processor.PostProcessor.exists", return_value=True): |
| | postname = PathCommand._resolve_post_processor_name(self.job) |
| | self.assertEqual(postname, "grbl") |
| |
|
| | def test040(self): |
| | |
| | if FreeCAD.GuiUp: |
| | with patch("Path.Post.Command.DlgSelectPostProcessor") as mock_dlg, patch( |
| | "Path.Post.Processor.PostProcessor.exists", return_value=True |
| | ): |
| | mock_dlg.return_value.exec_.return_value = "generic" |
| | postname = PathCommand._resolve_post_processor_name(self.job) |
| | self.assertEqual(postname, "generic") |
| | else: |
| | with patch.object(self.job, "PostProcessor", ""): |
| | with self.assertRaises(ValueError): |
| | PathCommand._resolve_post_processor_name(self.job) |
| |
|
| |
|
| | class TestPostProcessorFactory(unittest.TestCase): |
| | """Test creation of postprocessor objects.""" |
| |
|
| | @classmethod |
| | def setUpClass(cls): |
| | FreeCAD.ConfigSet("SuppressRecomputeRequiredDialog", "True") |
| | |
| | cls.doc = FreeCAD.newDocument("boxtest") |
| |
|
| | |
| | import Part |
| |
|
| | box = cls.doc.addObject("Part::Box", "TestBox") |
| | box.Length = 100 |
| | box.Width = 100 |
| | box.Height = 20 |
| |
|
| | |
| | cls.job = PathJob.Create("MainJob", [box], None) |
| | cls.job.PostProcessor = "linuxcnc" |
| | cls.job.PostProcessorOutputFile = "" |
| | cls.job.SplitOutput = False |
| | cls.job.OrderOutputBy = "Operation" |
| | cls.job.Fixtures = ["G54", "G55"] |
| |
|
| | @classmethod |
| | def tearDownClass(cls): |
| | FreeCAD.closeDocument(cls.doc.Name) |
| | FreeCAD.ConfigSet("SuppressRecomputeRequiredDialog", "") |
| |
|
| | def setUp(self): |
| | pass |
| |
|
| | def tearDown(self): |
| | pass |
| |
|
| | def test020(self): |
| | |
| | post = PostProcessorFactory.get_post_processor(self.job, "generic") |
| | self.assertTrue(post is not None) |
| | self.assertTrue(hasattr(post, "export")) |
| | self.assertTrue(hasattr(post, "_buildPostList")) |
| |
|
| | def test030(self): |
| | |
| | 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_legacy") |
| | self.assertEqual(post.script_module.__name__, "linuxcnc_legacy_post") |
| |
|
| |
|
| | class TestPathPostUtils(unittest.TestCase): |
| | def test010(self): |
| | """Test the utility functions in the PostUtils.py file.""" |
| | commands = [ |
| | Path.Command("G1 X-7.5 Y5.0 Z0.0"), |
| | Path.Command("G2 I2.5 J0.0 K0.0 X-5.0 Y7.5 Z0.0"), |
| | Path.Command("G1 X5.0 Y7.5 Z0.0"), |
| | Path.Command("G2 I0.0 J-2.5 K0.0 X7.5 Y5.0 Z0.0"), |
| | Path.Command("G1 X7.5 Y-5.0 Z0.0"), |
| | Path.Command("G2 I-2.5 J0.0 K0.0 X5.0 Y-7.5 Z0.0"), |
| | Path.Command("G1 X-5.0 Y-7.5 Z0.0"), |
| | Path.Command("G2 I0.0 J2.5 K0.0 X-7.5 Y-5.0 Z0.0"), |
| | Path.Command("G1 X-7.5 Y0.0 Z0.0"), |
| | ] |
| |
|
| | testpath = Path.Path(commands) |
| | self.assertTrue(len(testpath.Commands) == 9) |
| | self.assertTrue(len([c for c in testpath.Commands if c.Name in ["G2", "G3"]]) == 4) |
| |
|
| | results = PostUtils.splitArcs(testpath) |
| | |
| | self.assertTrue(len([c for c in results.Commands if c.Name in ["G2", "G3"]]) == 0) |
| |
|
| | def test020(self): |
| | """Test Termination of Canned Cycles""" |
| | |
| | cmd1 = Path.Command("G81", {"X": 1.0, "Y": 1.0, "Z": -0.5, "R": 0.1, "F": 10.0}) |
| | cmd1.Annotations = {"RetractMode": "G98"} |
| | cmd2 = Path.Command("G81", {"X": 2.0, "Y": 2.0, "Z": -1.0, "R": 0.2, "F": 10.0}) |
| | cmd2.Annotations = {"RetractMode": "G98"} |
| |
|
| | test_path = Path.Path( |
| | [ |
| | Path.Command("G0", {"Z": 1.0}), |
| | cmd1, |
| | cmd2, |
| | Path.Command("G1", {"X": 3.0, "Y": 3.0}), |
| | ] |
| | ) |
| |
|
| | expected_path = Path.Path( |
| | [ |
| | Path.Command("G0", {"Z": 1.0}), |
| | Path.Command("G98"), |
| | Path.Command("G81", {"X": 1.0, "Y": 1.0, "Z": -0.5, "R": 0.1, "F": 10.0}), |
| | Path.Command("G80"), |
| | Path.Command("G98"), |
| | Path.Command("G81", {"X": 2.0, "Y": 2.0, "Z": -1.0, "R": 0.2, "F": 10.0}), |
| | Path.Command("G80"), |
| | Path.Command("G1", {"X": 3.0, "Y": 3.0}), |
| | ] |
| | ) |
| |
|
| | result = PostUtils.cannedCycleTerminator(test_path) |
| |
|
| | self.assertEqual(len(result.Commands), len(expected_path.Commands)) |
| | for i, (res, exp) in enumerate(zip(result.Commands, expected_path.Commands)): |
| | self.assertEqual(res.Name, exp.Name, f"Command {i}: name mismatch") |
| | self.assertEqual(res.Parameters, exp.Parameters, f"Command {i}: parameters mismatch") |
| |
|
| | def test030_canned_cycle_termination_with_non_cycle_commands(self): |
| | """Test cycle termination when non-cycle commands are encountered""" |
| | cmd1 = Path.Command("G81", {"X": 1.0, "Y": 1.0, "Z": -0.5, "R": 0.1, "F": 10.0}) |
| | cmd1.Annotations = {"RetractMode": "G98"} |
| | cmd2 = Path.Command("G82", {"X": 3.0, "Y": 3.0, "Z": -1.0, "R": 0.2, "P": 1.0, "F": 10.0}) |
| | cmd2.Annotations = {"RetractMode": "G98"} |
| |
|
| | test_path = Path.Path( |
| | [ |
| | cmd1, |
| | Path.Command("G0", {"X": 2.0, "Y": 2.0}), |
| | cmd2, |
| | ] |
| | ) |
| |
|
| | expected_path = Path.Path( |
| | [ |
| | Path.Command("G98"), |
| | Path.Command("G81", {"X": 1.0, "Y": 1.0, "Z": -0.5, "R": 0.1, "F": 10.0}), |
| | Path.Command("G80"), |
| | Path.Command("G0", {"X": 2.0, "Y": 2.0}), |
| | Path.Command("G98"), |
| | Path.Command("G82", {"X": 3.0, "Y": 3.0, "Z": -1.0, "R": 0.2, "P": 1.0, "F": 10.0}), |
| | Path.Command("G80"), |
| | ] |
| | ) |
| |
|
| | result = PostUtils.cannedCycleTerminator(test_path) |
| | self.assertEqual(len(result.Commands), len(expected_path.Commands)) |
| | for i, (res, exp) in enumerate(zip(result.Commands, expected_path.Commands)): |
| | self.assertEqual(res.Name, exp.Name, f"Command {i}: name mismatch") |
| | self.assertEqual(res.Parameters, exp.Parameters, f"Command {i}: parameters mismatch") |
| |
|
| | def test040_canned_cycle_modal_same_parameters(self): |
| | """Test modal cycles with same parameters don't get terminated""" |
| | cmd1 = Path.Command("G81", {"X": 1.0, "Y": 1.0, "Z": -0.5, "R": 0.1, "F": 10.0}) |
| | cmd1.Annotations = {"RetractMode": "G98"} |
| | cmd2 = Path.Command("G81", {"X": 2.0, "Y": 2.0, "Z": -0.5, "R": 0.1, "F": 10.0}) |
| | cmd2.Annotations = {"RetractMode": "G98"} |
| | cmd3 = Path.Command("G81", {"X": 3.0, "Y": 3.0, "Z": -0.5, "R": 0.1, "F": 10.0}) |
| | cmd3.Annotations = {"RetractMode": "G98"} |
| |
|
| | test_path = Path.Path( |
| | [ |
| | cmd1, |
| | cmd2, |
| | cmd3, |
| | ] |
| | ) |
| |
|
| | expected_path = Path.Path( |
| | [ |
| | Path.Command("G98"), |
| | Path.Command("G81", {"X": 1.0, "Y": 1.0, "Z": -0.5, "R": 0.1, "F": 10.0}), |
| | Path.Command( |
| | "G81", {"X": 2.0, "Y": 2.0, "Z": -0.5, "R": 0.1, "F": 10.0} |
| | ), |
| | Path.Command( |
| | "G81", {"X": 3.0, "Y": 3.0, "Z": -0.5, "R": 0.1, "F": 10.0} |
| | ), |
| | Path.Command("G80"), |
| | ] |
| | ) |
| |
|
| | result = PostUtils.cannedCycleTerminator(test_path) |
| | self.assertEqual(len(result.Commands), len(expected_path.Commands)) |
| | for i, (res, exp) in enumerate(zip(result.Commands, expected_path.Commands)): |
| | self.assertEqual(res.Name, exp.Name, f"Command {i}: name mismatch") |
| | self.assertEqual(res.Parameters, exp.Parameters, f"Command {i}: parameters mismatch") |
| |
|
| | def test050_canned_cycle_feed_rate_change(self): |
| | """Test cycle termination when feed rate changes""" |
| | cmd1 = Path.Command("G81", {"X": 1.0, "Y": 1.0, "Z": -0.5, "R": 0.1, "F": 10.0}) |
| | cmd1.Annotations = {"RetractMode": "G98"} |
| | cmd2 = Path.Command("G81", {"X": 2.0, "Y": 2.0, "Z": -0.5, "R": 0.1, "F": 20.0}) |
| | cmd2.Annotations = {"RetractMode": "G98"} |
| |
|
| | test_path = Path.Path( |
| | [ |
| | cmd1, |
| | cmd2, |
| | ] |
| | ) |
| |
|
| | expected_path = Path.Path( |
| | [ |
| | Path.Command("G98"), |
| | Path.Command("G81", {"X": 1.0, "Y": 1.0, "Z": -0.5, "R": 0.1, "F": 10.0}), |
| | Path.Command("G80"), |
| | Path.Command("G98"), |
| | Path.Command("G81", {"X": 2.0, "Y": 2.0, "Z": -0.5, "R": 0.1, "F": 20.0}), |
| | Path.Command("G80"), |
| | ] |
| | ) |
| |
|
| | result = PostUtils.cannedCycleTerminator(test_path) |
| | self.assertEqual(len(result.Commands), len(expected_path.Commands)) |
| | for i, (res, exp) in enumerate(zip(result.Commands, expected_path.Commands)): |
| | self.assertEqual(res.Name, exp.Name, f"Command {i}: name mismatch") |
| | self.assertEqual(res.Parameters, exp.Parameters, f"Command {i}: parameters mismatch") |
| |
|
| | def test060_canned_cycle_retract_plane_change(self): |
| | """Test cycle termination when retract plane changes""" |
| | cmd1 = Path.Command("G81", {"X": 1.0, "Y": 1.0, "Z": -0.5, "R": 0.1, "F": 10.0}) |
| | cmd1.Annotations = {"RetractMode": "G98"} |
| | cmd2 = Path.Command("G81", {"X": 2.0, "Y": 2.0, "Z": -0.5, "R": 0.2, "F": 10.0}) |
| | cmd2.Annotations = {"RetractMode": "G98"} |
| |
|
| | test_path = Path.Path( |
| | [ |
| | cmd1, |
| | cmd2, |
| | ] |
| | ) |
| |
|
| | expected_path = Path.Path( |
| | [ |
| | Path.Command("G98"), |
| | Path.Command("G81", {"X": 1.0, "Y": 1.0, "Z": -0.5, "R": 0.1, "F": 10.0}), |
| | Path.Command("G80"), |
| | Path.Command("G98"), |
| | Path.Command("G81", {"X": 2.0, "Y": 2.0, "Z": -0.5, "R": 0.2, "F": 10.0}), |
| | Path.Command("G80"), |
| | ] |
| | ) |
| |
|
| | result = PostUtils.cannedCycleTerminator(test_path) |
| | self.assertEqual(len(result.Commands), len(expected_path.Commands)) |
| | for i, (res, exp) in enumerate(zip(result.Commands, expected_path.Commands)): |
| | self.assertEqual(res.Name, exp.Name, f"Command {i}: name mismatch") |
| | self.assertEqual(res.Parameters, exp.Parameters, f"Command {i}: parameters mismatch") |
| |
|
| | def test070_canned_cycle_mixed_cycle_types(self): |
| | """Test termination between different cycle types""" |
| | cmd1 = Path.Command("G81", {"X": 1.0, "Y": 1.0, "Z": -0.5, "R": 0.1, "F": 10.0}) |
| | cmd1.Annotations = {"RetractMode": "G98"} |
| | cmd2 = Path.Command("G82", {"X": 2.0, "Y": 2.0, "Z": -0.5, "R": 0.1, "P": 1.0, "F": 10.0}) |
| | cmd2.Annotations = {"RetractMode": "G98"} |
| |
|
| | test_path = Path.Path( |
| | [ |
| | cmd1, |
| | cmd2, |
| | ] |
| | ) |
| |
|
| | expected_path = Path.Path( |
| | [ |
| | Path.Command("G98"), |
| | Path.Command("G81", {"X": 1.0, "Y": 1.0, "Z": -0.5, "R": 0.1, "F": 10.0}), |
| | Path.Command("G80"), |
| | Path.Command("G98"), |
| | Path.Command("G82", {"X": 2.0, "Y": 2.0, "Z": -0.5, "R": 0.1, "P": 1.0, "F": 10.0}), |
| | Path.Command("G80"), |
| | ] |
| | ) |
| |
|
| | result = PostUtils.cannedCycleTerminator(test_path) |
| | self.assertEqual(len(result.Commands), len(expected_path.Commands)) |
| | for i, (res, exp) in enumerate(zip(result.Commands, expected_path.Commands)): |
| | self.assertEqual(res.Name, exp.Name, f"Command {i}: name mismatch") |
| | self.assertEqual(res.Parameters, exp.Parameters, f"Command {i}: parameters mismatch") |
| |
|
| | def test080_canned_cycle_retract_mode_change(self): |
| | """Test cycle termination and retract mode insertion when RetractMode annotation changes""" |
| | |
| | cmd1 = Path.Command("G81", {"X": 1.0, "Y": 1.0, "Z": -0.5, "R": 0.1, "F": 10.0}) |
| | cmd1.Annotations = {"RetractMode": "G98"} |
| |
|
| | cmd2 = Path.Command("G81", {"X": 2.0, "Y": 2.0, "Z": -0.5, "R": 0.1, "F": 10.0}) |
| | cmd2.Annotations = {"RetractMode": "G98"} |
| |
|
| | cmd3 = Path.Command("G81", {"X": 3.0, "Y": 3.0, "Z": -0.5, "R": 0.1, "F": 10.0}) |
| | cmd3.Annotations = {"RetractMode": "G99"} |
| |
|
| | test_path = Path.Path([cmd1, cmd2, cmd3]) |
| |
|
| | result = PostUtils.cannedCycleTerminator(test_path) |
| |
|
| | |
| | self.assertEqual(result.Commands[0].Name, "G98") |
| | self.assertEqual(result.Commands[1].Name, "G81") |
| | self.assertEqual(result.Commands[2].Name, "G81") |
| | self.assertEqual(result.Commands[3].Name, "G80") |
| | self.assertEqual(result.Commands[4].Name, "G99") |
| | self.assertEqual(result.Commands[5].Name, "G81") |
| | self.assertEqual(result.Commands[6].Name, "G80") |
| | self.assertEqual(len(result.Commands), 7) |
| |
|
| |
|
| | class TestBuildPostList(unittest.TestCase): |
| | """ |
| | The postlist is the list of postprocessable elements from the job. |
| | The list varies depending on |
| | -The operations |
| | -The tool controllers |
| | -The work coordinate systems (WCS) or 'fixtures' |
| | -How the job is ordering the output (WCS, tool, operation) |
| | -Whether or not the output is being split to multiple files |
| | This test case ensures that the correct sequence of postable objects is |
| | created. |
| | |
| | The list will be comprised of a list of tuples. Each tuple consists of |
| | (subobject string, [list of objects]) |
| | The subobject string can be used in output name generation if splitting output |
| | the list of objects is all postable elements to be written to that file |
| | |
| | """ |
| |
|
| | |
| | debug = False |
| |
|
| | @classmethod |
| | def _format_postables(cls, postables, title="Postables"): |
| | """Format postables for readable debug output, following dumper_post.py pattern.""" |
| | output = [] |
| | output.append("=" * 80) |
| | output.append(title) |
| | output.append("=" * 80) |
| | output.append("") |
| |
|
| | for idx, postable in enumerate(postables, 1): |
| | group_key = postable[0] |
| | objects = postable[1] |
| |
|
| | |
| | if group_key == "": |
| | display_key = "(empty string)" |
| | elif group_key == "allitems": |
| | display_key = '"allitems" (combined output)' |
| | else: |
| | display_key = f'"{group_key}"' |
| |
|
| | output.append(f"[{idx}] Group: {display_key}") |
| | output.append(f" Objects: {len(objects)}") |
| | output.append("") |
| |
|
| | for obj_idx, obj in enumerate(objects, 1): |
| | obj_label = getattr(obj, "Label", str(type(obj).__name__)) |
| | output.append(f" [{obj_idx}] {obj_label}") |
| |
|
| | |
| | obj_type = type(obj).__name__ |
| | if obj_type == "_FixtureSetupObject": |
| | output.append(f" Type: Fixture Setup") |
| | if hasattr(obj, "Path") and obj.Path and len(obj.Path.Commands) > 0: |
| | fixture_cmd = obj.Path.Commands[0] |
| | output.append(f" Fixture: {fixture_cmd.Name}") |
| | elif obj_type == "_CommandObject": |
| | output.append(f" Type: Command Object") |
| | if hasattr(obj, "Path") and obj.Path and len(obj.Path.Commands) > 0: |
| | cmd = obj.Path.Commands[0] |
| | params = " ".join( |
| | f"{k}:{v}" |
| | for k, v in zip( |
| | cmd.Parameters.keys() if hasattr(cmd.Parameters, "keys") else [], |
| | ( |
| | cmd.Parameters.values() |
| | if hasattr(cmd.Parameters, "values") |
| | else cmd.Parameters |
| | ), |
| | ) |
| | ) |
| | output.append(f" Command: {cmd.Name} {params}") |
| | elif hasattr(obj, "TypeId"): |
| | |
| | if hasattr(obj, "Proxy") and hasattr(obj.Proxy, "__class__"): |
| | proxy_name = obj.Proxy.__class__.__name__ |
| | if "ToolController" in proxy_name: |
| | output.append(f" Type: Tool Controller") |
| | if hasattr(obj, "ToolNumber"): |
| | output.append(f" Tool Number: {obj.ToolNumber}") |
| | if hasattr(obj, "Path") and obj.Path and obj.Path.Commands: |
| | for cmd in obj.Path.Commands: |
| | if cmd.Name == "M6": |
| | params = " ".join( |
| | f"{k}:{v}" |
| | for k, v in zip( |
| | ( |
| | cmd.Parameters.keys() |
| | if hasattr(cmd.Parameters, "keys") |
| | else [] |
| | ), |
| | ( |
| | cmd.Parameters.values() |
| | if hasattr(cmd.Parameters, "values") |
| | else cmd.Parameters |
| | ), |
| | ) |
| | ) |
| | output.append(f" M6 Command: {cmd.Name} {params}") |
| | else: |
| | output.append(f" Type: Operation") |
| | if hasattr(obj, "ToolController") and obj.ToolController: |
| | tc = obj.ToolController |
| | output.append( |
| | f" ToolController: {tc.Label} (T{tc.ToolNumber})" |
| | ) |
| | else: |
| | output.append(f" Type: {obj.TypeId}") |
| | else: |
| | output.append(f" Type: {obj_type}") |
| |
|
| | output.append("") |
| |
|
| | output.append("=" * 80) |
| | output.append(f"Total Groups: {len(postables)}") |
| | total_objects = sum(len(p[1]) for p in postables) |
| | output.append(f"Total Objects: {total_objects}") |
| | output.append("=" * 80) |
| |
|
| | return "\n".join(output) |
| |
|
| | @classmethod |
| | def setUpClass(cls): |
| | FreeCAD.ConfigSet("SuppressRecomputeRequiredDialog", "True") |
| | |
| | cls.doc = FreeCAD.newDocument("test_filenaming") |
| |
|
| | |
| | import Part |
| |
|
| | box = cls.doc.addObject("Part::Box", "TestBox") |
| | box.Length = 100 |
| | box.Width = 100 |
| | box.Height = 20 |
| |
|
| | |
| | cls.job = PathJob.Create("MainJob", [box], None) |
| | cls.job.PostProcessor = "generic" |
| | cls.job.PostProcessorOutputFile = "" |
| | cls.job.SplitOutput = False |
| | cls.job.OrderOutputBy = "Operation" |
| | cls.job.Fixtures = ["G54", "G55"] |
| |
|
| | |
| | |
| |
|
| | |
| | cls.job.Tools.Group[0].ToolNumber = 5 |
| | cls.job.Tools.Group[0].Label = ( |
| | 'TC: 7/16" two flute' |
| | ) |
| |
|
| | |
| | tc2 = PathToolController.Create() |
| | tc2.ToolNumber = 2 |
| | tc2.Label = 'TC: 7/16" two flute' |
| | cls.job.Proxy.addToolController(tc2) |
| |
|
| | |
| | cls.job.Tools.Group[0].recompute() |
| | cls.job.Tools.Group[1].recompute() |
| |
|
| | |
| | |
| | |
| | operation_names = ["outsideprofile", "DrillAllHoles", "Comment"] |
| |
|
| | for i, name in enumerate(operation_names): |
| | |
| | op = cls.doc.addObject("Path::FeaturePython", name) |
| | op.Label = name |
| | |
| | op.Path = Path.Path() |
| |
|
| | |
| | if name != "Comment": |
| | |
| | op.addProperty( |
| | "App::PropertyLink", |
| | "ToolController", |
| | "Base", |
| | "Tool controller for this operation", |
| | ) |
| | |
| | if i == 0: |
| | op.ToolController = cls.job.Tools.Group[0] |
| | elif i == 1: |
| | op.ToolController = cls.job.Tools.Group[1] |
| | |
| |
|
| | |
| | cls.job.Operations.addObject(op) |
| |
|
| | @classmethod |
| | def tearDownClass(cls): |
| | FreeCAD.closeDocument(cls.doc.Name) |
| | FreeCAD.ConfigSet("SuppressRecomputeRequiredDialog", "") |
| |
|
| | def setUp(self): |
| | self.pp = PathPost.PostProcessor(self.job, "generic", "", "") |
| |
|
| | def tearDown(self): |
| | pass |
| |
|
| | def test000(self): |
| |
|
| | |
| | self.assertEqual(len(self.job.Tools.Group), 2) |
| | self.assertEqual(len(self.job.Fixtures), 2) |
| | self.assertEqual( |
| | len(self.job.Operations.Group), 3 |
| | ) |
| |
|
| | self.job.SplitOutput = False |
| | self.job.OrderOutputBy = "Operation" |
| |
|
| | def test010(self): |
| | postlist = self.pp._buildPostList() |
| |
|
| | self.assertTrue(type(postlist) is list) |
| |
|
| | firstoutputitem = postlist[0] |
| | self.assertTrue(type(firstoutputitem) is tuple) |
| | self.assertTrue(type(firstoutputitem[0]) is str) |
| | self.assertTrue(type(firstoutputitem[1]) is list) |
| |
|
| | def test020(self): |
| | |
| | self.job.SplitOutput = False |
| | self.job.OrderOutputBy = "Operation" |
| | postlist = self.pp._buildPostList() |
| | self.assertEqual(len(postlist), 1) |
| |
|
| | def test030(self): |
| | |
| | self.job.SplitOutput = False |
| | self.job.OrderOutputBy = "Operation" |
| | postlist = self.pp._buildPostList() |
| | firstoutputitem = postlist[0] |
| | firstoplist = firstoutputitem[1] |
| | if self.debug: |
| | print(self._format_postables(postlist, "test030: No splitting, order by Operation")) |
| | self.assertEqual(len(firstoplist), 14) |
| |
|
| | def test040(self): |
| | |
| | |
| | teststring = "%T.nc" |
| | self.job.SplitOutput = True |
| | self.job.PostProcessorOutputFile = teststring |
| | self.job.OrderOutputBy = "Tool" |
| | postlist = self.pp._buildPostList() |
| |
|
| | firstoutputitem = postlist[0] |
| | if self.debug: |
| | print(self._format_postables(postlist, "test040: Split by tool, order by Tool")) |
| | self.assertTrue(firstoutputitem[0] == str(5)) |
| |
|
| | |
| | firstoplist = firstoutputitem[1] |
| | self.assertEqual(len(firstoplist), 5) |
| |
|
| | def test050(self): |
| | |
| | teststring = "%t.nc" |
| | self.job.SplitOutput = True |
| | self.job.PostProcessorOutputFile = teststring |
| | self.job.OrderOutputBy = "Tool" |
| | postlist = self.pp._buildPostList() |
| |
|
| | firstoutputitem = postlist[0] |
| | self.assertTrue(firstoutputitem[0] == "TC__7_16__two_flute") |
| |
|
| | def test060(self): |
| | |
| | teststring = "%W.nc" |
| | self.job.SplitOutput = True |
| | self.job.PostProcessorOutputFile = teststring |
| | self.job.OrderOutputBy = "Fixture" |
| | postlist = self.pp._buildPostList() |
| |
|
| | firstoutputitem = postlist[0] |
| | firstoplist = firstoutputitem[1] |
| | self.assertEqual(len(firstoplist), 6) |
| | self.assertTrue(firstoutputitem[0] == "G54") |
| |
|
| | def test070(self): |
| | self.job.SplitOutput = True |
| | self.job.PostProcessorOutputFile = "%T.nc" |
| | self.job.OrderOutputBy = "Tool" |
| | postables = self.pp._buildPostList(early_tool_prep=True) |
| | _, sublist = postables[0] |
| |
|
| | if self.debug: |
| | print(self._format_postables(postables, "test070: Early tool prep, split by tool")) |
| |
|
| | |
| | commands = [] |
| | if self.debug: |
| | print("\n=== Extracting commands from postables ===") |
| | for item in sublist: |
| | if self.debug: |
| | item_type = type(item).__name__ |
| | has_path = hasattr(item, "Path") |
| | path_exists = item.Path if has_path else None |
| | has_commands = path_exists and item.Path.Commands if path_exists else False |
| | print( |
| | f"Item: {getattr(item, 'Label', item_type)}, Type: {item_type}, HasPath: {has_path}, PathExists: {path_exists is not None}, HasCommands: {bool(has_commands)}" |
| | ) |
| | if has_commands: |
| | print(f" Commands: {[cmd.Name for cmd in item.Path.Commands]}") |
| | if hasattr(item, "Path") and item.Path and item.Path.Commands: |
| | commands.extend(item.Path.Commands) |
| |
|
| | if self.debug: |
| | print(f"\nTotal commands extracted: {len(commands)}") |
| | print("=" * 40) |
| |
|
| | |
| | m6_commands = [cmd for cmd in commands if cmd.Name == "M6"] |
| | self.assertTrue(len(m6_commands) > 0, "Should have M6 command") |
| |
|
| | |
| | first_m6 = m6_commands[0] |
| | self.assertTrue("T" in first_m6.Parameters, "First M6 should have T parameter") |
| | self.assertEqual(first_m6.Parameters["T"], 5.0, "First M6 should be for tool 5") |
| |
|
| | |
| | t2_commands = [cmd for cmd in commands if cmd.Name == "T2"] |
| | self.assertTrue(len(t2_commands) > 0, "Should have T2 early prep command") |
| |
|
| | |
| | first_m6_index = next((i for i, cmd in enumerate(commands) if cmd.Name == "M6"), None) |
| | t2_index = next((i for i, cmd in enumerate(commands) if cmd.Name == "T2"), None) |
| | self.assertIsNotNone(first_m6_index, "M6 should exist") |
| | self.assertIsNotNone(t2_index, "T2 should exist") |
| | self.assertLess(first_m6_index, t2_index, "M6 should come before T2 prep") |
| |
|
| | def test080(self): |
| | self.job.SplitOutput = False |
| | self.job.OrderOutputBy = "Tool" |
| |
|
| | postables = self.pp._buildPostList(early_tool_prep=True) |
| | _, sublist = postables[0] |
| |
|
| | if self.debug: |
| | print(self._format_postables(postables, "test080: Early tool prep, combined output")) |
| |
|
| | |
| | commands = [] |
| | if self.debug: |
| | print("\n=== Extracting commands from postables ===") |
| | for item in sublist: |
| | if self.debug: |
| | item_type = type(item).__name__ |
| | has_path = hasattr(item, "Path") |
| | path_exists = item.Path if has_path else None |
| | has_commands = path_exists and item.Path.Commands if path_exists else False |
| | print( |
| | f"Item: {getattr(item, 'Label', item_type)}, Type: {item_type}, HasPath: {has_path}, PathExists: {path_exists is not None}, HasCommands: {bool(has_commands)}" |
| | ) |
| | if has_commands: |
| | print(f" Commands: {[cmd.Name for cmd in item.Path.Commands]}") |
| | if hasattr(item, "Path") and item.Path and item.Path.Commands: |
| | commands.extend(item.Path.Commands) |
| |
|
| | if self.debug: |
| | print(f"\nTotal commands extracted: {len(commands)}") |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | if self.debug: |
| | print("\n=== Command Sequence ===") |
| | for i, cmd in enumerate(commands): |
| | params = " ".join( |
| | f"{k}:{v}" |
| | for k, v in zip( |
| | cmd.Parameters.keys() if hasattr(cmd.Parameters, "keys") else [], |
| | ( |
| | cmd.Parameters.values() |
| | if hasattr(cmd.Parameters, "values") |
| | else cmd.Parameters |
| | ), |
| | ) |
| | ) |
| | print(f"{i:3d}: {cmd.Name} {params}") |
| | print("=" * 40) |
| |
|
| | |
| | m6_commands = [(i, cmd) for i, cmd in enumerate(commands) if cmd.Name == "M6"] |
| | t2_commands = [(i, cmd) for i, cmd in enumerate(commands) if cmd.Name == "T2"] |
| |
|
| | self.assertTrue(len(m6_commands) >= 2, "Should have at least 2 M6 commands") |
| | self.assertTrue(len(t2_commands) >= 1, "Should have at least 1 T2 early prep command") |
| |
|
| | first_m6_idx, first_m6_cmd = m6_commands[0] |
| | second_m6_idx, second_m6_cmd = m6_commands[1] if len(m6_commands) >= 2 else (None, None) |
| | first_t2_idx = t2_commands[0][0] |
| |
|
| | |
| | self.assertTrue("T" in first_m6_cmd.Parameters, "First M6 should have T parameter") |
| | self.assertEqual(first_m6_cmd.Parameters["T"], 5.0, "First M6 should be for tool 5") |
| |
|
| | |
| | if second_m6_cmd is not None: |
| | self.assertTrue("T" in second_m6_cmd.Parameters, "Second M6 should have T parameter") |
| | self.assertEqual(second_m6_cmd.Parameters["T"], 2.0, "Second M6 should be for tool 2") |
| |
|
| | |
| | self.assertLess(first_m6_idx, first_t2_idx, "T2 prep should come after first M6") |
| | self.assertLess( |
| | first_t2_idx - first_m6_idx, 5, "T2 prep should be within a few commands of first M6" |
| | ) |
| |
|
| | |
| | if second_m6_idx is not None: |
| | self.assertLess( |
| | first_t2_idx, second_m6_idx, "T2 early prep should come before second M6" |
| | ) |
| |
|