Improve the output splitting

Add ability to substitute tool name, op name, or fixture name in filename
Add unit tests
This commit is contained in:
sliptonic
2022-03-12 15:50:11 -06:00
parent fb1a7af360
commit 97a52a991a
3 changed files with 633 additions and 662 deletions

View File

@@ -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()

File diff suppressed because one or more lines are too long

Binary file not shown.