Merge pull request #22764 from tarman3/postprocess

CAM: Post Process only selected Operations
This commit is contained in:
sliptonic
2025-11-14 11:35:27 -06:00
committed by GitHub
6 changed files with 1345 additions and 19 deletions

View File

@@ -39,6 +39,7 @@
<file>icons/CAM_OperationB.svg</file>
<file>icons/CAM_Pocket.svg</file>
<file>icons/CAM_Post.svg</file>
<file>icons/CAM_PostSelected.svg</file>
<file>icons/CAM_Probe.svg</file>
<file>icons/CAM_Profile_Edges.svg</file>
<file>icons/CAM_Profile_Face.svg</file>

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 38 KiB

View File

@@ -124,7 +124,8 @@ class CAMWorkbench(Workbench):
Path.GuiInit.Startup()
# build commands list
projcmdlist = ["CAM_Job", "CAM_Post", "CAM_Sanity"]
projcmdlist = ["CAM_Job", "CAM_Sanity"]
postcmdlist = ["CAM_Post", "CAM_PostSelected"]
toolcmdlist = ["CAM_Inspect", "CAM_SelectLoop", "CAM_OpActiveToggle"]
simcmdlist = ["CAM_SimulatorGL", "CAM_Simulator"]
@@ -168,6 +169,14 @@ class CAMWorkbench(Workbench):
toolcmdlist.extend(PathToolBitLibraryCmd.BarList)
toolbitcmdlist = PathToolBitLibraryCmd.MenuList
postcmdgroup = ["CAM_PostTools"]
FreeCADGui.addCommand(
"CAM_PostTools",
PathCommandGroup(
postcmdlist,
QT_TRANSLATE_NOOP("CAM_PostTools", "Post process Operations"),
),
)
simcmdgroup = ["CAM_SimTools"]
FreeCADGui.addCommand(
"CAM_SimTools",
@@ -253,7 +262,10 @@ class CAMWorkbench(Workbench):
if not Path.Preferences.suppressOpenCamLibWarning():
FreeCAD.Console.PrintError("OpenCamLib is not working!\n")
self.appendToolbar(QT_TRANSLATE_NOOP("Workbench", "Project Setup"), projcmdlist)
self.appendToolbar(
QT_TRANSLATE_NOOP("Workbench", "Project Setup"),
projcmdlist + postcmdgroup,
)
self.appendToolbar(
QT_TRANSLATE_NOOP("Workbench", "Tool Commands"),
simcmdgroup + toolcmdlist,
@@ -271,6 +283,7 @@ class CAMWorkbench(Workbench):
self.appendMenu(
[QT_TRANSLATE_NOOP("Workbench", "&CAM")],
projcmdlist
+ postcmdlist
+ ["CAM_ExportTemplate", "Separator"]
+ simcmdlist
+ toolcmdlist
@@ -356,16 +369,17 @@ class CAMWorkbench(Workbench):
import PathScripts
menuAppended = False
if len(FreeCADGui.Selection.getSelection()) == 1:
obj = FreeCADGui.Selection.getSelection()[0]
selection = FreeCADGui.Selection.getSelection()
if len(selection) == 1:
obj = selection[0]
selectedName = obj.Name
if obj.isDerivedFrom("Path::Feature"):
self.appendContextMenu("", "Separator")
self.appendContextMenu("", ["CAM_Inspect"])
selectedName = obj.Name
if "Remote" in selectedName:
self.appendContextMenu("", ["Refresh_Path"])
if "Job" in selectedName:
self.appendContextMenu("", ["CAM_ExportTemplate"] + self.toolbitctxmenu)
self.appendContextMenu("", ["CAM_ExportTemplate"])
menuAppended = True
if isinstance(obj.Proxy, Path.Op.Base.ObjectOp):
self.appendContextMenu("", ["CAM_OperationCopy", "CAM_OpActiveToggle"])
@@ -388,6 +402,14 @@ class CAMWorkbench(Workbench):
if isinstance(obj.Proxy, Path.Tool.ToolBit):
self.appendContextMenu("", ["CAM_ToolBitSave", "CAM_ToolBitSaveAs"])
menuAppended = True
if selection:
for obj in selection:
if not obj.isDerivedFrom("Path::Feature"):
break
else:
self.appendContextMenu("", ["CAM_Post", "CAM_PostSelected"])
if menuAppended:
self.appendContextMenu("", "Separator")

View File

@@ -277,7 +277,7 @@ class JobPreferencesPage:
self.form.stockCreateCylinder.hide()
def getPostProcessor(self, name):
if not name in self.processor:
if name not in self.processor:
processor = PostProcessorFactory.get_post_processor(None, name)
self.processor[name] = processor
return processor

View File

@@ -61,8 +61,7 @@ def _resolve_post_processor_name(job):
if valid_name and PostProcessor.exists(valid_name):
return valid_name
else:
raise ValueError(f"Post processor not identified.")
raise ValueError("Post processor not identified.")
class DlgSelectPostProcessor:
@@ -109,16 +108,15 @@ class CommandPathPost:
"Pixmap": "CAM_Post",
"MenuText": QT_TRANSLATE_NOOP("CAM_Post", "Post Process"),
"Accel": "P, P",
"ToolTip": QT_TRANSLATE_NOOP("CAM_Post", "Post Processes the selected job"),
"ToolTip": QT_TRANSLATE_NOOP("CAM_Post", "Post Processes the selected Job"),
}
def IsActive(self):
selected = FreeCADGui.Selection.getSelectionEx()
if len(selected) != 1:
selection = FreeCADGui.Selection.getSelection()
if not selection:
return False
selected_object = selected[0].Object
self.candidate = PathUtils.findParentJob(selected_object)
self.candidate = PathUtils.findParentJob(selection[0])
return self.candidate is not None
@@ -212,7 +210,10 @@ class CommandPathPost:
return
# get a postprocessor
postprocessor = PostProcessorFactory.get_post_processor(self.candidate, postprocessor_name)
postprocessor = PostProcessorFactory.get_post_processor(
self.candidate,
postprocessor_name,
)
post_data = postprocessor.export()
# None is returned if there was an error during argument processing
@@ -259,8 +260,105 @@ class CommandPathPost:
FreeCAD.ActiveDocument.recompute()
class CommandPathPostSelected(CommandPathPost):
def GetResources(self):
return {
"Pixmap": "CAM_PostSelected",
"MenuText": QT_TRANSLATE_NOOP("CAM_Post", "Post Process Selected"),
"Accel": "P, O",
"ToolTip": QT_TRANSLATE_NOOP("CAM_Post", "Post Processes the selected operations"),
}
def IsActive(self):
selection = FreeCADGui.Selection.getSelection()
if not selection:
return False
return all(hasattr(op, "Path") and not op.Name.startswith("Job") for op in selection)
def Activated(self):
"""
Handles the activation of post processing, initiating the process based
on user selection and document context.
"""
FreeCAD.ActiveDocument.openTransaction("Post Process the Selected operations")
selection = FreeCADGui.Selection.getSelection()
job = PathUtils.findParentJob(selection[0])
if (
not job
and hasattr(selection[0], "Base")
and isinstance(selection[0].Base, list)
and selection[0].Base
):
# find 'job' for operation inside 'Array' with multi tool controller
baseOp = FreeCAD.ActiveDocument.getObject(selection[0].Base[0])
job = PathUtils.findParentJob(baseOp)
opCandidates = [op for op in selection if hasattr(op, "Path") and "Job" not in op.Name]
operations = []
if opCandidates and job.Operations.Group != opCandidates:
msgBox = QtGui.QMessageBox()
msgBox.setWindowTitle("Post Process")
msgBox.setText("<p align='center'>What needs to be exported?</p>")
msgBox.setInformativeText(
"<p align='center'>Check to make sure that you won't break anything by leaving out operations</p>"
)
msgBox.findChild(QtGui.QGridLayout).setColumnMinimumWidth(1, 250)
btn1 = msgBox.addButton("Only selected", QtGui.QMessageBox.ButtonRole.YesRole)
btn2 = msgBox.addButton("All", QtGui.QMessageBox.ButtonRole.NoRole)
msgBox.setDefaultButton(btn2)
msgBox.exec()
if msgBox.clickedButton() == btn1:
print(
f"Post process only selected operations: {', '.join([op.Name for op in opCandidates])}"
)
operations = opCandidates
postprocessor_name = _resolve_post_processor_name(job)
Path.Log.debug(f"Post Processor: {postprocessor_name}")
if not postprocessor_name:
FreeCAD.ActiveDocument.abortTransaction()
return
# get a postprocessor
postprocessor = PostProcessorFactory.get_post_processor(
{"job": job, "operations": operations}, postprocessor_name
)
post_data = postprocessor.export()
# None is returned if there was an error during argument processing
# otherwise the "usual" post_data data structure is returned.
if not post_data:
FreeCAD.ActiveDocument.abortTransaction()
return
policy = Path.Preferences.defaultOutputPolicy()
generator = FilenameGenerator(job=job)
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)
if gcode is not None:
# write the results to the file
self._write_file(fname, gcode, policy)
FreeCAD.ActiveDocument.commitTransaction()
FreeCAD.ActiveDocument.recompute()
if FreeCAD.GuiUp:
# register the FreeCAD command
FreeCADGui.addCommand("CAM_Post", CommandPathPost())
FreeCADGui.addCommand("CAM_PostSelected", CommandPathPostSelected())
FreeCAD.Console.PrintLog("Loading PathPost… done\n")

View File

@@ -116,6 +116,8 @@ class PostProcessorFactory:
Path.Log.debug(f"Post processor {postname} is a script")
return WrapperPost(job, module_path, module_name)
return None
def needsTcOp(oldTc, newTc):
return (
@@ -133,11 +135,19 @@ class PostProcessor:
self._tooltip = tooltip
self._tooltipargs = tooltipargs
self._units = units
self._job = job
self._args = args
self._kwargs = kwargs
self.reinitialize()
if isinstance(job, dict):
# process only selected operations
self._job = job["job"]
self._operations = job["operations"]
else:
# get all operations from 'Operations' group
self._job = job
self._operations = getattr(job.Operations, "Group", [])
@classmethod
def exists(cls, processor):
return processor in Path.Preferences.allAvailablePostProcessors()
@@ -203,7 +213,7 @@ class PostProcessor:
sublist = [__fixtureSetup(index, f, self._job)]
# Now generate the gcode
for obj in self._job.Operations.Group:
for obj in self._operations:
tc = PathUtil.toolControllerForOp(obj)
if tc is not None and PathUtil.activeForOp(obj):
if needsTcOp(currTc, tc):
@@ -240,7 +250,7 @@ class PostProcessor:
postlist.append((toolstring, sublist))
Path.Log.track(self._job.PostProcessorOutputFile)
for idx, obj in enumerate(self._job.Operations.Group):
for idx, obj in enumerate(self._operations):
Path.Log.track(obj.Label)
# check if the operation is active
@@ -286,7 +296,7 @@ class PostProcessor:
currTc = None
# Now generate the gcode
for obj in self._job.Operations.Group:
for obj in self._operations:
# check if the operation is active
if not PathUtil.activeForOp(obj):