Merge pull request #13668 from Ondsel-Development/RefactorPostCommand

Refactor post command
This commit is contained in:
sliptonic
2024-05-06 09:49:28 -05:00
committed by GitHub
11 changed files with 1255 additions and 1103 deletions

View File

@@ -147,6 +147,7 @@ SET(PathPythonPostScripts_SRCS
Path/Post/scripts/fanuc_post.py
Path/Post/scripts/fangling_post.py
Path/Post/scripts/gcode_pre.py
Path/Post/scripts/generic_post.py
Path/Post/scripts/grbl_post.py
Path/Post/scripts/heidenhain_post.py
Path/Post/scripts/jtech_post.py

View File

@@ -20,7 +20,7 @@
# * *
# ***************************************************************************
from Path.Post.Processor import PostProcessor
from Path.Post.Processor import PostProcessorFactory # PostProcessor,
from PySide import QtCore
from PySide.QtCore import QT_TRANSLATE_NOOP
import FreeCAD
@@ -112,7 +112,9 @@ class ObjectJob:
"App::PropertyFile",
"PostProcessorOutputFile",
"Output",
QT_TRANSLATE_NOOP("App::Property", "The G-code output file for this project"),
QT_TRANSLATE_NOOP(
"App::Property", "The G-code output file for this project"
),
)
obj.addProperty(
"App::PropertyEnumeration",
@@ -549,7 +551,7 @@ class ObjectJob:
def onChanged(self, obj, prop):
if prop == "PostProcessor" and obj.PostProcessor:
processor = PostProcessor.load(obj.PostProcessor)
processor = PostProcessorFactory.get_post_processor(obj, obj.PostProcessor)
self.tooltip = processor.tooltip
self.tooltipArgs = processor.tooltipArgs

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,327 @@
# * 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
import re
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
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())
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."""
raise NotImplementedError("Subclass must implement abstract method")
# 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."""
raise NotImplementedError("Subclass must implement abstract method")
# 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,154 @@
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 = os.getcwd()
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

@@ -0,0 +1,87 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2024 Ondsel <development@ondsel.com> *
# * *
# * This program is free software; you can redistribute it and/or modify *
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENCE text file. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this program; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************
import os
from Path.Post.Processor import PostProcessor
import Path
import FreeCAD
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
translate = FreeCAD.Qt.translate
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 Generic(PostProcessor):
def __init__(self, job):
super().__init__(
job,
tooltip=translate("CAM", "Generic post processor"),
tooltipargs=["arg1", "arg2"],
units="kg",
)
Path.Log.debug("Generic post processor initialized")
def export(self):
Path.Log.debug("Exporting the job")
postables = self._buildPostList()
Path.Log.debug(f"postables count: {len(postables)}")
g_code_sections = []
for idx, section in enumerate(postables):
partname, sublist = section
# here is where the sections are converted to gcode.
g_code_sections.append((idx, partname))
return g_code_sections
@property
def tooltip(self):
tooltip = """
This is a generic post processor.
It doesn't do anything yet because we haven't immplemented it.
Implementing it would be a good idea
"""
return tooltip
@property
def tooltipArgs(self):
argtooltip = """
--arg1: This is the first argument
--arg2: This is the second argument
"""
return argtooltip
@property
def units(self):
return self._units

View File

@@ -401,6 +401,9 @@ def findToolController(obj, proxy, name=None):
def findParentJob(obj):
"""retrieves a parent job object for an operation or other Path object"""
Path.Log.track()
if hasattr(obj, "Proxy") and isinstance(obj.Proxy, PathJob.ObjectJob):
return obj
for i in obj.InList:
if hasattr(i, "Proxy") and isinstance(i.Proxy, PathJob.ObjectJob):
return i

View File

@@ -44,10 +44,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 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
@@ -83,10 +86,11 @@ 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
False if TestOutputNameSubstitution.__name__ else True
# False if TestOutputNameSubstitution.__name__ else True
False if TestPathAdaptive.__name__ else True
False if TestPathCore.__name__ else True
False if TestPathOpDeburr.__name__ else True
@@ -96,7 +100,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

@@ -34,6 +34,7 @@ Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
DOC = FreeCAD.getHomePath() + "Mod/CAM/Tests/test_geomop.fcstd"
def getWire(obj, nr=0):
return obj.Tip.Profile[0].Shape.Wires[nr]
@@ -81,6 +82,13 @@ def wireMarkers(wire):
class TestPathOpUtil(PathTestUtils.PathTestBase):
@classmethod
def setUpClass(cls):
cls.doc = FreeCAD.openDocument(DOC)
@classmethod
def tearDownClass(cls):
FreeCAD.closeDocument(cls.doc.Name)
def test00(self):
"""Verify isWireClockwise for polygon wires."""
@@ -137,8 +145,7 @@ class TestPathOpUtil(PathTestUtils.PathTestBase):
def test11(self):
"""Check offsetting a circular hole."""
doc = FreeCAD.openDocument(DOC)
obj = doc.getObjectsByLabel("offset-circle")[0]
obj = self.doc.getObjectsByLabel("offset-circle")[0]
small = getWireInside(obj)
self.assertRoughly(10, small.Edges[0].Curve.Radius)
@@ -154,12 +161,10 @@ class TestPathOpUtil(PathTestUtils.PathTestBase):
self.assertEqual(1, len(wire.Edges))
self.assertRoughly(0.1, wire.Edges[0].Curve.Radius)
self.assertCoincide(Vector(0, 0, 1), wire.Edges[0].Curve.Axis)
FreeCAD.closeDocument("test_geomop")
def test12(self):
"""Check offsetting a circular hole by the radius or more makes the hole vanish."""
doc = FreeCAD.openDocument(DOC)
obj = doc.getObjectsByLabel("offset-circle")[0]
obj = self.doc.getObjectsByLabel("offset-circle")[0]
small = getWireInside(obj)
self.assertRoughly(10, small.Edges[0].Curve.Radius)
@@ -168,12 +173,10 @@ class TestPathOpUtil(PathTestUtils.PathTestBase):
wire = PathOpUtil.offsetWire(small, obj.Shape, 15, True)
self.assertIsNone(wire)
FreeCAD.closeDocument("test_geomop")
def test13(self):
"""Check offsetting a cylinder succeeds."""
doc = FreeCAD.openDocument(DOC)
obj = doc.getObjectsByLabel("offset-circle")[0]
obj = self.doc.getObjectsByLabel("offset-circle")[0]
big = getWireOutside(obj)
self.assertRoughly(20, big.Edges[0].Curve.Radius)
@@ -189,12 +192,10 @@ class TestPathOpUtil(PathTestUtils.PathTestBase):
self.assertEqual(1, len(wire.Edges))
self.assertRoughly(40, wire.Edges[0].Curve.Radius)
self.assertCoincide(Vector(0, 0, -1), wire.Edges[0].Curve.Axis)
FreeCAD.closeDocument("test_geomop")
def test14(self):
"""Check offsetting a hole with Placement."""
doc = FreeCAD.openDocument(DOC)
obj = doc.getObjectsByLabel("offset-placement")[0]
obj = self.doc.getObjectsByLabel("offset-placement")[0]
wires = [
w
@@ -215,12 +216,10 @@ class TestPathOpUtil(PathTestUtils.PathTestBase):
self.assertRoughly(8, wire.Edges[0].Curve.Radius)
self.assertCoincide(Vector(0, 0, 0), wire.Edges[0].Curve.Center)
self.assertCoincide(Vector(0, 0, 1), wire.Edges[0].Curve.Axis)
FreeCAD.closeDocument("test_geomop")
def test15(self):
"""Check offsetting a cylinder with Placement."""
doc = FreeCAD.openDocument(DOC)
obj = doc.getObjectsByLabel("offset-placement")[0]
obj = self.doc.getObjectsByLabel("offset-placement")[0]
wires = [
w
@@ -241,12 +240,10 @@ class TestPathOpUtil(PathTestUtils.PathTestBase):
self.assertRoughly(22, wire.Edges[0].Curve.Radius)
self.assertCoincide(Vector(0, 0, 0), wire.Edges[0].Curve.Center)
self.assertCoincide(Vector(0, 0, -1), wire.Edges[0].Curve.Axis)
FreeCAD.closeDocument("test_geomop")
def test20(self):
"""Check offsetting hole wire succeeds."""
doc = FreeCAD.openDocument(DOC)
obj = doc.getObjectsByLabel("offset-edge")[0]
obj = self.doc.getObjectsByLabel("offset-edge")[0]
small = getWireInside(obj)
# sanity check
@@ -276,12 +273,10 @@ class TestPathOpUtil(PathTestUtils.PathTestBase):
False,
[Vector(0, 4, 0), Vector(-x, -2, 0), Vector(x, -2, 0), Vector(0, 4, 0)],
)
FreeCAD.closeDocument("test_geomop")
def test21(self):
"""Check offsetting hole wire for more than it's size makes hole vanish."""
doc = FreeCAD.openDocument(DOC)
obj = doc.getObjectsByLabel("offset-edge")[0]
obj = self.doc.getObjectsByLabel("offset-edge")[0]
small = getWireInside(obj)
# sanity check
@@ -299,12 +294,10 @@ class TestPathOpUtil(PathTestUtils.PathTestBase):
)
wire = PathOpUtil.offsetWire(small, obj.Shape, 5, True)
self.assertIsNone(wire)
FreeCAD.closeDocument("test_geomop")
def test22(self):
"""Check offsetting a body wire succeeds."""
doc = FreeCAD.openDocument(DOC)
obj = doc.getObjectsByLabel("offset-edge")[0]
obj = self.doc.getObjectsByLabel("offset-edge")[0]
big = getWireOutside(obj)
# sanity check
@@ -351,12 +344,10 @@ class TestPathOpUtil(PathTestUtils.PathTestBase):
self.assertIsNone("%s: angle=%s" % (type(e.Curve), angle))
lastAngle = angle
self.assertTrue(PathOpUtil.isWireClockwise(wire))
FreeCAD.closeDocument("test_geomop")
def test31(self):
"""Check offsetting a cylinder."""
doc = FreeCAD.openDocument(DOC)
obj = doc.getObjectsByLabel("circle-cut")[0]
obj = self.doc.getObjectsByLabel("circle-cut")[0]
wire = PathOpUtil.offsetWire(getWire(obj.Tool), getPositiveShape(obj), 3, True)
self.assertEqual(1, len(wire.Edges))
@@ -372,12 +363,10 @@ class TestPathOpUtil(PathTestUtils.PathTestBase):
self.assertCoincide(Vector(), edge.Curve.Center)
self.assertCoincide(Vector(0, 0, +1), edge.Curve.Axis)
self.assertRoughly(33, edge.Curve.Radius)
FreeCAD.closeDocument("test_geomop")
def test32(self):
"""Check offsetting a box."""
doc = FreeCAD.openDocument(DOC)
obj = doc.getObjectsByLabel("square-cut")[0]
obj = self.doc.getObjectsByLabel("square-cut")[0]
wire = PathOpUtil.offsetWire(getWire(obj.Tool), getPositiveShape(obj), 3, True)
self.assertEqual(8, len(wire.Edges))
@@ -413,12 +402,10 @@ class TestPathOpUtil(PathTestUtils.PathTestBase):
self.assertRoughly(3, e.Curve.Radius)
self.assertCoincide(Vector(0, 0, +1), e.Curve.Axis)
self.assertFalse(PathOpUtil.isWireClockwise(wire))
FreeCAD.closeDocument("test_geomop")
def test33(self):
"""Check offsetting a triangle."""
doc = FreeCAD.openDocument(DOC)
obj = doc.getObjectsByLabel("triangle-cut")[0]
obj = self.doc.getObjectsByLabel("triangle-cut")[0]
wire = PathOpUtil.offsetWire(getWire(obj.Tool), getPositiveShape(obj), 3, True)
self.assertEqual(6, len(wire.Edges))
@@ -447,12 +434,10 @@ class TestPathOpUtil(PathTestUtils.PathTestBase):
if Part.Circle == type(e.Curve):
self.assertRoughly(3, e.Curve.Radius)
self.assertCoincide(Vector(0, 0, +1), e.Curve.Axis)
FreeCAD.closeDocument("test_geomop")
def test34(self):
"""Check offsetting a shape."""
doc = FreeCAD.openDocument(DOC)
obj = doc.getObjectsByLabel("shape-cut")[0]
obj = self.doc.getObjectsByLabel("shape-cut")[0]
wire = PathOpUtil.offsetWire(getWire(obj.Tool), getPositiveShape(obj), 3, True)
self.assertEqual(6, len(wire.Edges))
@@ -482,12 +467,10 @@ class TestPathOpUtil(PathTestUtils.PathTestBase):
if Part.Circle == type(e.Curve):
self.assertRoughly(radius, e.Curve.Radius)
self.assertCoincide(Vector(0, 0, +1), e.Curve.Axis)
FreeCAD.closeDocument("test_geomop")
def test35(self):
"""Check offsetting a cylindrical hole."""
doc = FreeCAD.openDocument(DOC)
obj = doc.getObjectsByLabel("circle-cut")[0]
obj = self.doc.getObjectsByLabel("circle-cut")[0]
wire = PathOpUtil.offsetWire(getWire(obj.Tool), getNegativeShape(obj), 3, True)
self.assertEqual(1, len(wire.Edges))
@@ -503,12 +486,10 @@ class TestPathOpUtil(PathTestUtils.PathTestBase):
self.assertCoincide(Vector(), edge.Curve.Center)
self.assertCoincide(Vector(0, 0, -1), edge.Curve.Axis)
self.assertRoughly(27, edge.Curve.Radius)
FreeCAD.closeDocument("test_geomop")
def test36(self):
"""Check offsetting a square hole."""
doc = FreeCAD.openDocument(DOC)
obj = doc.getObjectsByLabel("square-cut")[0]
obj = self.doc.getObjectsByLabel("square-cut")[0]
wire = PathOpUtil.offsetWire(getWire(obj.Tool), getNegativeShape(obj), 3, True)
self.assertEqual(4, len(wire.Edges))
@@ -530,12 +511,10 @@ class TestPathOpUtil(PathTestUtils.PathTestBase):
if Path.Geom.isRoughly(e.Vertexes[0].Point.y, e.Vertexes[1].Point.y):
self.assertRoughly(54, e.Length)
self.assertTrue(PathOpUtil.isWireClockwise(wire))
FreeCAD.closeDocument("test_geomop")
def test37(self):
"""Check offsetting a triangular holee."""
doc = FreeCAD.openDocument(DOC)
obj = doc.getObjectsByLabel("triangle-cut")[0]
obj = self.doc.getObjectsByLabel("triangle-cut")[0]
wire = PathOpUtil.offsetWire(getWire(obj.Tool), getNegativeShape(obj), 3, True)
self.assertEqual(3, len(wire.Edges))
@@ -552,12 +531,10 @@ class TestPathOpUtil(PathTestUtils.PathTestBase):
for e in wire.Edges:
self.assertRoughly(length, e.Length)
self.assertTrue(PathOpUtil.isWireClockwise(wire))
FreeCAD.closeDocument("test_geomop")
def test38(self):
"""Check offsetting a shape hole."""
doc = FreeCAD.openDocument(DOC)
obj = doc.getObjectsByLabel("shape-cut")[0]
obj = self.doc.getObjectsByLabel("shape-cut")[0]
wire = PathOpUtil.offsetWire(getWire(obj.Tool), getNegativeShape(obj), 3, True)
self.assertEqual(6, len(wire.Edges))
@@ -587,12 +564,10 @@ class TestPathOpUtil(PathTestUtils.PathTestBase):
if Part.Circle == type(e.Curve):
self.assertRoughly(radius, e.Curve.Radius)
self.assertCoincide(Vector(0, 0, -1), e.Curve.Axis)
FreeCAD.closeDocument("test_geomop")
def test40(self):
"""Check offsetting a single outside edge forward."""
doc = FreeCAD.openDocument(DOC)
obj = doc.getObjectsByLabel("offset-edge")[0]
obj = self.doc.getObjectsByLabel("offset-edge")[0]
w = getWireOutside(obj)
length = 40 * math.cos(math.pi / 6)
@@ -628,12 +603,10 @@ class TestPathOpUtil(PathTestUtils.PathTestBase):
self.assertCoincide(Vector(+x, y, 0), wire.Edges[0].Vertexes[0].Point)
self.assertCoincide(Vector(-x, y, 0), wire.Edges[0].Vertexes[1].Point)
FreeCAD.closeDocument("test_geomop")
def test41(self):
"""Check offsetting a single outside edge not forward."""
doc = FreeCAD.openDocument(DOC)
obj = doc.getObjectsByLabel("offset-edge")[0]
obj = self.doc.getObjectsByLabel("offset-edge")[0]
w = getWireOutside(obj)
length = 40 * math.cos(math.pi / 6)
@@ -668,14 +641,12 @@ class TestPathOpUtil(PathTestUtils.PathTestBase):
self.assertCoincide(Vector(-x, y, 0), wire.Edges[0].Vertexes[0].Point)
self.assertCoincide(Vector(+x, y, 0), wire.Edges[0].Vertexes[1].Point)
FreeCAD.closeDocument("test_geomop")
def test42(self):
"""Check offsetting multiple outside edges."""
doc = FreeCAD.openDocument(DOC)
obj = doc.getObjectsByLabel("offset-edge")[0]
obj = self.doc.getObjectsByLabel("offset-edge")[0]
obj.Shape.tessellate(0.01)
doc.recompute()
self.doc.recompute()
w = getWireOutside(obj)
length = 40 * math.cos(math.pi / 6)
@@ -713,14 +684,12 @@ class TestPathOpUtil(PathTestUtils.PathTestBase):
self.assertEqual(1, len(rEdges))
self.assertCoincide(Vector(0, 20, 0), rEdges[0].Curve.Center)
self.assertCoincide(Vector(0, 0, +1), rEdges[0].Curve.Axis)
FreeCAD.closeDocument("test_geomop")
def test43(self):
"""Check offsetting multiple backwards outside edges."""
# This is exactly the same as test32, except that the wire is flipped to make
# sure the input orientation doesn't matter
doc = FreeCAD.openDocument(DOC)
obj = doc.getObjectsByLabel("offset-edge")[0]
obj = self.doc.getObjectsByLabel("offset-edge")[0]
w = getWireOutside(obj)
length = 40 * math.cos(math.pi / 6)
@@ -759,12 +728,10 @@ class TestPathOpUtil(PathTestUtils.PathTestBase):
self.assertEqual(1, len(rEdges))
self.assertCoincide(Vector(0, 20, 0), rEdges[0].Curve.Center)
self.assertCoincide(Vector(0, 0, +1), rEdges[0].Curve.Axis)
FreeCAD.closeDocument("test_geomop")
def test44(self):
"""Check offsetting a single inside edge forward."""
doc = FreeCAD.openDocument(DOC)
obj = doc.getObjectsByLabel("offset-edge")[0]
obj = self.doc.getObjectsByLabel("offset-edge")[0]
w = getWireInside(obj)
length = 20 * math.cos(math.pi / 6)
@@ -800,12 +767,10 @@ class TestPathOpUtil(PathTestUtils.PathTestBase):
self.assertCoincide(Vector(-x, y, 0), wire.Edges[0].Vertexes[0].Point)
self.assertCoincide(Vector(+x, y, 0), wire.Edges[0].Vertexes[1].Point)
FreeCAD.closeDocument("test_geomop")
def test45(self):
"""Check offsetting a single inside edge not forward."""
doc = FreeCAD.openDocument(DOC)
obj = doc.getObjectsByLabel("offset-edge")[0]
obj = self.doc.getObjectsByLabel("offset-edge")[0]
w = getWireInside(obj)
length = 20 * math.cos(math.pi / 6)
@@ -841,12 +806,10 @@ class TestPathOpUtil(PathTestUtils.PathTestBase):
self.assertCoincide(Vector(+x, y, 0), wire.Edges[0].Vertexes[0].Point)
self.assertCoincide(Vector(-x, y, 0), wire.Edges[0].Vertexes[1].Point)
FreeCAD.closeDocument("test_geomop")
def test46(self):
"""Check offsetting multiple inside edges."""
doc = FreeCAD.openDocument(DOC)
obj = doc.getObjectsByLabel("offset-edge")[0]
obj = self.doc.getObjectsByLabel("offset-edge")[0]
w = getWireInside(obj)
length = 20 * math.cos(math.pi / 6)
@@ -878,14 +841,12 @@ class TestPathOpUtil(PathTestUtils.PathTestBase):
rEdges = [e for e in wire.Edges if Part.Circle == type(e.Curve)]
self.assertEqual(0, len(rEdges))
FreeCAD.closeDocument("test_geomop")
def test47(self):
"""Check offsetting multiple backwards inside edges."""
# This is exactly the same as test36 except that the wire is flipped to make
# sure it's orientation doesn't matter
doc = FreeCAD.openDocument(DOC)
obj = doc.getObjectsByLabel("offset-edge")[0]
obj = self.doc.getObjectsByLabel("offset-edge")[0]
w = getWireInside(obj)
length = 20 * math.cos(math.pi / 6)
@@ -918,7 +879,6 @@ class TestPathOpUtil(PathTestUtils.PathTestBase):
rEdges = [e for e in wire.Edges if Part.Circle == type(e.Curve)]
self.assertEqual(0, len(rEdges))
FreeCAD.closeDocument("test_geomop")
def test50(self):
"""Orient an already oriented wire"""

File diff suppressed because it is too large Load Diff