diff --git a/src/Mod/Path/PathScripts/PathPost.py b/src/Mod/Path/PathScripts/PathPost.py index ddb6d8dfb9..3250aabdb8 100644 --- a/src/Mod/Path/PathScripts/PathPost.py +++ b/src/Mod/Path/PathScripts/PathPost.py @@ -33,6 +33,7 @@ import PathScripts.PathPreferences as PathPreferences import PathScripts.PathUtil as PathUtil import PathScripts.PathUtils as PathUtils import os +import re from PathScripts.PathPostProcessor import PostProcessor from PySide import QtCore, QtGui @@ -47,15 +48,359 @@ if False: else: PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) + translate = FreeCAD.Qt.translate class _TempObject: Path = None Name = "Fixture" + InList = [] Label = "Fixture" +def resolveFileName(job, subpartname, sequencenumber): + PathLog.track(subpartname, sequencenumber) + + validPathSubstitutions = ["D", "d", "M", "j"] + validFilenameSubstitutions = ["j", "d", "T", "t", "W", "O", "S"] + + # Look for preference default + outputpath, filename = os.path.split(PathPreferences.defaultOutputFile()) + filename, ext = os.path.splitext(filename) + + # Override with document default if it exists + if job.PostProcessorOutputFile: + matchstring = job.PostProcessorOutputFile + candidateOutputPath, candidateFilename = os.path.split(matchstring) + + if candidateOutputPath: + outputpath = candidateOutputPath + + if candidateFilename: + filename, ext = os.path.splitext(candidateFilename) + + # Strip any invalid substitutions from the ouputpath + for match in re.findall("%(.)", outputpath): + if match not in validPathSubstitutions: + outputpath = outputpath.replace(f"%{match}", "") + + # if nothing else, use current directory + if not outputpath: + outputpath = "." + + # Strip any invalid substitutions from the filename + for match in re.findall("%(.)", filename): + if match not in validFilenameSubstitutions: + filename = filename.replace(f"%{match}", "") + + # if no filename, use the active document label + if not filename: + filename = FreeCAD.ActiveDocument.Label + + # if no extension, use something sensible + if not ext: + ext = "nc" + + # By now we should have a sanitized path, filename and extension to work with + PathLog.track(f"path: {outputpath} name: {filename} ext: {ext}") + + # The following section allows substitution within the path part + PathLog.track(f"path before substitution: {outputpath}") + + if "%D" in outputpath: # Directory of active document + D = FreeCAD.ActiveDocument.FileName + if D: + D = os.path.dirname(D) + # in case the document is in the current working directory + if not D: + D = "." + else: + FreeCAD.Console.PrintError( + "Please save document in order to resolve output path!\n" + ) + return None + outputpath = outputpath.replace("%D", D) + + if "%M" in outputpath: + M = FreeCAD.getUserMacroDir() + outputpath = outputpath.replace("%M", M) + + # Use the file label + if "%d" in outputpath: + d = FreeCAD.ActiveDocument.Label + outputpath = outputpath.replace("%d", d) + + # Use the name of the active job object + if "%j" in outputpath: + j = job.Label + outputpath = outputpath.replace("%j", j) + + PathLog.track(f"path after substitution: {outputpath}") + + # The following section allows substitution within the filename part + PathLog.track(f"filename before substitution: {filename}") + + # Use the file label + if "%d" in filename: + d = FreeCAD.ActiveDocument.Label + filename = filename.replace("%d", d) + + # Use the name of the active job object + if "%j" in filename: + j = job.Label + filename = filename.replace("%j", j) + + # Use the sequnce number if explicitly called + if "%S" in filename: + j = job.Label + filename = filename.replace("%S", str(sequencenumber)) + + # This section handles unique names for splitting output + if job.SplitOutput: + PathLog.track() + if "%T" in filename and job.OrderOutputBy == "Tool": + filename = filename.replace("%T", subpartname) + + if "%t" in filename and job.OrderOutputBy == "Tool": + filename = filename.replace("%t", subpartname) + + if "%W" in filename and job.OrderOutputBy == "Fixture": + filename = filename.replace("%W", subpartname) + + if "%O" in filename and job.OrderOutputBy == "Operation": + filename = filename.replace("%O", subpartname) + + if ( + "%S" in filename + ): # We always add a sequence number but the user can say where + filename = filename.replace("%S", str(sequencenumber)) + else: + filename = f"{filename}-{sequencenumber}" + + PathLog.track(f"filename after substitution: {filename}") + + if not ext: + ext = ".nc" + PathLog.track(f"file extension: {ext}") + + fullPath = f"{outputpath}{os.path.sep}{filename}{ext}" + + PathLog.track(f"full filepath: {fullPath}") + + # This section determines whether user interaction is necessary + policy = PathPreferences.defaultOutputPolicy() + + openDialog = policy == "Open File Dialog" + # if os.path.isdir(filename) or not os.path.isdir(os.path.dirname(filename)): + # # Either the entire filename resolves into a directory or the parent directory doesn't exist. + # # Either way I don't know what to do - ask for help + # openDialog = True + + if not FreeCAD.GuiUp: # if testing, or scripting, never open dialog. + policy = "Append Unique ID on conflict" + openDialog = False + + if os.path.isfile(fullPath) and not openDialog: + if policy == "Open File Dialog on conflict": + openDialog = True + elif policy == "Append Unique ID on conflict": + fn, ext = os.path.splitext(fullPath) + nr = fn[-3:] + n = 1 + if nr.isdigit(): + n = int(nr) + while os.path.isfile("%s%03d%s" % (fn, n, ext)): + n = n + 1 + fullPath = "%s%03d%s" % (fn, n, ext) + + if openDialog: + foo = QtGui.QFileDialog.getSaveFileName( + QtGui.QApplication.activeWindow(), "Output File", filename + ) + if foo[0]: + fullPath = foo[0] + else: + fullPath = None + + # remove any unused substitution strings: + for s in validPathSubstitutions + validFilenameSubstitutions: + fullPath = fullPath.replace(f"%{s}", "") + + fullPath = os.path.normpath(fullPath) + PathLog.track(fullPath) + return fullPath + + +def buildPostList(job): + """Takes the job and determines the specific objects and order to + postprocess Returns a list of objects which can be passed to + exportObjectsWith() for final posting""" + wcslist = job.Fixtures + orderby = job.OrderOutputBy + + postlist = [] + + if orderby == "Fixture": + PathLog.debug("Ordering by Fixture") + # Order by fixture means all operations and tool changes will be completed in one + # fixture before moving to the next. + + currTool = None + for index, f in enumerate(wcslist): + # create an object to serve as the fixture path + fobj = _TempObject() + c1 = Path.Command(f) + fobj.Path = Path.Path([c1]) + if index != 0: + c2 = Path.Command( + "G0 Z" + + str( + job.Stock.Shape.BoundBox.ZMax + + job.SetupSheet.ClearanceHeightOffset.Value + ) + ) + fobj.Path.addCommands(c2) + fobj.InList.append(job) + sublist = [fobj] + + # Now generate the gcode + for obj in job.Operations.Group: + tc = PathUtil.toolControllerForOp(obj) + if tc is not None and PathUtil.opProperty(obj, "Active"): + if tc.ToolNumber != currTool: + sublist.append(tc) + PathLog.debug("Appending TC: {}".format(tc.Name)) + currTool = tc.ToolNumber + sublist.append(obj) + postlist.append((f, sublist)) + + elif orderby == "Tool": + PathLog.debug("Ordering by Tool") + # Order by tool means tool changes are minimized. + # all operations with the current tool are processed in the current + # fixture before moving to the next fixture. + + toolstring = "None" + currTool = None + + # Build the fixture list + fixturelist = [] + for f in wcslist: + # create an object to serve as the fixture path + fobj = _TempObject() + c1 = Path.Command(f) + c2 = Path.Command( + "G0 Z" + + str( + job.Stock.Shape.BoundBox.ZMax + + job.SetupSheet.ClearanceHeightOffset.Value + ) + ) + fobj.Path = Path.Path([c1, c2]) + fobj.InList.append(job) + fixturelist.append(fobj) + + # Now generate the gcode + curlist = [] # list of ops for tool, will repeat for each fixture + sublist = [] # list of ops for output splitting + + PathLog.track(job.PostProcessorOutputFile) + for idx, obj in enumerate(job.Operations.Group): + PathLog.track(obj.Label) + + # check if the operation is active + if not getattr(obj, "Active", True): + PathLog.track() + continue + + # Determine the proper string for the Op's TC + tc = PathUtil.toolControllerForOp(obj) + if tc is None: + tcstring = "None" + elif "%T" in job.PostProcessorOutputFile: + tcstring = f"{tc.ToolNumber}" + else: + tcstring = re.sub(r"[^\w\d-]", "_", tc.Label) + PathLog.track(toolstring) + + if tc is None or tc.ToolNumber == currTool: + curlist.append(obj) + elif tc.ToolNumber != currTool and currTool is None: # first TC + sublist.append(tc) + curlist.append(obj) + currTool = tc.ToolNumber + toolstring = tcstring + + elif tc.ToolNumber != currTool and currTool is not None: # TC + for fixture in fixturelist: + sublist.append(fixture) + sublist.extend(curlist) + postlist.append((toolstring, sublist)) + sublist = [tc] + curlist = [obj] + currTool = tc.ToolNumber + toolstring = tcstring + + if idx == len(job.Operations.Group) - 1: # Last operation. + for fixture in fixturelist: + sublist.append(fixture) + sublist.extend(curlist) + + postlist.append((toolstring, sublist)) + + elif orderby == "Operation": + PathLog.debug("Ordering by Operation") + # Order by operation means ops are done in each fixture in + # sequence. + currTool = None + firstFixture = True + + # Now generate the gcode + for obj in job.Operations.Group: + + # check if the operation is active + if not getattr(obj, "Active", True): + continue + + sublist = [] + PathLog.debug("obj: {}".format(obj.Name)) + + for f in wcslist: + fobj = _TempObject() + c1 = Path.Command(f) + fobj.Path = Path.Path([c1]) + if not firstFixture: + c2 = Path.Command( + "G0 Z" + + str( + job.Stock.Shape.BoundBox.ZMax + + job.SetupSheet.ClearanceHeightOffset.Value + ) + ) + fobj.Path.addCommands(c2) + fobj.InList.append(job) + sublist.append(fobj) + firstFixture = False + tc = PathUtil.toolControllerForOp(obj) + if tc is not None: + if job.SplitOutput or (tc.ToolNumber != currTool): + sublist.append(tc) + currTool = tc.ToolNumber + sublist.append(obj) + postlist.append((obj.Label, sublist)) + + if job.SplitOutput: + PathLog.track() + return postlist + else: + PathLog.track() + finalpostlist = [ + ("allitems", [item for slist in postlist for item in slist[1]]) + ] + return finalpostlist + + class DlgSelectPostProcessor: def __init__(self, parent=None): self.dialog = FreeCADGui.PySideUic.loadUi(":/panels/DlgSelectPostProcessor.ui") @@ -96,78 +441,6 @@ class DlgSelectPostProcessor: class CommandPathPost: subpart = 1 - def resolveFileName(self, job): - path = PathPreferences.defaultOutputFile() - if job.PostProcessorOutputFile: - path = job.PostProcessorOutputFile - filename = path - - if "%D" in filename: - D = FreeCAD.ActiveDocument.FileName - if D: - D = os.path.dirname(D) - # in case the document is in the current working directory - if not D: - D = "." - else: - FreeCAD.Console.PrintError( - "Please save document in order to resolve output path!\n" - ) - return None - filename = filename.replace("%D", D) - - if "%d" in filename: - d = FreeCAD.ActiveDocument.Label - filename = filename.replace("%d", d) - - if "%j" in filename: - j = job.Label - filename = filename.replace("%j", j) - - if "%M" in filename: - pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Macro") - M = pref.GetString("MacroPath", FreeCAD.getUserAppDataDir()) - filename = filename.replace("%M", M) - - if "%s" in filename: - if job.SplitOutput: - filename = filename.replace("%s", "_" + str(self.subpart)) - self.subpart += 1 - else: - filename = filename.replace("%s", "") - - policy = PathPreferences.defaultOutputPolicy() - - openDialog = policy == "Open File Dialog" - if os.path.isdir(filename) or not os.path.isdir(os.path.dirname(filename)): - # Either the entire filename resolves into a directory or the parent directory doesn't exist. - # Either way I don't know what to do - ask for help - openDialog = True - - if os.path.isfile(filename) and not openDialog: - if policy == "Open File Dialog on conflict": - openDialog = True - elif policy == "Append Unique ID on conflict": - fn, ext = os.path.splitext(filename) - nr = fn[-3:] - n = 1 - if nr.isdigit(): - n = int(nr) - while os.path.isfile("%s%03d%s" % (fn, n, ext)): - n = n + 1 - filename = "%s%03d%s" % (fn, n, ext) - - if openDialog: - foo = QtGui.QFileDialog.getSaveFileName( - QtGui.QApplication.activeWindow(), "Output File", filename - ) - if foo[0]: - filename = foo[0] - else: - filename = None - - return filename - def resolvePostProcessor(self, job): if hasattr(job, "PostProcessor"): post = PathPreferences.defaultPostProcessor() @@ -195,191 +468,42 @@ class CommandPathPost: return False - def exportObjectsWith(self, objs, job, needFilename=True): - PathLog.track() + def exportObjectsWith(self, objs, partname, job, sequence, extraargs=None): + PathLog.track(extraargs) # check if the user has a project and has set the default post and # output filename + # extraargs can be passed in at this time + PathLog.track(partname, sequence) + PathLog.track(objs) + + partname = objs[0] + slist = objs[1] + postArgs = PathPreferences.defaultPostProcessorArgs() if hasattr(job, "PostProcessorArgs") and job.PostProcessorArgs: postArgs = job.PostProcessorArgs elif hasattr(job, "PostProcessor") and job.PostProcessor: postArgs = "" + if extraargs is not None: + postArgs += " {}".format(extraargs) + + PathLog.track(postArgs) + postname = self.resolvePostProcessor(job) - filename = "-" - if postname and needFilename: - filename = self.resolveFileName(job) + # filename = "-" + filename = resolveFileName(job, partname, sequence) + # if postname and needFilename: + # filename = resolveFileName(job) if postname and filename: print("post: %s(%s, %s)" % (postname, filename, postArgs)) processor = PostProcessor.load(postname) - gcode = processor.export(objs, filename, postArgs) + gcode = processor.export(slist, filename, postArgs) return (False, gcode, filename) else: return (True, "", filename) - def buildPostList(self, job): - """ - Parses the job and returns the list(s) of objects to be written by the post - Postlist is a list of lists. Each sublist is intended to be a separate file - """ - orderby = job.OrderOutputBy - - fixturelist = [] - for f in job.Fixtures: - # create an object to serve as the fixture path - fobj = _TempObject() - fobj.Label = f - c1 = Path.Command(f) - c2 = Path.Command( - "G0 Z" - + str( - job.Stock.Shape.BoundBox.ZMax - + job.SetupSheet.ClearanceHeightOffset.Value - ) - ) - fobj.Path = Path.Path([c1, c2]) - # fobj.InList.append(job) - fixturelist.append(fobj) - - postlist = [] - - if orderby == "Fixture": - PathLog.debug("Ordering by Fixture") - # Order by fixture means all operations and tool changes will be completed in one - # fixture before moving to the next. - - for f in fixturelist: - scratchpad = [(f, None)] - - # Now generate the gcode - for obj in job.Operations.Group: - if not PathUtil.opProperty(obj, "Active"): - continue - tc = PathUtil.toolControllerForOp(obj) - scratchpad.append((obj, tc)) - - sublist = [] - temptool = None - for item in scratchpad: - if item[1] in [temptool, None]: - sublist.append(item[0]) - else: - sublist.append(item[1]) - temptool = item[1] - sublist.append(item[0]) - postlist.append(sublist) - - elif orderby == "Tool": - PathLog.debug("Ordering by Tool") - # Order by tool means tool changes are minimized. - # all operations with the current tool are processed in the current - # fixture before moving to the next fixture. - - currTool = None - - # Now generate the gcode - curlist = [] # list of ops for tool, will repeat for each fixture - # sublist = [] # list of ops for output splitting - - for idx, obj in enumerate(job.Operations.Group): - - # check if the operation is active - active = PathUtil.opProperty(obj, "Active") - - tc = PathUtil.toolControllerForOp(obj) - - if not active: # pass on any inactive ops - continue - - if tc is None: - curlist.append((obj, None)) - continue - - if tc == currTool: - curlist.append((obj, tc)) - continue - - if tc != currTool and currTool is None: # first TC - currTool = tc - curlist.append((obj, tc)) - continue - - if tc != currTool and currTool is not None: # TC changed - if tc.ToolNumber == currTool.ToolNumber: # Same tool /diff params - curlist.append((obj, tc)) - currTool = tc - else: # Actual Toolchange - # dump current state to postlist - sublist = [] - t = None - for fixture in fixturelist: - sublist.append(fixture) - for item in curlist: - if item[1] == t: - sublist.append(item[0]) - else: - sublist.append(item[1]) - t = item[1] - sublist.append(item[0]) - - postlist.append(sublist) - - # set up for next tool group - currTool = tc - curlist = [(obj, tc)] - - # flush remaining curlist to output - sublist = [] - t = None - for fixture in fixturelist: - sublist.append(fixture) - for item in curlist: - if item[1] == t: - sublist.append(item[0]) - else: - sublist.append(item[1]) - t = item[1] - sublist.append(item[0]) - postlist.append(sublist) - - elif orderby == "Operation": - PathLog.debug("Ordering by Operation") - # Order by operation means ops are done in each fixture in - # sequence. - currTool = None - # firstFixture = True - - # Now generate the gcode - for obj in job.Operations.Group: - scratchpad = [] - tc = PathUtil.toolControllerForOp(obj) - if not PathUtil.opProperty(obj, "Active"): - continue - - PathLog.debug("obj: {}".format(obj.Name)) - for f in fixturelist: - - scratchpad.append((f, None)) - scratchpad.append((obj, tc)) - - sublist = [] - temptool = None - for item in scratchpad: - if item[1] in [temptool, None]: - sublist.append(item[0]) - else: - sublist.append(item[1]) - temptool = item[1] - sublist.append(item[0]) - postlist.append(sublist) - - if job.SplitOutput: - return postlist - else: - finalpostlist = [item for slist in postlist for item in slist] - return [finalpostlist] - def Activated(self): PathLog.track() FreeCAD.ActiveDocument.openTransaction("Post Process the Selected path(s)") @@ -429,26 +553,33 @@ class CommandPathPost: PathLog.debug("about to postprocess job: {}".format(job.Name)) - postlist = self.buildPostList(job) + postlist = buildPostList(job) + filename = resolveFileName(job) - fail = True - rc = "" - for slist in postlist: - (fail, rc, filename) = self.exportObjectsWith(slist, job) - if fail: - break + success = True + gcode = "" + if job.SplitOutput: + for idx, sublist in enumerate(postlist): # name, slist in postlist: + result = self.exportObjectsWith(sublist[1], sublist[0], job, idx) - self.subpart = 1 + if result is None: + success = False + else: + gcode += result - if fail: - FreeCAD.ActiveDocument.abortTransaction() else: + finalpostlist = [item for (_, slist) in postlist for item in slist] + gcode = self.exportObjectsWith(finalpostlist, "allitems", job, 1) + success = gcode is not None + + if success: if hasattr(job, "LastPostProcessDate"): job.LastPostProcessDate = str(datetime.now()) if hasattr(job, "LastPostProcessOutput"): job.LastPostProcessOutput = filename FreeCAD.ActiveDocument.commitTransaction() - + else: + FreeCAD.ActiveDocument.abortTransaction() FreeCAD.ActiveDocument.recompute() diff --git a/src/Mod/Path/PathTests/TestPathPost.py b/src/Mod/Path/PathTests/TestPathPost.py index a90d3bf235..692371b6aa 100644 --- a/src/Mod/Path/PathTests/TestPathPost.py +++ b/src/Mod/Path/PathTests/TestPathPost.py @@ -32,6 +32,8 @@ import PathScripts.PostUtils as PostUtils import difflib import unittest import Path +import os +import PathScripts.PathPost as PathPost WriteDebugOutput = False @@ -127,7 +129,7 @@ class PathPostTestCases(unittest.TestCase): class TestPathPostUtils(unittest.TestCase): - def testSplitArcs(self): + def test010(self): commands = [ Path.Command("G1 X-7.5 Y5.0 Z0.0"), @@ -154,473 +156,311 @@ class TestPathPostUtils(unittest.TestCase): ) -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 +def dumpgroup(group): + print("====Dump Group======") + for i in group: + print(i[0]) + for j in i[1]: + print(f"--->{j.Name}") + print("====================") 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 +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. - doc = FreeCAD.newDocument("TestPathPost") + 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 - 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 + testfile = FreeCAD.getHomePath() + "Mod/Path/PathTests/test_filenaming.fcstd" + doc = FreeCAD.open(testfile) + job = doc.getObjectsByLabel("MainJob")[0] - doc = FreeCAD.newDocument("TestPathPost") + def test000(self): - # 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 + # check that the test file is structured correctly + self.assertTrue(len(self.job.Tools.Group) == 2) + self.assertTrue(len(self.job.Fixtures) == 2) + self.assertTrue(len(self.job.Operations.Group) == 3) - box.ViewObject.Visibility = False - job = PathJobGui.Create([box], openTaskPanel=False) - else: - import PathScripts.PathJob as PathJob + self.job.SplitOutput = False + self.job.OrderOutputBy = "Operation" - job = PathJob.Create("Job", [box]) + def test010(self): + # check that function returns correct hash + postlist = PathPost.buildPostList(self.job) - importFile = FreeCAD.getHomePath() + "Mod/Path/PathTests/test_centroid_00.ngc" - gcode_pre.insert(importFile, "test_centroid_00") + self.assertTrue(type(postlist) is list) - # self.assertTrue(doc.Name == "test_centroid_00") + firstoutputitem = postlist[0] + self.assertTrue(type(firstoutputitem) is tuple) + self.assertTrue(type(firstoutputitem[0]) is str) + self.assertTrue(type(firstoutputitem[1]) is list) - 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" - ) + def test020(self): + # Without splitting, result should be list of one item + self.job.SplitOutput = False + self.job.OrderOutputBy = "Operation" + postlist = PathPost.buildPostList(self.job) + self.assertTrue(len(postlist) == 1) - if close: - FreeCAD.closeDocument(doc.Name) + def test030(self): + # No splitting should include all ops, tools, and fixtures + self.job.SplitOutput = False + self.job.OrderOutputBy = "Operation" + postlist = PathPost.buildPostList(self.job) + firstoutputitem = postlist[0] + firstoplist = firstoutputitem[1] + self.assertTrue(len(firstoplist) == 14) - def test004(self): - """test004()... Verify g-code imported with g-code pre-processor""" + def test040(self): + # Test splitting by tool + # ordering by tool with toolnumber for string + teststring = "%T.nc" + self.job.SplitOutput = True + self.job.PostProcessorOutputFile = teststring + self.job.OrderOutputBy = "Tool" + postlist = PathPost.buildPostList(self.job) - self.test003(close=False) + firstoutputitem = postlist[0] + self.assertTrue(firstoutputitem[0] == str(5)) - doc = FreeCAD.ActiveDocument - op1 = doc.Job.Operations.Group[0] - op2 = doc.Job.Operations.Group[1] + # check length of output + firstoplist = firstoutputitem[1] + self.assertTrue(len(firstoplist) == 5) - # 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." - ) + def test050(self): + # ordering by tool with tool description for string + teststring = "%t.nc" + self.job.SplitOutput = True + self.job.PostProcessorOutputFile = teststring + self.job.OrderOutputBy = "Tool" + postlist = PathPost.buildPostList(self.job) - # 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) + firstoutputitem = postlist[0] + self.assertTrue(firstoutputitem[0] == "TC__7_16__two_flute") - def test005(self): - """test005()... verify `_identifygcodeByToolNumberList()` produces correct output""" + def test060(self): + # Ordering by fixture and splitting + teststring = "%W.nc" + self.job.SplitOutput = True + self.job.PostProcessorOutputFile = teststring + self.job.OrderOutputBy = "Fixture" - from PathScripts.post import gcode_pre as gcode_pre + postlist = PathPost.buildPostList(self.job) + firstoutputitem = postlist[0] + firstoplist = firstoutputitem[1] + self.assertTrue(len(firstoplist) == 6) + self.assertTrue(firstoutputitem[0] == "G54") 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) +class TestOutputNameSubstitution(unittest.TestCase): - 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) + """ + 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 -class OutputOrderingTestCases(unittest.TestCase): - def setUp(self): - testfile = FreeCAD.getHomePath() + "Mod/Path/PathTests/boxtest.fcstd" - self.doc = FreeCAD.open(testfile) - self.job = FreeCAD.ActiveDocument.getObject("Job001") + The Following can be used if output is being split. If Output is not split + these will be ignored. + %S ... Sequence Number (default) - def tearDown(self): - FreeCAD.closeDocument("boxtest") + %T ... Tool Number + %t ... Tool Controller label - def test010(self): - # Basic postprocessing: + %W ... Work Coordinate System + %O ... Operation Label self.job.Fixtures = ["G54"] self.job.SplitOutput = False self.job.OrderOutputBy = "Fixture" - cpp = PathPost.CommandPathPost - self.postlist = cpp.buildPostList(self, self.job) + 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) - outlist = [i.Label for i in self.postlist[0]] + 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" - self.assertTrue(len(self.postlist) == 1) - expected = [ - "G54", - "T1", - "FirstOp-(T1)", - "SecondOp-(T1)", - "T2", - "ThirdOp-(T2)", - "T1", - "FourthOp-(T1)", - "T3", - "FifthOp-(T3)", - ] - self.assertListEqual(outlist, expected) + """ + + testfile = FreeCAD.getHomePath() + "Mod/Path/PathTests/test_filenaming.fcstd" + testfilepath, testfilename = os.path.split(testfile) + testfilename, ext = os.path.splitext(testfilename) + + doc = FreeCAD.open(testfile) + job = doc.getObjectsByLabel("MainJob")[0] + macro = FreeCAD.getUserMacroDir() + + def test000(self): + # Test basic string substitution without splitting + teststring = "~/Desktop/%j.nc" + self.job.PostProcessorOutputFile = teststring + self.job.SplitOutput = False + outlist = PathPost.buildPostList(self.job) + + self.assertTrue(len(outlist) == 1) + subpart, objs = outlist[0] + + filename = PathPost.resolveFileName(self.job, subpart, 0) + self.assertEqual(filename, "~/Desktop/MainJob.nc") + + def test010(self): + # Substitute current file path + teststring = "%D/testfile.nc" + self.job.PostProcessorOutputFile = teststring + outlist = PathPost.buildPostList(self.job) + subpart, objs = outlist[0] + filename = PathPost.resolveFileName(self.job, subpart, 0) + self.assertEqual(filename, f"{self.testfilepath}/testfile.nc") def test020(self): - # Multiple Fixtures - - self.job.Fixtures = ["G54", "G55"] - self.job.SplitOutput = False - self.job.OrderOutputBy = "Fixture" - - cpp = PathPost.CommandPathPost - self.postlist = cpp.buildPostList(self, self.job) - - self.assertTrue(len(self.postlist) == 1) - - outlist = [i.Label for i in self.postlist[0]] - expected = [ - "G54", - "T1", - "FirstOp-(T1)", - "SecondOp-(T1)", - "T2", - "ThirdOp-(T2)", - "T1", - "FourthOp-(T1)", - "T3", - "FifthOp-(T3)", - "G55", - "T1", - "FirstOp-(T1)", - "SecondOp-(T1)", - "T2", - "ThirdOp-(T2)", - "T1", - "FourthOp-(T1)", - "T3", - "FifthOp-(T3)", - ] - - self.assertListEqual(outlist, expected) + teststring = "%d.nc" + self.job.PostProcessorOutputFile = teststring + outlist = PathPost.buildPostList(self.job) + subpart, objs = outlist[0] + filename = PathPost.resolveFileName(self.job, subpart, 0) + self.assertEqual(filename, f"{self.testfilename}.nc") def test030(self): - # Multiple Fixtures - Split output - - self.job.Fixtures = ["G54", "G55"] - self.job.SplitOutput = True - self.job.OrderOutputBy = "Fixture" - - cpp = PathPost.CommandPathPost - self.postlist = cpp.buildPostList(self, self.job) - - self.assertTrue(len(self.postlist) == 2) - - outlist = [i.Label for i in self.postlist[0]] - print(outlist) - - expected = [ - "G54", - "T1", - "FirstOp-(T1)", - "SecondOp-(T1)", - "T2", - "ThirdOp-(T2)", - "T1", - "FourthOp-(T1)", - "T3", - "FifthOp-(T3)", - ] - self.assertListEqual(outlist, expected) - - expected = [ - "G55", - "T1", - "FirstOp-(T1)", - "SecondOp-(T1)", - "T2", - "ThirdOp-(T2)", - "T1", - "FourthOp-(T1)", - "T3", - "FifthOp-(T3)", - ] - outlist = [i.Label for i in self.postlist[1]] - self.assertListEqual(outlist, expected) + teststring = "%M/outfile.nc" + self.job.PostProcessorOutputFile = teststring + outlist = PathPost.buildPostList(self.job) + subpart, objs = outlist[0] + filename = PathPost.resolveFileName(self.job, subpart, 0) + self.assertEqual(filename, f"{self.macro}outfile.nc") def test040(self): - # Order by 'Tool' - - self.job.Fixtures = ["G54", "G55"] - self.job.SplitOutput = False - self.job.OrderOutputBy = "Tool" - - cpp = PathPost.CommandPathPost - self.postlist = cpp.buildPostList(self, self.job) - outlist = [i.Label for i in self.postlist[0]] - - self.assertTrue(len(self.postlist) == 1) - expected = [ - "G54", - "T1", - "FirstOp-(T1)", - "SecondOp-(T1)", - "T2", - "ThirdOp-(T2)", - "T1", - "FourthOp-(T1)", - "G55", - "FirstOp-(T1)", - "SecondOp-(T1)", - "T2", - "ThirdOp-(T2)", - "T1", - "FourthOp-(T1)", - "G54", - "T3", - "FifthOp-(T3)", - "G55", - "FifthOp-(T3)", - ] - - self.assertListEqual(outlist, expected) + # unused substitution strings should be ignored + teststring = "%d%T%t%W%O/testdoc.nc" + self.job.PostProcessorOutputFile = teststring + outlist = PathPost.buildPostList(self.job) + subpart, objs = outlist[0] + filename = PathPost.resolveFileName(self.job, subpart, 0) + self.assertEqual(filename, f"{self.testfilename}/testdoc.nc") def test050(self): - # Order by 'Tool' and split - - self.job.Fixtures = ["G54", "G55"] - self.job.SplitOutput = True - self.job.OrderOutputBy = "Tool" - - cpp = PathPost.CommandPathPost - self.postlist = cpp.buildPostList(self, self.job) - outlist = [i.Label for i in self.postlist[0]] - - expected = [ - "G54", - "T1", - "FirstOp-(T1)", - "SecondOp-(T1)", - "T2", - "ThirdOp-(T2)", - "T1", - "FourthOp-(T1)", - "G55", - "FirstOp-(T1)", - "SecondOp-(T1)", - "T2", - "ThirdOp-(T2)", - "T1", - "FourthOp-(T1)", - ] - self.assertListEqual(outlist, expected) - - outlist = [i.Label for i in self.postlist[1]] - - expected = [ - "G54", - "T3", - "FifthOp-(T3)", - "G55", - "FifthOp-(T3)", - ] - self.assertListEqual(outlist, expected) + # explicitly using the sequence number should include it where indicated. + teststring = "%S-%d.nc" + self.job.PostProcessorOutputFile = teststring + outlist = PathPost.buildPostList(self.job) + subpart, objs = outlist[0] + filename = PathPost.resolveFileName(self.job, subpart, 0) + self.assertEqual(filename, "0-test_filenaming.nc") def test060(self): - # Order by 'Operation' + # # Split by Tool + self.job.SplitOutput = True + self.job.OrderOutputBy = "Tool" + outlist = PathPost.buildPostList(self.job) - self.job.Fixtures = ["G54", "G55"] - self.job.SplitOutput = False - self.job.OrderOutputBy = "Operation" + # substitute jobname and use default sequence numbers + teststring = "%j.nc" + self.job.PostProcessorOutputFile = teststring + subpart, objs = outlist[0] + filename = PathPost.resolveFileName(self.job, subpart, 0) + self.assertEqual(filename, "MainJob-0.nc") + subpart, objs = outlist[1] + filename = PathPost.resolveFileName(self.job, subpart, 1) + self.assertEqual(filename, "MainJob-1.nc") - cpp = PathPost.CommandPathPost - self.postlist = cpp.buildPostList(self, self.job) - outlist = [i.Label for i in self.postlist[0]] + # Use Toolnumbers and default sequence numbers + teststring = "%T.nc" + self.job.PostProcessorOutputFile = teststring + outlist = PathPost.buildPostList(self.job) + subpart, objs = outlist[0] + filename = PathPost.resolveFileName(self.job, subpart, 0) + self.assertEqual(filename, "5-0.nc") + subpart, objs = outlist[1] + filename = PathPost.resolveFileName(self.job, subpart, 1) + self.assertEqual(filename, "2-1.nc") - self.assertTrue(len(self.postlist) == 1) - expected = [ - "G54", - "T1", - "FirstOp-(T1)", - "G55", - "FirstOp-(T1)", - "G54", - "T1", - "SecondOp-(T1)", - "G55", - "SecondOp-(T1)", - "G54", - "T2", - "ThirdOp-(T2)", - "G55", - "ThirdOp-(T2)", - "G54", - "T1", - "FourthOp-(T1)", - "G55", - "FourthOp-(T1)", - "G54", - "T3", - "FifthOp-(T3)", - "G55", - "FifthOp-(T3)", - ] - - self.assertListEqual(outlist, expected) + # Use Tooldescriptions and default sequence numbers + teststring = "%t.nc" + self.job.PostProcessorOutputFile = teststring + outlist = PathPost.buildPostList(self.job) + subpart, objs = outlist[0] + filename = PathPost.resolveFileName(self.job, subpart, 0) + self.assertEqual(filename, "TC__7_16__two_flute-0.nc") + subpart, objs = outlist[1] + filename = PathPost.resolveFileName(self.job, subpart, 1) + self.assertEqual(filename, "TC__Drill-1.nc") def test070(self): - # Order by 'Operation' and split + # Split by WCS + self.job.SplitOutput = True + self.job.OrderOutputBy = "Fixture" + outlist = PathPost.buildPostList(self.job) - self.job.Fixtures = ["G54", "G55"] + teststring = "%j.nc" + self.job.PostProcessorOutputFile = teststring + subpart, objs = outlist[0] + filename = PathPost.resolveFileName(self.job, subpart, 0) + self.assertEqual(filename, "MainJob-0.nc") + subpart, objs = outlist[1] + filename = PathPost.resolveFileName(self.job, subpart, 1) + self.assertEqual(filename, "MainJob-1.nc") + + teststring = "%W-%j.nc" + self.job.PostProcessorOutputFile = teststring + subpart, objs = outlist[0] + filename = PathPost.resolveFileName(self.job, subpart, 0) + self.assertEqual(filename, "G54-MainJob-0.nc") + subpart, objs = outlist[1] + filename = PathPost.resolveFileName(self.job, subpart, 1) + self.assertEqual(filename, "G55-MainJob-1.nc") + + def test080(self): + # Split by Operation self.job.SplitOutput = True self.job.OrderOutputBy = "Operation" + outlist = PathPost.buildPostList(self.job) - cpp = PathPost.CommandPathPost - self.postlist = cpp.buildPostList(self, self.job) - self.assertTrue(len(self.postlist) == 5) + teststring = "%j.nc" + self.job.PostProcessorOutputFile = teststring + subpart, objs = outlist[0] + filename = PathPost.resolveFileName(self.job, subpart, 0) + self.assertEqual(filename, "MainJob-0.nc") + subpart, objs = outlist[1] + filename = PathPost.resolveFileName(self.job, subpart, 1) + self.assertEqual(filename, "MainJob-1.nc") - outlist = [i.Label for i in self.postlist[0]] - expected = [ - "G54", - "T1", - "FirstOp-(T1)", - "G55", - "FirstOp-(T1)", - ] - self.assertListEqual(outlist, expected) - - outlist = [i.Label for i in self.postlist[1]] - expected = [ - "G54", - "T1", - "SecondOp-(T1)", - "G55", - "SecondOp-(T1)", - ] - self.assertListEqual(outlist, expected) - - outlist = [i.Label for i in self.postlist[2]] - expected = [ - "G54", - "T2", - "ThirdOp-(T2)", - "G55", - "ThirdOp-(T2)", - ] - self.assertListEqual(outlist, expected) - - outlist = [i.Label for i in self.postlist[3]] - expected = [ - "G54", - "T1", - "FourthOp-(T1)", - "G55", - "FourthOp-(T1)", - ] - self.assertListEqual(outlist, expected) - - outlist = [i.Label for i in self.postlist[4]] - expected = [ - "G54", - "T3", - "FifthOp-(T3)", - "G55", - "FifthOp-(T3)", - ] - self.assertListEqual(outlist, expected) + teststring = "%O-%j.nc" + self.job.PostProcessorOutputFile = teststring + subpart, objs = outlist[0] + filename = PathPost.resolveFileName(self.job, subpart, 0) + self.assertEqual(filename, "OutsideProfile-MainJob-0.nc") + subpart, objs = outlist[1] + filename = PathPost.resolveFileName(self.job, subpart, 1) + self.assertEqual(filename, "DrillAllHoles-MainJob-1.nc") diff --git a/src/Mod/Path/PathTests/test_filenaming.fcstd b/src/Mod/Path/PathTests/test_filenaming.fcstd new file mode 100644 index 0000000000..75bbf08f19 Binary files /dev/null and b/src/Mod/Path/PathTests/test_filenaming.fcstd differ