Major refactor on post stack

This commit is contained in:
sliptonic
2024-04-24 19:08:28 -05:00
parent d327ceb65b
commit 8022d7df40
6 changed files with 898 additions and 729 deletions

View File

@@ -27,21 +27,17 @@ Processor entries in PathJob """
import FreeCAD
import FreeCADGui
import Path
import Path.Base.Util as PathUtil
import Path.Main.Job as PathJob
from PathScripts import PathUtils
from Path.Post.Utils import FilenameGenerator
import os
import re
from Path.Post.Processor import PostProcessor
from Path.Post.Processor import PostProcessor, PostProcessorFactory
from PySide import QtCore, QtGui
from datetime import datetime
from PySide.QtCore import QT_TRANSLATE_NOOP
LOG_MODULE = Path.Log.thisModule()
debugmodule = False
if debugmodule:
DEBUG = False
if DEBUG:
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
Path.Log.trackModule(Path.Log.thisModule())
else:
@@ -51,372 +47,29 @@ else:
translate = FreeCAD.Qt.translate
class _TempObject:
Path = None
Name = "Fixture"
InList = []
Label = "Fixture"
def processFileNameSubstitutions(
job,
subpartname,
sequencenumber,
outputpath,
filename,
ext,
):
"""Process any substitutions in the outputpath or filename."""
# The following section allows substitution within the path part
Path.Log.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)
Path.Log.track(f"path after substitution: {outputpath}")
# The following section allows substitution within the filename part
Path.Log.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 sequence 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:
Path.Log.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}"
Path.Log.track(f"filename after substitution: {filename}")
if not ext:
ext = ".nc"
Path.Log.track(f"file extension: {ext}")
fullPath = f"{outputpath}{os.path.sep}{filename}{ext}"
Path.Log.track(f"full filepath: {fullPath}")
return fullPath
def resolveFileName(job, subpartname, sequencenumber):
"""Generate the file name to use as output."""
Path.Log.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(Path.Preferences.defaultOutputFile())
filename, ext = os.path.splitext(filename)
# Override with document default if it exists
if job.PostProcessorOutputFile:
candidateOutputPath, candidateFilename = os.path.split(
job.PostProcessorOutputFile
)
if candidateOutputPath:
outputpath = candidateOutputPath
if candidateFilename:
filename, ext = os.path.splitext(candidateFilename)
# Strip any invalid substitutions from the outputpath
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
Path.Log.track(f"path: {outputpath} name: {filename} ext: {ext}")
fullPath = processFileNameSubstitutions(
job,
subpartname,
sequencenumber,
outputpath,
filename,
ext,
)
# This section determines whether user interaction is necessary
policy = Path.Preferences.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(f"{fn}{n:03d}{ext}"):
n = n + 1
fullPath = f"{fn}{n:03d}{ext}"
if openDialog:
requestedfile = QtGui.QFileDialog.getSaveFileName(
QtGui.QApplication.activeWindow(), "Output File", fullPath
)
if requestedfile[0]:
fullPath = requestedfile[0]
else:
fullPath = None
if fullPath:
# remove any unused substitution strings:
for s in validPathSubstitutions + validFilenameSubstitutions:
fullPath = fullPath.replace(f"%{s}", "")
fullPath = os.path.normpath(fullPath)
Path.Log.track(fullPath)
return fullPath
def fixtureSetup(order, fixture, job):
"""Convert a Fixure setting to _TempObject instance with a G0 move to a
safe height every time the fixture coordinate system change. Skip
the move for first fixture, to avoid moving before tool and tool
height compensation is enabled.
"""
fobj = _TempObject()
c1 = Path.Command(fixture)
fobj.Path = Path.Path([c1])
# Avoid any tool move after G49 in preamble and before tool change
# and G43 in case tool height compensation is in use, to avoid
# dangerous move without tool compesation.
if order != 0:
c2 = Path.Command(
"G0 Z"
+ str(
job.Stock.Shape.BoundBox.ZMax
+ job.SetupSheet.ClearanceHeightOffset.Value
)
)
fobj.Path.addCommands(c2)
fobj.InList.append(job)
return fobj
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":
Path.Log.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
sublist = [fixtureSetup(index, f, job)]
# 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)
Path.Log.debug(f"Appending TC: {tc.Name}")
currTool = tc.ToolNumber
sublist.append(obj)
postlist.append((f, sublist))
elif orderby == "Tool":
Path.Log.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 index, f in enumerate(wcslist):
# create an object to serve as the fixture path
fixturelist.append(fixtureSetup(index, f, job))
# Now generate the gcode
curlist = [] # list of ops for tool, will repeat for each fixture
sublist = [] # list of ops for output splitting
Path.Log.track(job.PostProcessorOutputFile)
for idx, obj in enumerate(job.Operations.Group):
Path.Log.track(obj.Label)
# check if the operation is active
if not getattr(obj, "Active", True):
Path.Log.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)
Path.Log.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":
Path.Log.debug("Ordering by Operation")
# Order by operation means ops are done in each fixture in
# sequence.
currTool = None
# Now generate the gcode
for obj in job.Operations.Group:
# check if the operation is active
if not getattr(obj, "Active", True):
continue
sublist = []
Path.Log.debug(f"obj: {obj.Name}")
for index, f in enumerate(wcslist):
sublist.append(fixtureSetup(index, f, job))
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:
Path.Log.track()
return postlist
Path.Log.track()
finalpostlist = [
("allitems", [item for slist in postlist for item in slist[1]])
]
return finalpostlist
def _resolve_post_processor_name(job):
Path.Log.debug("_resolve_post_processor_name()")
if job.PostProcessor:
valid_name = job.PostProcessor
elif Path.Preferences.defaultPostProcessor():
valid_name = Path.Preferences.defaultPostProcessor()
elif FreeCAD.GuiUp:
valid_name = (
DlgSelectPostProcessor().exec_()
) # Ensure DlgSelectPostProcessor is defined
else:
valid_name = None
if valid_name and PostProcessor.exists(valid_name):
return valid_name
else:
raise ValueError(f"Post processor not identified.")
class DlgSelectPostProcessor:
"""Provide user with list of available and active post processor
choices."""
def __init__(self):
self.dialog = FreeCADGui.PySideUic.loadUi(":/panels/DlgSelectPostProcessor.ui")
firstItem = None
@@ -454,18 +107,6 @@ class DlgSelectPostProcessor:
class CommandPathPost:
subpart = 1
def resolvePostProcessor(self, job):
if hasattr(job, "PostProcessor"):
post = Path.Preferences.defaultPostProcessor()
if job.PostProcessor:
post = job.PostProcessor
if post and PostProcessor.exists(post):
return post
dlg = DlgSelectPostProcessor()
return dlg.exec_()
def GetResources(self):
return {
"Pixmap": "CAM_Post",
@@ -475,144 +116,97 @@ class CommandPathPost:
}
def IsActive(self):
if FreeCAD.ActiveDocument is not None:
if FreeCADGui.Selection.getCompleteSelection():
for o in FreeCAD.ActiveDocument.Objects:
if o.Name[:3] == "Job":
return True
selected = FreeCADGui.Selection.getSelectionEx()
if len(selected) != 1:
return False
return False
selected_object = selected[0].Object
self.candidate = PathUtils.findParentJob(selected_object)
def exportObjectsWith(self, objs, partname, job, sequence, extraargs=None):
Path.Log.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
Path.Log.track(partname, sequence)
Path.Log.track(objs)
return self.candidate is not None
# partname = objs[0]
# slist = objs[1]
Path.Log.track(objs, partname)
def _write_file(self, filename, gcode, policy):
if policy == "Open File Dialog":
dlg = QtGui.QFileDialog()
dlg.setFileMode(QtGui.QFileDialog.FileMode.AnyFile)
dlg.setAcceptMode(QtGui.QFileDialog.AcceptMode.AcceptSave)
dlg.setDirectory(os.path.dirname(filename))
dlg.selectFile(os.path.basename(filename))
if dlg.exec_():
filename = dlg.selectedFiles()[0]
Path.Log.debug(filename)
with open(filename, "w") as f:
f.write(gcode)
postArgs = Path.Preferences.defaultPostProcessorArgs()
if hasattr(job, "PostProcessorArgs") and job.PostProcessorArgs:
postArgs = job.PostProcessorArgs
elif hasattr(job, "PostProcessor") and job.PostProcessor:
postArgs = ""
elif policy == "Append Unique ID on conflict":
while os.path.isfile(filename):
base, ext = os.path.splitext(filename)
filename = f"{base}-1{ext}"
with open(filename, "w") as f:
f.write(gcode)
if extraargs is not None:
postArgs += f" {extraargs}"
Path.Log.track(postArgs)
postname = self.resolvePostProcessor(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)
return (False, gcode, filename)
else:
return (True, "", filename)
elif policy == "Open File Dialog on conflict":
if os.path.isfile(filename):
dlg = QtGui.QFileDialog()
dlg.setFileMode(QtGui.QFileDialog.FileMode.AnyFile)
dlg.setAcceptMode(QtGui.QFileDialog.AcceptSave)
dlg.setDirectory(os.path.dirname(filename))
dlg.selectFile(os.path.basename(filename))
if dlg.exec_():
filename = dlg.selectedFiles()[0]
Path.Log.debug(filename)
with open(filename, "w") as f:
f.write(gcode)
else:
with open(filename, "w") as f:
f.write(gcode)
else: # Overwrite
with open(filename, "w") as f:
f.write(gcode)
FreeCAD.Console.PrintMessage(f"File written to {filename}\n")
def Activated(self):
Path.Log.track()
FreeCAD.ActiveDocument.openTransaction("Post Process the Selected path(s)")
FreeCADGui.addModule("Path.Post.Command")
"""
Handles the activation of post processing, initiating the process based
on user selection and document context.
"""
Path.Log.debug(self.candidate.Name)
FreeCAD.ActiveDocument.openTransaction("Post Process the Selected Job")
# Attempt to figure out what the user wants to post-process
# If a job is selected, post that.
# If there's only one job in a document, post it.
# If a user has selected a subobject of a job, post the job.
# If multiple jobs and can't guess, ask them.
postprocessor_name = _resolve_post_processor_name(self.candidate)
Path.Log.debug(f"Post Processor: {postprocessor_name}")
selected = FreeCADGui.Selection.getSelectionEx()
if len(selected) > 1:
FreeCAD.Console.PrintError(
"Please select a single job or other path object\n"
)
return
elif len(selected) == 1:
sel = selected[0].Object
if sel.Name[:3] == "Job":
job = sel
elif hasattr(sel, "Path"):
try:
job = PathUtils.findParentJob(sel)
except Exception:
job = None
else:
job = None
if job is None:
targetlist = []
for o in FreeCAD.ActiveDocument.Objects:
if hasattr(o, "Proxy"):
if isinstance(o.Proxy, PathJob.ObjectJob):
targetlist.append(o.Label)
Path.Log.debug(f"Possible post objects: {targetlist}")
if len(targetlist) > 1:
jobname, result = QtGui.QInputDialog.getItem(
None, translate("Path", "Choose a Path Job"), None, targetlist
)
if result is False:
return
else:
jobname = targetlist[0]
job = FreeCAD.ActiveDocument.getObject(jobname)
Path.Log.debug(f"about to postprocess job: {job.Name}")
postlist = buildPostList(job)
# filename = resolveFileName(job, "allitems", 0)
filenames = []
success = True
finalgcode = ""
for idx, section in enumerate(postlist):
partname = section[0]
sublist = section[1]
result, gcode, name = self.exportObjectsWith(sublist, partname, job, idx)
filenames.append(name)
Path.Log.track(result, gcode, name)
if name is None:
success = False
else:
finalgcode += gcode
# if job.SplitOutput:
# for idx, sublist in enumerate(postlist): # name, slist in postlist:
# result = self.exportObjectsWith(sublist[1], sublist[0], job, idx)
# if result is None:
# success = False
# else:
# gcode += result
# else:
# finalpostlist = [item for (_, slist) in postlist for item in slist]
# gcode = self.exportObjectsWith(finalpostlist, "allitems", job, 1)
# success = gcode is not None
Path.Log.track(success)
if success:
if hasattr(job, "LastPostProcessDate"):
job.LastPostProcessDate = str(datetime.now())
if hasattr(job, "LastPostProcessOutput"):
job.LastPostProcessOutput = " \n".join(filenames)
Path.Log.track(job.LastPostProcessOutput)
FreeCAD.ActiveDocument.commitTransaction()
else:
if not postprocessor_name:
FreeCAD.ActiveDocument.abortTransaction()
return
# get a postprocessor
postprocessor = PostProcessorFactory.get_post_processor(
self.candidate, postprocessor_name
)
post_data = postprocessor.export()
if not post_data:
FreeCAD.ActiveDocument.abortTransaction()
return
policy = Path.Preferences.defaultOutputPolicy()
generator = FilenameGenerator(job=self.candidate)
generated_filename = generator.generate_filenames()
for item in post_data:
subpart, gcode = item
# get a name for the file
subpart = "" if subpart == "allitems" else subpart
Path.Log.debug(subpart)
generator.set_subpartname(subpart)
fname = next(generated_filename)
# write the results to the file
self._write_file(fname, gcode, policy)
FreeCAD.ActiveDocument.commitTransaction()
FreeCAD.ActiveDocument.recompute()

View File

@@ -19,62 +19,324 @@
# * USA *
# * *
# ***************************************************************************
import Path
import sys
"""
The base classes for post processors in CAM workbench.
"""
from PySide import QtCore, QtGui
from importlib import reload
import FreeCAD
import Path
import Path.Base.Util as PathUtil
import importlib.util
import os
import sys
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
debug = True
if debug:
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
Path.Log.trackModule(Path.Log.thisModule())
else:
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
class _TempObject:
Path = None
Name = "Fixture"
InList = []
Label = "Fixture"
class PostProcessorFactory:
"""Factory class for creating post processors."""
@staticmethod
def get_post_processor(job, postname):
# Log initial debug message
Path.Log.debug("PostProcessorFactory.get_post_processor()")
# Posts have to be in a place we can find them
syspath = sys.path
paths = Path.Preferences.searchPathsPost()
paths.extend(sys.path)
module_name = f"{postname}_post"
class_name = postname.title()
# Iterate all the paths to find the module
for path in paths:
module_path = os.path.join(path, f"{module_name}.py")
spec = importlib.util.spec_from_file_location(module_name, module_path)
if spec and spec.loader:
module = importlib.util.module_from_spec(spec)
try:
spec.loader.exec_module(module)
Path.Log.debug(f"found module {module_name} at {module_path}")
except (FileNotFoundError, ImportError, ModuleNotFoundError):
continue
try:
PostClass = getattr(module, class_name)
return PostClass(job)
except AttributeError:
# Return an instance of WrapperPost if no valid module is found
Path.Log.debug(f"Post processor {postname} is a script")
return WrapperPost(job, module_path)
class PostProcessor:
"""Base Class. All postprocessors should inherit from this class."""
def __init__(self, job, tooltip, tooltipargs, units, *args, **kwargs):
self._tooltip = tooltip
self._tooltipargs = tooltipargs
self._units = units
self._job = job
self._args = args
self._kwargs = kwargs
@classmethod
def exists(cls, processor):
return processor in Path.Preferences.allAvailablePostProcessors()
@classmethod
def load(cls, processor):
Path.Log.track(processor)
syspath = sys.path
paths = Path.Preferences.searchPathsPost()
paths.extend(sys.path)
sys.path = paths
def export(self):
raise NotImplementedError("Subclass must implement abstract method")
postname = processor + "_post"
namespace = {}
@property
def tooltip(self):
"""Get the tooltip text for the post processor."""
return self._tooltip
# can't modify function local scope with exec in python3
exec("import %s as current_post" % postname, namespace)
current_post = namespace["current_post"]
@property
def tooltipargs(self):
"""Get the tooltip arguments for the post processor."""
return self._tooltipargs
# make sure the script is reloaded if it was previously loaded
# should the script have been imported for the first time above
# then the initialization code of the script gets executed twice
# resulting in 2 load messages if the script outputs one of those.
@property
def units(self):
"""Get the units used by the post processor."""
return self._units
exec("reload(%s)" % "current_post")
def _buildPostList(self):
"""
determines the specific objects and order to
postprocess Returns a list of objects which can be passed to
exportObjectsWith() for final posting."""
sys.path = syspath
def __fixtureSetup(order, fixture, job):
"""Convert a Fixure setting to _TempObject instance with a G0 move to a
safe height every time the fixture coordinate system change. Skip
the move for first fixture, to avoid moving before tool and tool
height compensation is enabled.
instance = PostProcessor(current_post)
if hasattr(current_post, "UNITS"):
if current_post.UNITS == "G21":
instance.units = "Metric"
else:
instance.units = "Inch"
"""
if hasattr(current_post, "TOOLTIP"):
instance.tooltip = current_post.TOOLTIP
if hasattr(current_post, "TOOLTIP_ARGS"):
instance.tooltipArgs = current_post.TOOLTIP_ARGS
return instance
fobj = _TempObject()
c1 = Path.Command(fixture)
fobj.Path = Path.Path([c1])
# Avoid any tool move after G49 in preamble and before tool change
# and G43 in case tool height compensation is in use, to avoid
# dangerous move without toolgg compesation.
if order != 0:
c2 = Path.Command(
"G0 Z"
+ str(
job.Stock.Shape.BoundBox.ZMax
+ job.SetupSheet.ClearanceHeightOffset.Value
)
)
fobj.Path.addCommands(c2)
fobj.InList.append(job)
return fobj
def __init__(self, script):
self.script = script
self.tooltip = None
self.tooltipArgs = None
self.units = None
self.machineName = None
wcslist = self._job.Fixtures
orderby = self._job.OrderOutputBy
Path.Log.debug(f"Ordering by {orderby}")
def export(self, obj, filename, args):
return self.script.export(obj, filename, args)
postlist = []
if orderby == "Fixture":
Path.Log.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
sublist = [__fixtureSetup(index, f, self._job)]
# Now generate the gcode
for obj in self._job.Operations.Group:
tc = PathUtil.toolControllerForOp(obj)
if tc is not None and PathUtil.opProperty(obj, "Active"):
if tc.ToolNumber != currTool:
sublist.append(tc)
Path.Log.debug(f"Appending TC: {tc.Name}")
currTool = tc.ToolNumber
sublist.append(obj)
postlist.append((f, sublist))
elif orderby == "Tool":
Path.Log.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 index, f in enumerate(wcslist):
# create an object to serve as the fixture path
fixturelist.append(__fixtureSetup(index, f, self._job))
# Now generate the gcode
curlist = [] # list of ops for tool, will repeat for each fixture
sublist = [] # list of ops for output splitting
Path.Log.track(self._job.PostProcessorOutputFile)
for idx, obj in enumerate(self._job.Operations.Group):
Path.Log.track(obj.Label)
# check if the operation is active
if not getattr(obj, "Active", True):
Path.Log.track()
continue
# Determine the proper string for the Op's TC
tc = PathUtil.toolControllerForOp(obj)
if tc is None:
tcstring = "None"
elif "%T" in self._job.PostProcessorOutputFile:
tcstring = f"{tc.ToolNumber}"
else:
tcstring = re.sub(r"[^\w\d-]", "_", tc.Label)
Path.Log.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(self._job.Operations.Group) - 1: # Last operation.
for fixture in fixturelist:
sublist.append(fixture)
sublist.extend(curlist)
postlist.append((toolstring, sublist))
elif orderby == "Operation":
Path.Log.debug("Ordering by Operation")
# Order by operation means ops are done in each fixture in
# sequence.
currTool = None
# Now generate the gcode
for obj in self._job.Operations.Group:
# check if the operation is active
if not getattr(obj, "Active", True):
continue
sublist = []
Path.Log.debug(f"obj: {obj.Name}")
for index, f in enumerate(wcslist):
sublist.append(_fixtureSetup(index, f, self._job))
tc = PathUtil.toolControllerForOp(obj)
if tc is not None:
if self._job.SplitOutput or (tc.ToolNumber != currTool):
sublist.append(tc)
currTool = tc.ToolNumber
sublist.append(obj)
postlist.append((obj.Label, sublist))
Path.Log.debug(f"Postlist: {postlist}")
if self._job.SplitOutput:
Path.Log.track()
return postlist
Path.Log.track()
finalpostlist = [
("allitems", [item for slist in postlist for item in slist[1]])
]
Path.Log.debug(f"Postlist: {postlist}")
return finalpostlist
class WrapperPost(PostProcessor):
"""Wrapper class for old post processors that are scripts."""
def __init__(self, job, script_path, *args, **kwargs):
super().__init__(
job, tooltip=None, tooltipargs=None, units=None, *args, **kwargs
)
self.script_path = script_path
Path.Log.debug(f"WrapperPost.__init__({script_path})")
self.load_script()
def load_script(self):
# Dynamically load the script as a module
try:
spec = importlib.util.spec_from_file_location(
"script_module", self.script_path
)
self.script_module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(self.script_module)
except Exception as e:
raise ImportError(f"Failed to load script: {e}")
if not hasattr(self.script_module, "export"):
raise AttributeError("The script does not have an 'export' function.")
# Set properties based on attributes of the module
self._units = (
"Metric" if getattr(self.script_module, "UNITS", "G21") == "G21" else "Inch"
)
self._tooltip = getattr(self.script_module, "TOOLTIP", "No tooltip provided")
self._tooltipargs = getattr(self.script_module, "TOOLTIP_ARGS", [])
def export(self):
# Dynamically reload the module for the export to ensure up-to-date usage
postables = self._buildPostList()
Path.Log.debug(f"postables count: {len(postables)}")
g_code_sections = []
for idx, section in enumerate(postables):
partname, sublist = section
gcode = self.script_module.export(sublist, "-", self._job.PostProcessorArgs)
Path.Log.debug(f"Exported {partname}")
g_code_sections.append((partname, gcode))
return g_code_sections
@property
def tooltip(self):
return self._tooltip
@property
def tooltipargs(self):
return self._tooltipargs
@property
def units(self):
return self._units

View File

@@ -27,21 +27,155 @@
These are common functions and classes for creating custom post processors.
"""
from PySide import QtCore, QtGui
import FreeCAD
import Path
import Part
from Path.Base.MachineState import MachineState
from PySide import QtCore, QtGui
import FreeCAD
import Part
import Path
import os
import re
debug = False
if debug:
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
Path.Log.trackModule(Path.Log.thisModule())
else:
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
translate = FreeCAD.Qt.translate
FreeCADGui = None
if FreeCAD.GuiUp:
import FreeCADGui
class FilenameGenerator:
def __init__(self, job):
self.job = job
self.subpartname = ""
self.sequencenumber = 0
path, filename, ext = self.get_path_and_filename_default()
self.qualified_path = self._apply_path_substitutions(path)
self.qualified_filename = self._apply_filename_substitutions(filename)
self.extension = ext
def get_path_and_filename_default(self):
outputpath = ""
filename = ""
ext = ".nc"
validPathSubstitutions = ["D", "d", "M", "j"]
validFilenameSubstitutions = ["j", "d", "T", "t", "W", "O", "S"]
if self.job.PostProcessorOutputFile:
candidateOutputPath, candidateFilename = os.path.split(
self.job.PostProcessorOutputFile
)
if candidateOutputPath:
outputpath = candidateOutputPath
if candidateFilename:
filename, ext = os.path.splitext(candidateFilename)
else:
outputpath, filename = os.path.split(Path.Preferences.defaultOutputFile())
filename, ext = os.path.splitext(filename)
# Make sure we have something to work with
if not filename:
filename = FreeCAD.ActiveDocument.Label
if not outputpath:
outputpath = "."
if not ext:
ext = ".nc"
# Check for invalid matches
for match in re.findall("%(.)", outputpath):
Path.Log.debug(f"match: {match}")
if match not in validPathSubstitutions:
outputpath = outputpath.replace(f"%{match}", "")
FreeCAD.Console.PrintWarning(
"Invalid substitution strings will be ignored in output path: %s\n"
% match
)
for match in re.findall("%(.)", filename):
Path.Log.debug(f"match: {match}")
if match not in validFilenameSubstitutions:
filename = filename.replace(f"%{match}", "")
FreeCAD.Console.PrintWarning(
"Invalid substitution strings will be ignored in file path: %s\n"
% match
)
Path.Log.debug(f"outputpath: {outputpath} filename: {filename} ext: {ext}")
return outputpath, filename, ext
def set_subpartname(self, subpartname):
self.subpartname = subpartname
def _apply_path_substitutions(self, file_path):
"""Apply substitutions based on job settings and other parameters."""
substitutions = {
"%D": os.path.dirname(self.job.Document.FileName or "."),
"%d": self.job.Document.Label,
"%j": self.job.Label,
"%M": os.path.dirname(FreeCAD.getUserMacroDir()),
}
for key, value in substitutions.items():
file_path = file_path.replace(key, value)
Path.Log.debug(f"file_path: {file_path}")
return file_path
def _apply_filename_substitutions(self, file_name):
Path.Log.debug(f"file_name: {file_name}")
"""Apply substitutions based on job settings and other parameters."""
substitutions = {
"%d": self.job.Document.Label,
"%j": self.job.Label,
"%T": self.subpartname, # Tool
"%t": self.subpartname, # Tool
"%W": self.subpartname, # Fixture
"%O": self.subpartname, # Operation
}
for key, value in substitutions.items():
file_name = file_name.replace(key, value)
Path.Log.debug(f"file_name: {file_name}")
return file_name
def generate_filenames(self):
"""Yield filenames indefinitely with proper substitutions."""
while True:
temp_filename = self.qualified_filename
Path.Log.debug(f"temp_filename: {temp_filename}")
explicit_sequence = False
matches = re.findall("%S", temp_filename)
if matches:
Path.Log.debug(f"matches: {matches}")
temp_filename = re.sub("%S", str(self.sequencenumber), temp_filename)
explicit_sequence = True
subpart = f"-{self.subpartname}" if self.subpartname else ""
sequence = (
f"-{self.sequencenumber}"
if not explicit_sequence and self.sequencenumber
else ""
)
filename = f"{temp_filename}{subpart}{sequence}{self.extension}"
full_path = os.path.join(self.qualified_path, filename)
self.sequencenumber += 1
Path.Log.debug(f"yielding filename: {full_path}")
yield os.path.normpath(full_path)
class GCodeHighlighter(QtGui.QSyntaxHighlighter):
def __init__(self, parent=None):
super(GCodeHighlighter, self).__init__(parent)

View File

@@ -27,7 +27,7 @@ import PathScripts.PathUtils as PathUtils
from builtins import open as pyopen
TOOLTIP = """
Dumper is an extremely simple postprocessor file for the Path workbench. It is used
Dumper is an extremely simple postprocessor file for the CAM workbench. It is used
to dump the command list from one or more Path objects for simple inspection. This post
doesn't do any manipulation of the path and doesn't write anything to disk. It just
shows the dialog so you can see it. Useful for debugging, but not much else.

View File

@@ -43,10 +43,13 @@ from Tests.TestPathHelixGenerator import TestPathHelixGenerator
from Tests.TestPathLog import TestPathLog
from Tests.TestPathOpUtil import TestPathOpUtil
# from Tests.TestPathPost import TestPathPost
#from Tests.TestPathPost import TestPathPost
from Tests.TestPathPost import TestPathPostUtils
from Tests.TestPathPost import TestBuildPostList
from Tests.TestPathPost import TestOutputNameSubstitution
from Tests.TestPathPost import TestPostProcessorFactory
from Tests.TestPathPost import TestResolvingPostProcessorName
from Tests.TestPathPost import TestFileNameGenerator
from Tests.TestPathPreferences import TestPathPreferences
from Tests.TestPathProfile import TestPathProfile
@@ -81,6 +84,7 @@ False if TestApp.__name__ else True
False if TestBuildPostList.__name__ else True
False if TestDressupDogbone.__name__ else True
False if TestDressupDogboneII.__name__ else True
False if TestFileNameGenerator.__name__ else True
False if TestGeneratorDogboneII.__name__ else True
False if TestHoldingTags.__name__ else True
False if TestPathLanguage.__name__ else True
@@ -94,7 +98,9 @@ False if TestPathHelpers.__name__ else True
# False if TestPathHelix.__name__ else True
False if TestPathLog.__name__ else True
False if TestPathOpUtil.__name__ else True
# False if TestPathPost.__name__ else True
#False if TestPathPost.__name__ else True
False if TestPostProcessorFactory.__name__ else True
False if TestResolvingPostProcessorName.__name__ else True
False if TestPathPostUtils.__name__ else True
False if TestPathPreferences.__name__ else True
False if TestPathProfile.__name__ else True

View File

@@ -24,189 +24,362 @@
import difflib
import os
import unittest
from unittest.mock import patch, MagicMock
import FreeCAD
import Path
import Path.Post.Command as PathPost
import Path.Post.Command as PathCommand
import Path.Post.Utils as PostUtils
from Path.Post.Processor import PostProcessor
from Path.Post.Processor import PostProcessor, PostProcessorFactory
from Path.Post.Command import processFileNameSubstitutions, DlgSelectPostProcessor
# If KEEP_DEBUG_OUTPUT is False, remove the gcode file after the test succeeds.
# If KEEP_DEBUG_OUTPUT is True or the test fails leave the gcode file behind
# so it can be looked at easily.
KEEP_DEBUG_OUTPUT = False
PathPost.LOG_MODULE = Path.Log.thisModule()
Path.Log.setLevel(Path.Log.Level.INFO, PathPost.LOG_MODULE)
PathCommand.LOG_MODULE = Path.Log.thisModule()
Path.Log.setLevel(Path.Log.Level.DEBUG, PathCommand.LOG_MODULE)
class TestPathPost(unittest.TestCase):
"""Test some of the output of the postprocessors.
So far there are three tests each for the linuxcnc
and centroid postprocessors.
"""
class TestFileNameGenerator(unittest.TestCase):
def setUp(self):
"""Set up the postprocessor tests."""
pass
self.doc = FreeCAD.open(FreeCAD.getHomePath() + "/Mod/CAM/Tests/boxtest.fcstd")
self.job = self.doc.getObject("Job")
def tearDown(self):
"""Tear down after the postprocessor tests."""
pass
FreeCAD.closeDocument("boxtest")
#
# You can run just this test using:
# ./FreeCAD -c -t Tests.TestPathPost.TestPathPost.test_postprocessors
#
def test_postprocessors(self):
"""Test the postprocessors."""
#
# The tests are performed in the order they are listed:
# one test performed on all of the postprocessors
# then the next test on all of the postprocessors, etc.
# You can comment out the tuples for tests that you don't want
# to use.
#
tests_to_perform = (
# (output_file_id, freecad_document, job_name, postprocessor_arguments,
# postprocessor_list)
#
# test with all of the defaults (metric mode, etc.)
("default", "boxtest1", "Job", "--no-show-editor", ()),
# test in Imperial mode
("imperial", "boxtest1", "Job", "--no-show-editor --inches", ()),
# test in metric, G55, M4, the other way around the part
("other_way", "boxtest1", "Job001", "--no-show-editor", ()),
# test in metric, split by fixtures, G54, G55, G56
("split", "boxtest1", "Job002", "--no-show-editor", ()),
# test in metric mode without the header
("no_header", "boxtest1", "Job", "--no-header --no-show-editor", ()),
# test translating G81, G82, and G83 to G00 and G01 commands
(
"drill_translate",
"drill_test1",
"Job",
"--no-show-editor --translate_drill",
("grbl", "refactored_grbl"),
),
)
#
# The postprocessors to test.
# You can comment out any postprocessors that you don't want
# to test.
#
postprocessors_to_test = (
"centroid",
# "fanuc",
"grbl",
"linuxcnc",
"mach3_mach4",
"refactored_centroid",
# "refactored_fanuc",
"refactored_grbl",
"refactored_linuxcnc",
"refactored_mach3_mach4",
"refactored_test",
)
#
# Enough of the path to where the tests are stored so that
# they can be found by the python interpreter.
#
PATHTESTS_LOCATION = "Mod/CAM/Tests"
#
# The following code tries to re-use an open FreeCAD document
# as much as possible. It compares the current document with
# the document for the next test. If the names are different
# then the current document is closed and the new document is
# opened. The final document is closed at the end of the code.
#
current_document = ""
for (
output_file_id,
freecad_document,
job_name,
postprocessor_arguments,
postprocessor_list,
) in tests_to_perform:
if current_document != freecad_document:
if current_document != "":
FreeCAD.closeDocument(current_document)
current_document = freecad_document
current_document_path = (
FreeCAD.getHomePath()
+ PATHTESTS_LOCATION
+ os.path.sep
+ current_document
+ ".fcstd"
)
FreeCAD.open(current_document_path)
job = FreeCAD.ActiveDocument.getObject(job_name)
# Create the objects to be written by the postprocessor.
postlist = PathPost.buildPostList(job)
for postprocessor_id in postprocessors_to_test:
if postprocessor_list == () or postprocessor_id in postprocessor_list:
print(
"\nRunning %s test on %s postprocessor:\n"
% (output_file_id, postprocessor_id)
)
processor = PostProcessor.load(postprocessor_id)
output_file_path = FreeCAD.getHomePath() + PATHTESTS_LOCATION
output_file_pattern = "test_%s_%s" % (
postprocessor_id,
output_file_id,
)
output_file_extension = ".ngc"
for idx, section in enumerate(postlist):
partname = section[0]
sublist = section[1]
output_filename = PathPost.processFileNameSubstitutions(
job,
partname,
idx,
output_file_path,
output_file_pattern,
output_file_extension,
)
# print("output file: " + output_filename)
file_path, extension = os.path.splitext(output_filename)
reference_file_name = "%s%s%s" % (file_path, "_ref", extension)
# print("reference file: " + reference_file_name)
gcode = processor.export(
sublist, output_filename, postprocessor_arguments
)
if not gcode:
print("no gcode")
with open(reference_file_name, "r") as fp:
reference_gcode = fp.read()
if not reference_gcode:
print("no reference gcode")
# Remove the "Output Time:" line in the header from the
# comparison if it is present because it changes with
# every test.
gcode_lines = [
i for i in gcode.splitlines(True) if "Output Time:" not in i
]
reference_gcode_lines = [
i
for i in reference_gcode.splitlines(True)
if "Output Time:" not in i
]
if gcode_lines != reference_gcode_lines:
msg = "".join(
difflib.ndiff(gcode_lines, reference_gcode_lines)
)
self.fail(
os.path.basename(output_filename)
+ " output doesn't match:\n"
+ msg
)
if not KEEP_DEBUG_OUTPUT:
os.remove(output_filename)
if current_document != "":
FreeCAD.closeDocument(current_document)
# def test010(self):
# # Assuming PostUtils.FilenameGenerator has been imported correctly
# generator = PostUtils.FilenameGenerator(job=self.job, base_output_path="output",
# filename_template="file", extension=".nc")
# filename_generator = generator.generate_filenames()
# for i in range(5):
# generated_filename = next(filename_generator)
# expected_filename = f"output/file-{i}.nc"
# self.assertEqual(generated_filename, expected_filename)
def test010(self):
generator = PostUtils.FilenameGenerator(job=self.job, base_output_path="output",
filename_template="file", extension=".nc")
filename_generator = generator.generate_filenames()
expected_filenames = ["output/file.nc"] + [f"output/file-{i}.nc" for i in range(1, 5)]
for expected_filename in expected_filenames:
generated_filename = next(filename_generator)
self.assertEqual(generated_filename, expected_filename)
class TestResolvingPostProcessorName(unittest.TestCase):
def setUp(self):
self.doc = FreeCAD.open(FreeCAD.getHomePath() + "/Mod/CAM/Tests/boxtest.fcstd")
self.job = self.doc.getObject("Job")
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/CAM")
pref.SetString("PostProcessorDefault", "")
def tearDown(self):
FreeCAD.closeDocument("boxtest")
def test010(self):
# Test if post is defined in job
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):
# Test if post is invalid
with patch('Path.Post.Processor.PostProcessor.exists', return_value=False):
with self.assertRaises(ValueError):
PathCommand._resolve_post_processor_name(self.job)
def test030(self):
# Test if post is defined in prefs
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/CAM")
pref.SetString("PostProcessorDefault", "grbl")
self.job.PostProcessor = ""
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):
# Test if user interaction is correctly handled
self.job.PostProcessor = ""
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 self.assertRaises(ValueError):
PathCommand._resolve_post_processor_name(self.job)
class TestPostProcessorFactory(unittest.TestCase):
"""Test creation of postprocessor objects."""
def setUp(self):
self.doc = FreeCAD.open(FreeCAD.getHomePath() + "/Mod/CAM/Tests/boxtest.fcstd")
self.job = self.doc.getObject("Job")
def tearDown(self):
FreeCAD.closeDocument("boxtest")
def test020(self):
# test creation of postprocessor object
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):
# test wrapping of old school postprocessor scripts
post = PostProcessorFactory.get_post_processor(self.job, "linuxcnc")
self.assertTrue(post is not None)
self.assertTrue(hasattr(post, "_buildPostList"))
def test100(self):
"""Test the processFileNameSubstitutions function."""
document_dir = os.path.dirname(FreeCAD.ActiveDocument.FileName)
Path.Log.debug(f"document_dir: {document_dir}")
user_macro_dir = os.path.dirname(FreeCAD.getUserMacroDir())
Path.Log.debug(f"user_macro_dir: {user_macro_dir}")
outputpath = "%D/output"
filename = "outputfile-%j-%d"
ext = ".txt"
expected_path = f"{document_dir}{os.path.sep}output/outputfile-{self.job.Label}-{self.doc.Label}{ext}"
# Call the function
result = processFileNameSubstitutions(self.job, "Subpart", 1, outputpath, filename, ext)
Path.Log.debug(f"result: {result}")
self.assertEqual(result, expected_path)
# test macro path substitution
outputpath = "%M/output"
filename = "file-%j"
ext = ".txt"
result = processFileNameSubstitutions(self.job, "Subpart", 1, outputpath, filename, ext)
expected_path = f"{user_macro_dir}/output/file-{self.job.Label}.txt"
self.assertEqual(result, expected_path)
# test job name substitution
document_label = FreeCAD.ActiveDocument.Label
outputpath = "output/%d"
filename = "file-%d-%j"
ext = ".txt"
result = processFileNameSubstitutions(self.job, "Subpart", 1, outputpath, filename, ext)
expected_path = f"output/{document_label}/file-{document_label}-{self.job.Label}.txt"
self.assertEqual(result, expected_path)
# test sequence number substitution
outputpath = "output"
filename = "file-%S"
ext = ".txt"
result = processFileNameSubstitutions(self.job, "Subpart", 42, outputpath, filename, ext)
expected_path = f"output/file-42.txt"
self.assertEqual(result, expected_path)
# test tool number substitution
outputpath = "output"
filename = "file"
ext = ""
result = processFileNameSubstitutions(self.job, "Subpart", 1, outputpath, filename, ext)
expected_path = f"output/file.nc" # Expect default .nc extension
self.assertEqual(result, expected_path)
class TestPostProcessorClass(unittest.TestCase):
"""Test new post structure objects."""
def setUp(self):
self.doc = FreeCAD.open(FreeCAD.getHomePath() + "/Mod/CAM/Tests/boxtest.fcstd")
self.job = self.doc.getObject("Job")
post = PostProcessorFactory.get_post_processor(self.job, "generic")
def tearDown(self):
FreeCAD.closeDocument("boxtest")
class TestPostProcessorScript(unittest.TestCase):
"""Test old-school posts"""
def setUp(self):
self.doc = FreeCAD.open(FreeCAD.getHomePath() + "/Mod/CAM/Tests/boxtest.fcstd")
self.job = self.doc.getObject("Job")
post = PostProcessorFactory.get_post_processor(self.job, "linuxcnc")
def tearDown(self):
FreeCAD.closeDocument("boxtest")
##
## You can run just this test using:
## ./FreeCAD -c -t Tests.TestPathPost.TestPathPost.test_postprocessors
##
#def test_postprocessors(self):
# """Test the postprocessors."""
# #
# # The tests are performed in the order they are listed:
# # one test performed on all of the postprocessors
# # then the next test on all of the postprocessors, etc.
# # You can comment out the tuples for tests that you don't want
# # to use.
# #
# tests_to_perform = (
# # (output_file_id, freecad_document, job_name, postprocessor_arguments,
# # postprocessor_list)
# #
# # test with all of the defaults (metric mode, etc.)
# ("default", "boxtest1", "Job", "--no-show-editor", ()),
# # test in Imperial mode
# ("imperial", "boxtest1", "Job", "--no-show-editor --inches", ()),
# # test in metric, G55, M4, the other way around the part
# ("other_way", "boxtest1", "Job001", "--no-show-editor", ()),
# # test in metric, split by fixtures, G54, G55, G56
# ("split", "boxtest1", "Job002", "--no-show-editor", ()),
# # test in metric mode without the header
# ("no_header", "boxtest1", "Job", "--no-header --no-show-editor", ()),
# # test translating G81, G82, and G83 to G00 and G01 commands
# (
# "drill_translate",
# "drill_test1",
# "Job",
# "--no-show-editor --translate_drill",
# ("grbl", "refactored_grbl"),
# ),
# )
# #
# # The postprocessors to test.
# # You can comment out any postprocessors that you don't want
# # to test.
# #
# postprocessors_to_test = (
# "centroid",
# # "fanuc",
# "grbl",
# "linuxcnc",
# "mach3_mach4",
# "refactored_centroid",
# # "refactored_fanuc",
# "refactored_grbl",
# "refactored_linuxcnc",
# "refactored_mach3_mach4",
# "refactored_test",
# )
# #
# # Enough of the path to where the tests are stored so that
# # they can be found by the python interpreter.
# #
# PATHTESTS_LOCATION = "Mod/CAM/Tests"
# #
# # The following code tries to re-use an open FreeCAD document
# # as much as possible. It compares the current document with
# # the document for the next test. If the names are different
# # then the current document is closed and the new document is
# # opened. The final document is closed at the end of the code.
# #
# current_document = ""
# for (
# output_file_id,
# freecad_document,
# job_name,
# postprocessor_arguments,
# postprocessor_list,
# ) in tests_to_perform:
# if current_document != freecad_document:
# if current_document != "":
# FreeCAD.closeDocument(current_document)
# current_document = freecad_document
# current_document_path = (
# FreeCAD.getHomePath()
# + PATHTESTS_LOCATION
# + os.path.sep
# + current_document
# + ".fcstd"
# )
# FreeCAD.open(current_document_path)
# job = FreeCAD.ActiveDocument.getObject(job_name)
# # Create the objects to be written by the postprocessor.
# postlist = PathPost.buildPostList(job)
# for postprocessor_id in postprocessors_to_test:
# if postprocessor_list == () or postprocessor_id in postprocessor_list:
# print(
# "\nRunning %s test on %s postprocessor:\n"
# % (output_file_id, postprocessor_id)
# )
# processor = PostProcessor.load(postprocessor_id)
# output_file_path = FreeCAD.getHomePath() + PATHTESTS_LOCATION
# output_file_pattern = "test_%s_%s" % (
# postprocessor_id,
# output_file_id,
# )
# output_file_extension = ".ngc"
# for idx, section in enumerate(postlist):
# partname = section[0]
# sublist = section[1]
# output_filename = PathPost.processFileNameSubstitutions(
# job,
# partname,
# idx,
# output_file_path,
# output_file_pattern,
# output_file_extension,
# )
# # print("output file: " + output_filename)
# file_path, extension = os.path.splitext(output_filename)
# reference_file_name = "%s%s%s" % (file_path, "_ref", extension)
# # print("reference file: " + reference_file_name)
# gcode = processor.export(
# sublist, output_filename, postprocessor_arguments
# )
# if not gcode:
# print("no gcode")
# with open(reference_file_name, "r") as fp:
# reference_gcode = fp.read()
# if not reference_gcode:
# print("no reference gcode")
# # Remove the "Output Time:" line in the header from the
# # comparison if it is present because it changes with
# # every test.
# gcode_lines = [
# i for i in gcode.splitlines(True) if "Output Time:" not in i
# ]
# reference_gcode_lines = [
# i
# for i in reference_gcode.splitlines(True)
# if "Output Time:" not in i
# ]
# if gcode_lines != reference_gcode_lines:
# msg = "".join(
# difflib.ndiff(gcode_lines, reference_gcode_lines)
# )
# self.fail(
# os.path.basename(output_filename)
# + " output doesn't match:\n"
# + msg
# )
# if not KEEP_DEBUG_OUTPUT:
# os.remove(output_filename)
# if current_document != "":
# FreeCAD.closeDocument(current_document)
class TestPathPostUtils(unittest.TestCase):
@@ -237,13 +410,13 @@ class TestPathPostUtils(unittest.TestCase):
)
def dumpgroup(group):
print("====Dump Group======")
for i in group:
print(i[0])
for j in i[1]:
print(f"--->{j.Name}")
print("====================")
#def dumpgroup(group):
#print("====Dump Group======")
#for i in group:
# print(i[0])
# for j in i[1]:
# print(f"--->{j.Name}")
#print("====================")
class TestBuildPostList(unittest.TestCase):