From b1e4864910ceaf4f9b27033ea93b7f8e06eff657 Mon Sep 17 00:00:00 2001 From: Russell Johnson <47639332+Russ4262@users.noreply.github.com> Date: Sat, 26 Mar 2022 09:53:55 -0500 Subject: [PATCH 1/3] Path: Allow GUI Job creation without task panel interaction These changes allow for a Job object to be created with view provider support, but without the initial task panel interaction at creation. This is useful for scripting and other internal Job creation tasks. --- src/Mod/Path/PathScripts/PathJobGui.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathJobGui.py b/src/Mod/Path/PathScripts/PathJobGui.py index b1ae35eff1..8ea9cb60ab 100644 --- a/src/Mod/Path/PathScripts/PathJobGui.py +++ b/src/Mod/Path/PathScripts/PathJobGui.py @@ -1631,7 +1631,7 @@ class TaskPanel: self.updateSelection() -def Create(base, template=None): +def Create(base, template=None, openTaskPanel=True): """Create(base, template) ... creates a job instance for the given base object using template to configure it.""" FreeCADGui.addModule("PathScripts.PathJob") @@ -1642,7 +1642,10 @@ def Create(base, template=None): obj.ViewObject.addExtension("Gui::ViewProviderGroupExtensionPython") FreeCAD.ActiveDocument.commitTransaction() obj.Document.recompute() - obj.ViewObject.Proxy.editObject(obj.Stock) + if openTaskPanel: + obj.ViewObject.Proxy.editObject(obj.Stock) + else: + obj.ViewObject.Proxy.deleteOnReject = False return obj except Exception as exc: PathLog.error(exc) From e82283bd5b0d66eac748f07da6e34bd807b5fe56 Mon Sep 17 00:00:00 2001 From: Russell Johnson <47639332+Russ4262@users.noreply.github.com> Date: Sat, 26 Mar 2022 09:57:08 -0500 Subject: [PATCH 2/3] Path: Refactor `insert()` function and catch exceptions Path: Add method to verify dependencies for import Path: Add label to Custom op referencing imported filename when available Path: Fix module for command-line usage --- src/Mod/Path/PathScripts/post/gcode_pre.py | 150 ++++++++++++++------- 1 file changed, 102 insertions(+), 48 deletions(-) diff --git a/src/Mod/Path/PathScripts/post/gcode_pre.py b/src/Mod/Path/PathScripts/post/gcode_pre.py index 0c9c3f35ed..508b8aed12 100644 --- a/src/Mod/Path/PathScripts/post/gcode_pre.py +++ b/src/Mod/Path/PathScripts/post/gcode_pre.py @@ -47,11 +47,17 @@ import FreeCAD import PathScripts.PathUtils as PathUtils import PathScripts.PathLog as PathLog import re -import PathScripts.PathCustom as PathCustom -import PathScripts.PathCustomGui as PathCustomGui -import PathScripts.PathOpGui as PathOpGui from PySide.QtCore import QT_TRANSLATE_NOOP +if FreeCAD.GuiUp: + import PathScripts.PathCustomGui as PathCustomGui + + PathCustom = PathCustomGui.PathCustom +else: + import PathScripts.PathCustom as PathCustom + +translate = FreeCAD.Qt.translate + if False: PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) @@ -60,6 +66,20 @@ else: PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) +class PathNoActiveDocumentException(Exception): + """PathNoActiveDocumentException is raised when no active document is found.""" + + def __init__(self): + super().__init__("No active document") + + +class PathNoJobException(Exception): + """PathNoJobException is raised when no target Job object is available.""" + + def __init__(self): + super().__init__("No job object") + + # to distinguish python built-in open function from the one declared below if open.__module__ in ["__builtin__", "io"]: pythonopen = open @@ -82,54 +102,20 @@ def matchToolController(op, toolnumber): return toolcontrollers[0] -def insert(filename, docname): - """called when freecad imports a file""" - PathLog.track(filename) - gfile = pythonopen(filename) - gcode = gfile.read() - gfile.close() +def _isImportEnvironmentReady(): + """_isImportEnvironmentReady(docname)... + Helper function to verify an active document exists, and that a Job object is available + as a receiver for the Custom operation(s) that will be created as a result of the import process.""" - # Regular expression to match tool changes in the format 'M6 Tn' - p = re.compile("[mM]+?\s?0?6\s?T\d*\s") + # Verify active document exists + if FreeCAD.ActiveDocument is None: + raise PathNoActiveDocumentException() - # split the gcode on tool changes - paths = re.split("([mM]+?\s?0?6\s?T\d*\s)", gcode) + # Verify a Job object is available, and add one if not + if not PathUtils.GetJobs(): + raise PathNoJobException() - # iterate the gcode sections and add customs for each - toolnumber = 0 - - for path in paths: - - # if the section is a tool change, extract the tool number - m = p.match(path) - if m: - toolnumber = int(m.group().split("T")[-1]) - continue - - # Parse the gcode and throw away any empty lists - gcode = parse(path) - if len(gcode) == 0: - continue - - # Create a custom and viewobject - obj = PathCustom.Create("Custom") - res = PathOpGui.CommandResources( - "Custom", - PathCustom.Create, - PathCustomGui.TaskPanelOpPage, - "Path_Custom", - QT_TRANSLATE_NOOP("Path_Custom", "Custom"), - "", - "", - ) - obj.ViewObject.Proxy = PathOpGui.ViewProvider(obj.ViewObject, res) - obj.ViewObject.Proxy.setDeleteObjectsOnReject(False) - - # Set the gcode and try to match a tool controller - obj.Gcode = gcode - obj.ToolController = matchToolController(obj, toolnumber) - - FreeCAD.ActiveDocument.recompute() + return True def parse(inputstring): @@ -195,4 +181,72 @@ def parse(inputstring): return output +def _identifygcodeByToolNumberList(filename): + """called when freecad imports a file""" + PathLog.track(filename) + gcodeByToolNumberList = [] + + gfile = pythonopen(filename) + gcode = gfile.read() + gfile.close() + + # Regular expression to match tool changes in the format 'M6 Tn' + p = re.compile("[mM]+?\s?0?6\s?T\d*\s") + + # split the gcode on tool changes + paths = re.split("([mM]+?\s?0?6\s?T\d*\s)", gcode) + + # iterate the gcode sections and add customs for each + toolnumber = 0 + + for path in paths: + + # if the section is a tool change, extract the tool number + m = p.match(path) + if m: + toolnumber = int(m.group().split("T")[-1]) + continue + + # Parse the gcode and throw away any empty lists + gcode = parse(path) + if len(gcode) > 0: + gcodeByToolNumberList.append((gcode, toolnumber)) + + return gcodeByToolNumberList + + +def insert(filename, docname=None): + """called when freecad imports a file""" + PathLog.track(filename) + + try: + if not _isImportEnvironmentReady(): + return + except PathNoActiveDocumentException: + PathLog.error(translate("Path_Gcode_pre", "No active document")) + return + except PathNoJobException: + PathLog.error(translate("Path_Gcode_pre", "No job object")) + return + + # Create a Custom operation for each gcode-toolNumber pair + for gcode, toolNumber in _identifygcodeByToolNumberList(filename): + obj = PathCustom.Create("Custom") + + # Set the gcode and try to match a tool controller + obj.Gcode = gcode + obj.ToolController = matchToolController(obj, toolNumber) + if docname: + obj.Label = obj.Label + "_" + docname + + if FreeCAD.GuiUp: + # Add a view provider to a Custom operation object + obj.ViewObject.Proxy = PathCustomGui.PathOpGui.ViewProvider( + obj.ViewObject, PathCustomGui.Command.res + ) + obj.ViewObject.Proxy.setDeleteObjectsOnReject(False) + + FreeCAD.ActiveDocument.recompute() + + print(__name__ + " gcode preprocessor loaded.") From df9d9843c378068c020ceaca64778ae9bfd52cdf Mon Sep 17 00:00:00 2001 From: Russell Johnson <47639332+Russ4262@users.noreply.github.com> Date: Sat, 26 Mar 2022 19:23:42 -0500 Subject: [PATCH 3/3] Path: Add 5 unit tests for importing gcode with gcode_pre --- src/Mod/Path/PathTests/TestPathPost.py | 172 +++++++++++++++++++++++++ src/Mod/Path/TestPathApp.py | 2 + 2 files changed, 174 insertions(+) diff --git a/src/Mod/Path/PathTests/TestPathPost.py b/src/Mod/Path/PathTests/TestPathPost.py index 468e2f2316..3bddac16c3 100644 --- a/src/Mod/Path/PathTests/TestPathPost.py +++ b/src/Mod/Path/PathTests/TestPathPost.py @@ -152,3 +152,175 @@ class TestPathPostUtils(unittest.TestCase): self.assertTrue( len([c for c in results.Commands if c.Name in ["G2", "G3"]]) == 0 ) + + +class TestPathPostImport(unittest.TestCase): + def test001(self): + """test001()... Verify 'No active document' exception thrown if no document open.""" + from PathScripts.post import gcode_pre as gcode_pre + + self.assertRaises( + gcode_pre.PathNoActiveDocumentException, + gcode_pre._isImportEnvironmentReady, + ) + + def test002(self): + """test002()... Verify 'No job object' exception thrown if Job object available.""" + from PathScripts.post import gcode_pre as gcode_pre + + doc = FreeCAD.newDocument("TestPathPost") + + self.assertRaises( + gcode_pre.PathNoJobException, + gcode_pre._isImportEnvironmentReady, + ) + FreeCAD.closeDocument(doc.Name) + + def test003(self, close=True): + """test003()... Verify 'No job object' exception thrown if Job object available.""" + from PathScripts.post import gcode_pre as gcode_pre + + doc = FreeCAD.newDocument("TestPathPost") + + # Add temporary receiving Job object + box = FreeCAD.ActiveDocument.addObject("Part::Box", "Box") + box.Label = "Temporary Box" + # Add Job object with view provider support when possible + if FreeCAD.GuiUp: + import PathScripts.PathJobGui as PathJobGui + + box.ViewObject.Visibility = False + job = PathJobGui.Create([box], openTaskPanel=False) + else: + import PathScripts.PathJob as PathJob + + job = PathJob.Create("Job", [box]) + + importFile = FreeCAD.getHomePath() + "Mod/Path/PathTests/test_centroid_00.ngc" + gcode_pre.insert(importFile, "test_centroid_00") + + # self.assertTrue(doc.Name == "test_centroid_00") + + opList = doc.Job.Operations.Group + self.assertTrue( + len(opList) == 2, + "Expected 2 Custom operations to be created from source g-code file, test_centroid_00.ngc", + ) + self.assertTrue( + opList[0].Name == "Custom", "Expected first operation to be Custom" + ) + self.assertTrue( + opList[1].Name == "Custom001", "Expected second operation to be Custom001" + ) + + if close: + FreeCAD.closeDocument(doc.Name) + + def test004(self): + """test004()... Verify g-code imported with g-code pre-processor""" + + self.test003(close=False) + + doc = FreeCAD.ActiveDocument + op1 = doc.Job.Operations.Group[0] + op2 = doc.Job.Operations.Group[1] + + # Verify g-code sizes + self.assertTrue( + op1.Path.Size == 4, "Expected Custom g-code command count to be 4." + ) + self.assertTrue( + op2.Path.Size == 60, "Expected Custom g-code command count to be 60." + ) + + # Verify g-code commands + op1_code = ( + "(Custom_test_centroid_00)\n(Begin Custom)\nG90 G49.000000\n(End Custom)\n" + ) + op2_code = "(Custom001_test_centroid_00)\n(Begin Custom)\nG0 Z15.000000\nG90\nG0 Z15.000000\nG0 X10.000000 Y10.000000\nG0 Z10.000000\nG1 X10.000000 Y10.000000 Z9.000000\nG1 X10.000000 Y0.000000 Z9.000000\nG1 X0.000000 Y0.000000 Z9.000000\nG1 X0.000000 Y10.000000 Z9.000000\nG1 X10.000000 Y10.000000 Z9.000000\nG1 X10.000000 Y10.000000 Z8.000000\nG1 X10.000000 Y0.000000 Z8.000000\nG1 X0.000000 Y0.000000 Z8.000000\nG1 X0.000000 Y10.000000 Z8.000000\nG1 X10.000000 Y10.000000 Z8.000000\nG1 X10.000000 Y10.000000 Z7.000000\nG1 X10.000000 Y0.000000 Z7.000000\nG1 X0.000000 Y0.000000 Z7.000000\nG1 X0.000000 Y10.000000 Z7.000000\nG1 X10.000000 Y10.000000 Z7.000000\nG1 X10.000000 Y10.000000 Z6.000000\nG1 X10.000000 Y0.000000 Z6.000000\nG1 X0.000000 Y0.000000 Z6.000000\nG1 X0.000000 Y10.000000 Z6.000000\nG1 X10.000000 Y10.000000 Z6.000000\nG1 X10.000000 Y10.000000 Z5.000000\nG1 X10.000000 Y0.000000 Z5.000000\nG1 X0.000000 Y0.000000 Z5.000000\nG1 X0.000000 Y10.000000 Z5.000000\nG1 X10.000000 Y10.000000 Z5.000000\nG1 X10.000000 Y10.000000 Z4.000000\nG1 X10.000000 Y0.000000 Z4.000000\nG1 X0.000000 Y0.000000 Z4.000000\nG1 X0.000000 Y10.000000 Z4.000000\nG1 X10.000000 Y10.000000 Z4.000000\nG1 X10.000000 Y10.000000 Z3.000000\nG1 X10.000000 Y0.000000 Z3.000000\nG1 X0.000000 Y0.000000 Z3.000000\nG1 X0.000000 Y10.000000 Z3.000000\nG1 X10.000000 Y10.000000 Z3.000000\nG1 X10.000000 Y10.000000 Z2.000000\nG1 X10.000000 Y0.000000 Z2.000000\nG1 X0.000000 Y0.000000 Z2.000000\nG1 X0.000000 Y10.000000 Z2.000000\nG1 X10.000000 Y10.000000 Z2.000000\nG1 X10.000000 Y10.000000 Z1.000000\nG1 X10.000000 Y0.000000 Z1.000000\nG1 X0.000000 Y0.000000 Z1.000000\nG1 X0.000000 Y10.000000 Z1.000000\nG1 X10.000000 Y10.000000 Z1.000000\nG1 X10.000000 Y10.000000 Z0.000000\nG1 X10.000000 Y0.000000 Z0.000000\nG1 X0.000000 Y0.000000 Z0.000000\nG1 X0.000000 Y10.000000 Z0.000000\nG1 X10.000000 Y10.000000 Z0.000000\nG0 Z15.000000\nG90 G49.000000\n(End Custom)\n" + code1 = op1.Path.toGCode() + self.assertTrue( + code1 == op1_code, + f"Gcode is not what is expected:\n~~~\n{code1}\n~~~\n\n\n~~~\n{op1_code}\n~~~", + ) + code2 = op2.Path.toGCode() + self.assertTrue( + code2 == op2_code, + f"Gcode is not what is expected:\n~~~\n{code2}\n~~~\n\n\n~~~\n{op2_code}\n~~~", + ) + FreeCAD.closeDocument(doc.Name) + + def test005(self): + """test005()... verify `_identifygcodeByToolNumberList()` produces correct output""" + + from PathScripts.post import gcode_pre as gcode_pre + + importFile = FreeCAD.getHomePath() + "Mod/Path/PathTests/test_centroid_00.ngc" + gcodeByToolNumberList = gcode_pre._identifygcodeByToolNumberList(importFile) + + self.assertTrue(gcodeByToolNumberList[0][0] == ["G90 G80 G40 G49"]) + self.assertTrue(gcodeByToolNumberList[0][1] == 0) + + self.assertTrue( + gcodeByToolNumberList[1][0] + == [ + "G0 Z15.00", + "G90", + "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", + "G90 G80 G40 G49", + ] + ) + self.assertTrue(gcodeByToolNumberList[1][1] == 2) diff --git a/src/Mod/Path/TestPathApp.py b/src/Mod/Path/TestPathApp.py index 28a58e0fb9..f917454ede 100644 --- a/src/Mod/Path/TestPathApp.py +++ b/src/Mod/Path/TestPathApp.py @@ -40,6 +40,7 @@ from PathTests.TestPathOpTools import TestPathOpTools # from PathTests.TestPathPost import PathPostTestCases from PathTests.TestPathPost import TestPathPostUtils +from PathTests.TestPathPost import TestPathPostImport from PathTests.TestPathPreferences import TestPathPreferences from PathTests.TestPathPropertyBag import TestPathPropertyBag from PathTests.TestPathRotationGenerator import TestPathRotationGenerator @@ -70,6 +71,7 @@ False if TestPathHelpers.__name__ else True # False if TestPathHelix.__name__ else True False if TestPathLog.__name__ else True False if TestPathOpTools.__name__ else True +False if TestPathPostImport.__name__ else True # False if TestPathPost.__name__ else True False if TestPathPostUtils.__name__ else True False if TestPathPreferences.__name__ else True