예제 #1
0
    def loadSettings(self):
        self.form.leDefaultFilePath.setText(PathPreferences.defaultFilePath())
        self.form.leDefaultJobTemplate.setText(PathPreferences.defaultJobTemplate())

        blacklist = PathPreferences.postProcessorBlacklist()
        for processor in PathPreferences.allAvailablePostProcessors():
            item = QtGui.QListWidgetItem(processor)
            if processor in blacklist:
                item.setCheckState(QtCore.Qt.CheckState.Unchecked)
            else:
                item.setCheckState(QtCore.Qt.CheckState.Checked)
            item.setFlags( QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled | QtCore.Qt.ItemFlag.ItemIsUserCheckable)
            self.form.postProcessorList.addItem(item)
        self.verifyAndUpdateDefaultPostProcessorWith(PathPreferences.defaultPostProcessor())

        self.form.defaultPostProcessorArgs.setText(PathPreferences.defaultPostProcessorArgs())

        geomTol = Units.Quantity(PathPreferences.defaultGeometryTolerance(), Units.Length)
        self.form.geometryTolerance.setText(geomTol.UserString)
        self.form.curveAccuracy.setText(Units.Quantity(PathPreferences.defaultLibAreaCurveAccuracy(), Units.Length).UserString)

        self.form.leOutputFile.setText(PathPreferences.defaultOutputFile())
        self.selectComboEntry(self.form.cboOutputPolicy, PathPreferences.defaultOutputPolicy())

        self.form.tbDefaultFilePath.clicked.connect(self.browseDefaultFilePath)
        self.form.tbDefaultJobTemplate.clicked.connect(self.browseDefaultJobTemplate)
        self.form.postProcessorList.itemEntered.connect(self.setProcessorListTooltip)
        self.form.postProcessorList.itemChanged.connect(self.verifyAndUpdateDefaultPostProcessor)
        self.form.defaultPostProcessor.currentIndexChanged.connect(self.updateDefaultPostProcessorToolTip)
        self.form.tbOutputFile.clicked.connect(self.browseOutputFile)

        self.loadStockSettings()
예제 #2
0
    def __init__(self, obj, models, templateFile = None):
        self.obj = obj
        obj.addProperty("App::PropertyFile", "PostProcessorOutputFile", "Output", QtCore.QT_TRANSLATE_NOOP("PathJob","The NC output file for this project"))
        obj.addProperty("App::PropertyEnumeration", "PostProcessor", "Output", QtCore.QT_TRANSLATE_NOOP("PathJob","Select the Post Processor"))
        obj.addProperty("App::PropertyString", "PostProcessorArgs", "Output", QtCore.QT_TRANSLATE_NOOP("PathJob", "Arguments for the Post Processor (specific to the script)"))

        obj.addProperty("App::PropertyString", "Description", "Path", QtCore.QT_TRANSLATE_NOOP("PathJob","An optional description for this job"))
        obj.addProperty("App::PropertyString", "CycleTime", "Path", QtCore.QT_TRANSLATE_NOOP("PathOp", "Operations Cycle Time Estimation"))
        obj.setEditorMode('CycleTime', 1)  # read-only
        obj.addProperty("App::PropertyDistance", "GeometryTolerance", "Geometry", QtCore.QT_TRANSLATE_NOOP("PathJob", "For computing Paths; smaller increases accuracy, but slows down computation"))

        obj.addProperty("App::PropertyLink", "Stock", "Base", QtCore.QT_TRANSLATE_NOOP("PathJob", "Solid object to be used as stock."))
        obj.addProperty("App::PropertyLink", "Operations", "Base", QtCore.QT_TRANSLATE_NOOP("PathJob", "Compound path of all operations in the order they are processed."))
        obj.addProperty("App::PropertyLinkList", "ToolController", "Base", QtCore.QT_TRANSLATE_NOOP("PathJob", "Collection of tool controllers available for this job."))

        obj.addProperty("App::PropertyBool", "SplitOutput", "Output", QtCore.QT_TRANSLATE_NOOP("PathJob","Split output into multiple gcode files"))
        obj.addProperty("App::PropertyEnumeration", "OrderOutputBy", "WCS", QtCore.QT_TRANSLATE_NOOP("PathJob", "If multiple WCS, order the output this way"))
        obj.addProperty("App::PropertyStringList", "Fixtures", "WCS", QtCore.QT_TRANSLATE_NOOP("PathJob", "The Work Coordinate Systems for the Job"))
        obj.OrderOutputBy = ['Fixture', 'Tool', 'Operation']
        obj.Fixtures = ['G54']
        
        obj.PostProcessorOutputFile = PathPreferences.defaultOutputFile()
        #obj.setEditorMode("PostProcessorOutputFile", 0)  # set to default mode
        obj.PostProcessor = postProcessors = PathPreferences.allEnabledPostProcessors()
        defaultPostProcessor = PathPreferences.defaultPostProcessor()
        # Check to see if default post processor hasn't been 'lost' (This can happen when Macro dir has changed)
        if defaultPostProcessor in postProcessors:
            obj.PostProcessor = defaultPostProcessor
        else:
            obj.PostProcessor = postProcessors[0]
        obj.PostProcessorArgs = PathPreferences.defaultPostProcessorArgs()
        obj.GeometryTolerance = PathPreferences.defaultGeometryTolerance()

        ops = FreeCAD.ActiveDocument.addObject("Path::FeatureCompoundPython", "Operations")
        if ops.ViewObject:
            ops.ViewObject.Proxy = 0
            ops.ViewObject.Visibility = False

        obj.Operations = ops
        obj.setEditorMode('Operations', 2) # hide
        obj.setEditorMode('Placement', 2)

        self.setupSetupSheet(obj)
        self.setupBaseModel(obj, models)

        self.tooltip = None
        self.tooltipArgs = None

        obj.Proxy = self

        self.setFromTemplateFile(obj, templateFile)
        if not obj.Stock:
            stockTemplate = PathPreferences.defaultStockTemplate()
            if stockTemplate:
                obj.Stock = PathStock.CreateFromTemplate(obj, json.loads(stockTemplate))
            if not obj.Stock:
                obj.Stock = PathStock.CreateFromBase(obj)
        if obj.Stock.ViewObject:
            obj.Stock.ViewObject.Visibility = False
예제 #3
0
    def resolveFileName(self, job):
        path = PathPreferences.defaultOutputFile()
        if job.PostProcessorOutputFile:
            path = job.PostProcessorOutputFile
        filename = path
        if '%D' in filename:
            D = FreeCAD.ActiveDocument.FileName
            if D:
                D = os.path.dirname(D)
                # in case the document is in the current working directory
                if not D:
                    D = '.'
            else:
                FreeCAD.Console.PrintError("Please save document in order to resolve output path!\n")
                return None
            filename = filename.replace('%D', D)

        if '%d' in filename:
            d = FreeCAD.ActiveDocument.Label
            filename = filename.replace('%d', d)

        if '%j' in filename:
            j = job.Label
            filename = filename.replace('%j', j)

        if '%M' in filename:
            pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Macro")
            M = pref.GetString("MacroPath", FreeCAD.getUserAppDataDir())
            filename = filename.replace('%M', M)

        policy = PathPreferences.defaultOutputPolicy()

        openDialog = policy == 'Open File Dialog'
        if os.path.isdir(filename) or not os.path.isdir(os.path.dirname(filename)):
            # Either the entire filename resolves into a directory or the parent directory doesn't exist.
            # Either way I don't know what to do - ask for help
            openDialog = True

        if os.path.isfile(filename) and not openDialog:
            if policy == 'Open File Dialog on conflict':
                openDialog = True
            elif policy == 'Append Unique ID on conflict':
                fn, ext = os.path.splitext(filename)
                nr = fn[-3:]
                n = 1
                if nr.isdigit():
                    n = int(nr)
                while os.path.isfile("%s%03d%s" % (fn, n, ext)):
                    n = n + 1
                filename = "%s%03d%s" % (fn, n, ext)

        if openDialog:
            foo = QtGui.QFileDialog.getSaveFileName(QtGui.QApplication.activeWindow(), "Output File", filename)
            if foo:
                filename = foo[0]
            else:
                filename = None

        return filename
예제 #4
0
    def resolveOutputPath(self, job):
        if job.PostProcessorOutputFile != "":
            filepath = job.PostProcessorOutputFile
        elif PathPreferences.defaultOutputFile() != "":
            filepath = PathPreferences.defaultOutputFile()
        else:
            filepath = PathPreferences.macroFilePath()

        if "%D" in filepath:
            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
            filepath = filepath.replace("%D", D)

        if "%d" in filepath:
            d = FreeCAD.ActiveDocument.Label
            filepath = filepath.replace("%d", d)

        if "%j" in filepath:
            j = job.Label
            filepath = filepath.replace("%j", j)

        if "%M" in filepath:
            pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Macro")
            M = pref.GetString("MacroPath", FreeCAD.getUserAppDataDir())
            filepath = filepath.replace("%M", M)

        PathLog.debug("filepath: {}".format(filepath))

        # starting at the derived filename, iterate up until we have a valid
        # directory to write to
        while not os.path.isdir(filepath):
            filepath = os.path.dirname(filepath)

        PathLog.debug("filepath: {}".format(filepath))
        return filepath + os.sep
예제 #5
0
    def __init__(self, obj, models, templateFile = None):
        self.obj = obj
        obj.addProperty("App::PropertyFile", "PostProcessorOutputFile", "Output", QtCore.QT_TRANSLATE_NOOP("PathJob","The NC output file for this project"))
        obj.addProperty("App::PropertyEnumeration", "PostProcessor", "Output", QtCore.QT_TRANSLATE_NOOP("PathJob","Select the Post Processor"))
        obj.addProperty("App::PropertyString", "PostProcessorArgs", "Output", QtCore.QT_TRANSLATE_NOOP("PathJob", "Arguments for the Post Processor (specific to the script)"))

        obj.addProperty("App::PropertyString", "Description", "Path", QtCore.QT_TRANSLATE_NOOP("PathJob","An optional description for this job"))
        obj.addProperty("App::PropertyDistance", "GeometryTolerance", "Geometry", QtCore.QT_TRANSLATE_NOOP("PathJob", "For computing Paths; smaller increases accuracy, but slows down computation"))

        obj.addProperty("App::PropertyLink", "Stock", "Base", QtCore.QT_TRANSLATE_NOOP("PathJob", "Solid object to be used as stock."))
        obj.addProperty("App::PropertyLink", "Operations", "Base", QtCore.QT_TRANSLATE_NOOP("PathJob", "Compound path of all operations in the order they are processed."))
        obj.addProperty("App::PropertyLinkList", "ToolController", "Base", QtCore.QT_TRANSLATE_NOOP("PathJob", "Collection of tool controllers available for this job."))

        obj.addProperty("App::PropertyBool", "SplitOutput", "Output", QtCore.QT_TRANSLATE_NOOP("PathJob","Split output into multiple gcode files"))
        obj.addProperty("App::PropertyEnumeration", "OrderOutputBy", "WCS", QtCore.QT_TRANSLATE_NOOP("PathJob", "If multiple WCS, order the output this way"))
        obj.addProperty("App::PropertyStringList", "Fixtures", "WCS", QtCore.QT_TRANSLATE_NOOP("PathJob", "The Work Coordinate Systems for the Job"))
        obj.OrderOutputBy = ['Fixture', 'Tool', 'Operation']
        obj.Fixtures = ['G54']

        obj.PostProcessorOutputFile = PathPreferences.defaultOutputFile()
        #obj.setEditorMode("PostProcessorOutputFile", 0)  # set to default mode
        obj.PostProcessor = postProcessors = PathPreferences.allEnabledPostProcessors()
        defaultPostProcessor = PathPreferences.defaultPostProcessor()
        # Check to see if default post processor hasn't been 'lost' (This can happen when Macro dir has changed)
        if defaultPostProcessor in postProcessors:
            obj.PostProcessor = defaultPostProcessor
        else:
            obj.PostProcessor = postProcessors[0]
        obj.PostProcessorArgs = PathPreferences.defaultPostProcessorArgs()
        obj.GeometryTolerance = PathPreferences.defaultGeometryTolerance()

        ops = FreeCAD.ActiveDocument.addObject("Path::FeatureCompoundPython", "Operations")
        if ops.ViewObject:
            ops.ViewObject.Proxy = 0
            ops.ViewObject.Visibility = False

        obj.Operations = ops
        obj.setEditorMode('Operations', 2) # hide
        obj.setEditorMode('Placement', 2)

        self.setupSetupSheet(obj)
        self.setupBaseModel(obj, models)

        obj.Proxy = self

        self.setFromTemplateFile(obj, templateFile)
        if not obj.Stock:
            stockTemplate = PathPreferences.defaultStockTemplate()
            if stockTemplate:
                obj.Stock = PathStock.CreateFromTemplate(obj, json.loads(stockTemplate))
            if not obj.Stock:
                obj.Stock = PathStock.CreateFromBase(obj)
        if obj.Stock.ViewObject:
            obj.Stock.ViewObject.Visibility = False
예제 #6
0
파일: PathJob.py 프로젝트: Roy-043/FreeCAD
    def __init__(self, obj, models, templateFile=None):
        self.obj = obj
        self.tooltip = None
        self.tooltipArgs = None
        obj.Proxy = self

        obj.addProperty(
            "App::PropertyFile",
            "PostProcessorOutputFile",
            "Output",
            QT_TRANSLATE_NOOP("App::Property",
                              "The NC output file for this project"),
        )
        obj.addProperty(
            "App::PropertyEnumeration",
            "PostProcessor",
            "Output",
            QT_TRANSLATE_NOOP("App::Property", "Select the Post Processor"),
        )
        obj.addProperty(
            "App::PropertyString",
            "PostProcessorArgs",
            "Output",
            QT_TRANSLATE_NOOP(
                "App::Property",
                "Arguments for the Post Processor (specific to the script)",
            ),
        )
        obj.addProperty(
            "App::PropertyString",
            "LastPostProcessDate",
            "Output",
            QT_TRANSLATE_NOOP("App::Property",
                              "Last Time the Job was post-processed"),
        )
        obj.setEditorMode("LastPostProcessDate", 2)  # Hide
        obj.addProperty(
            "App::PropertyString",
            "LastPostProcessOutput",
            "Output",
            QT_TRANSLATE_NOOP("App::Property",
                              "Last Time the Job was post-processed"),
        )
        obj.setEditorMode("LastPostProcessOutput", 2)  # Hide

        obj.addProperty(
            "App::PropertyString",
            "Description",
            "Path",
            QT_TRANSLATE_NOOP("App::Property",
                              "An optional description for this job"),
        )
        obj.addProperty(
            "App::PropertyString",
            "CycleTime",
            "Path",
            QT_TRANSLATE_NOOP("App::Property", "Job Cycle Time Estimation"),
        )
        obj.setEditorMode("CycleTime", 1)  # read-only
        obj.addProperty(
            "App::PropertyDistance",
            "GeometryTolerance",
            "Geometry",
            QT_TRANSLATE_NOOP(
                "App::Property",
                "For computing Paths; smaller increases accuracy, but slows down computation",
            ),
        )

        obj.addProperty(
            "App::PropertyLink",
            "Stock",
            "Base",
            QT_TRANSLATE_NOOP("App::Property",
                              "Solid object to be used as stock."),
        )
        obj.addProperty(
            "App::PropertyLink",
            "Operations",
            "Base",
            QT_TRANSLATE_NOOP(
                "App::Property",
                "Compound path of all operations in the order they are processed.",
            ),
        )

        obj.addProperty(
            "App::PropertyEnumeration",
            "JobType",
            "Base",
            QT_TRANSLATE_NOOP("App::Property", "Select the Type of Job"),
        )
        obj.setEditorMode("JobType", 2)  # Hide

        obj.addProperty(
            "App::PropertyBool",
            "SplitOutput",
            "Output",
            QT_TRANSLATE_NOOP("App::Property",
                              "Split output into multiple gcode files"),
        )
        obj.addProperty(
            "App::PropertyEnumeration",
            "OrderOutputBy",
            "WCS",
            QT_TRANSLATE_NOOP("App::Property",
                              "If multiple WCS, order the output this way"),
        )
        obj.addProperty(
            "App::PropertyStringList",
            "Fixtures",
            "WCS",
            QT_TRANSLATE_NOOP("App::Property",
                              "The Work Coordinate Systems for the Job"),
        )

        obj.Fixtures = ["G54"]

        for n in self.propertyEnumerations():
            setattr(obj, n[0], n[1])

        obj.PostProcessorOutputFile = PathPreferences.defaultOutputFile()
        obj.PostProcessor = postProcessors = PathPreferences.allEnabledPostProcessors(
        )
        defaultPostProcessor = PathPreferences.defaultPostProcessor()
        # Check to see if default post processor hasn't been 'lost' (This can happen when Macro dir has changed)
        if defaultPostProcessor in postProcessors:
            obj.PostProcessor = defaultPostProcessor
        else:
            obj.PostProcessor = postProcessors[0]
        obj.PostProcessorArgs = PathPreferences.defaultPostProcessorArgs()
        obj.GeometryTolerance = PathPreferences.defaultGeometryTolerance()

        self.setupOperations(obj)
        self.setupSetupSheet(obj)
        self.setupBaseModel(obj, models)
        self.setupToolTable(obj)
        self.setFromTemplateFile(obj, templateFile)
        self.setupStock(obj)
예제 #7
0
    def __init__(self, obj, base, templateFile=None):
        self.obj = obj
        obj.addProperty(
            "App::PropertyFile", "PostProcessorOutputFile", "Output",
            QtCore.QT_TRANSLATE_NOOP("App::Property",
                                     "The NC output file for this project"))
        obj.addProperty(
            "App::PropertyEnumeration", "PostProcessor", "Output",
            QtCore.QT_TRANSLATE_NOOP("App::Property",
                                     "Select the Post Processor"))
        obj.addProperty(
            "App::PropertyString", "PostProcessorArgs", "Output",
            QtCore.QT_TRANSLATE_NOOP(
                "App::Property",
                "Arguments for the Post Processor (specific to the script)"))

        obj.addProperty(
            "App::PropertyString", "Description", "Path",
            QtCore.QT_TRANSLATE_NOOP("App::Property",
                                     "An optional description for this job"))
        obj.addProperty(
            "App::PropertyDistance", "GeometryTolerance", "Geometry",
            QtCore.QT_TRANSLATE_NOOP(
                "App::Property",
                "For computing Paths; smaller increases accuracy, but slows down computation"
            ))

        obj.addProperty(
            "App::PropertyLink", "Base", "Base",
            QtCore.QT_TRANSLATE_NOOP("PathJob",
                                     "The base object for all operations"))
        obj.addProperty(
            "App::PropertyLink", "Stock", "Base",
            QtCore.QT_TRANSLATE_NOOP("PathJob",
                                     "Solid object to be used as stock."))
        obj.addProperty(
            "App::PropertyLink", "Operations", "Base",
            QtCore.QT_TRANSLATE_NOOP(
                "PathJob",
                "Compound path of all operations in the order they are processed."
            ))
        obj.addProperty(
            "App::PropertyLinkList", "ToolController", "Base",
            QtCore.QT_TRANSLATE_NOOP(
                "PathJob",
                "Collection of tool controllers available for this job."))

        obj.PostProcessorOutputFile = PathPreferences.defaultOutputFile()
        #obj.setEditorMode("PostProcessorOutputFile", 0)  # set to default mode
        obj.PostProcessor = postProcessors = PathPreferences.allEnabledPostProcessors(
        )
        defaultPostProcessor = PathPreferences.defaultPostProcessor()
        # Check to see if default post processor hasn't been 'lost' (This can happen when Macro dir has changed)
        if defaultPostProcessor in postProcessors:
            obj.PostProcessor = defaultPostProcessor
        else:
            obj.PostProcessor = postProcessors[0]
        obj.PostProcessorArgs = PathPreferences.defaultPostProcessorArgs()
        obj.GeometryTolerance = PathPreferences.defaultGeometryTolerance()

        ops = FreeCAD.ActiveDocument.addObject("Path::FeatureCompoundPython",
                                               "Operations")
        if ops.ViewObject:
            ops.ViewObject.Proxy = 0
            ops.ViewObject.Visibility = False
        obj.Operations = ops
        obj.setEditorMode('Operations', 2)  # hide
        obj.setEditorMode('Placement', 2)

        self.setupSetupSheet(obj)

        obj.Base = createResourceClone(obj, base, 'Base', 'BaseGeometry')
        obj.Proxy = self

        self.setFromTemplateFile(obj, templateFile)
        if not obj.Stock:
            stockTemplate = PathPreferences.defaultStockTemplate()
            if stockTemplate:
                obj.Stock = PathStock.CreateFromTemplate(
                    obj, json.loads(stockTemplate))
            if not obj.Stock:
                obj.Stock = PathStock.CreateFromBase(obj)
        if obj.Stock.ViewObject:
            obj.Stock.ViewObject.Visibility = False
예제 #8
0
class CommandPathSanity:
    baseobj = None
    outputpath = PathPreferences.defaultOutputFile()
    squawkData = {"items": []}

    def GetResources(self):
        return {
            'Pixmap':
            'Path-Sanity',
            'MenuText':
            QtCore.QT_TRANSLATE_NOOP("Path_Sanity",
                                     "Check the path job for common errors"),
            'Accel':
            "P, S",
            'ToolTip':
            QtCore.QT_TRANSLATE_NOOP("Path_Sanity",
                                     "Check the path job for common errors")
        }

    def IsActive(self):
        obj = FreeCADGui.Selection.getSelectionEx()[0].Object
        return isinstance(obj.Proxy, PathScripts.PathJob.ObjectJob)

    def Activated(self):
        # if everything is ok, execute
        self.squawkData["items"] = []

        obj = FreeCADGui.Selection.getSelectionEx()[0].Object
        data = self.__summarize(obj)
        html = self.__report(data)
        if html is not None:
            print("HTML report written to {}".format(html))
            webbrowser.open(html)

    def __makePicture(self, obj, imageName):
        """
        Makes an image of the target object.  Returns filename
        """

        # remember vis state of document objects. Turn off all but target
        visible = [o for o in obj.Document.Objects if o.Visibility]
        for o in obj.Document.Objects:
            o.Visibility = False
        obj.Visibility = True

        aview = FreeCADGui.activeDocument().activeView()
        aview.setAnimationEnabled(False)

        mw = FreeCADGui.getMainWindow()
        mdi = mw.findChild(QtGui.QMdiArea)
        view = mdi.activeSubWindow()
        view.showNormal()
        view.resize(320, 320)

        imagepath = self.outputpath + '/{}'.format(imageName)

        aview.viewIsometric()
        FreeCADGui.Selection.clearSelection()
        FreeCADGui.SendMsgToActiveView("PerspectiveCamera")
        FreeCADGui.Selection.addSelection(obj)
        FreeCADGui.SendMsgToActiveView("ViewSelection")
        FreeCADGui.Selection.clearSelection()
        aview.saveImage(imagepath + '.png', 320, 320, 'Current')
        aview.saveImage(imagepath + '_t.png', 320, 320, 'Transparent')

        view.showMaximized()

        aview.setAnimationEnabled(True)

        # Restore visibility
        obj.Visibility = False
        for o in visible:
            o.Visibility = True

        return "{}_t.png".format(imagepath)

    def __report(self, data):
        """
        generates an asciidoc file with the report information
        """

        reportTemplate = """
= Setup Report for FreeCAD Job: {jobname}
:toc:
:icons: font
:imagesdir: ""
:data-uri:

== Part Information

|===
{infoTable}
|===


== Run Summary

|===
{runTable}
|===

== Rough Stock

|===
{stockTable}
|===

== Tool Data

{toolTables}

== Output (Gcode)

|===
{outTable}
|===

== Fixtures and Workholding

|===
{fixtureTable}
|===

== Squawks

|===
{squawkTable}
|===

"""
        # Generate the markup for the Part Information Section

        infoTable = ""

        PartLabel = translate("Path_Sanity", "Base Object(s)")
        SequenceLabel = translate("Path_Sanity", "Job Sequence")
        DescriptionLabel = translate("Path_Sanity", "Job Description")
        JobTypeLabel = translate("Path_Sanity", "Job Type")
        CADLabel = translate("Path_Sanity", "CAD File Name")
        LastSaveLabel = translate("Path_Sanity", "Last Save Date")
        CustomerLabel = translate("Path_Sanity", "Customer")
        DesignerLabel = translate("Path_Sanity", "Designer")

        d = data['designData']
        b = data['baseData']

        jobname = d['JobLabel']

        basestable = "!===\n"
        for key, val in b['bases'].items():
            basestable += "! " + key + " ! " + val + "\n"

        basestable += "!==="

        infoTable += "|*" + PartLabel + "* a| " + basestable + " .7+a|" + \
            "image::" + b['baseimage'] + "[" + PartLabel + "]\n"
        infoTable += "|*" + SequenceLabel + "*|" + d['Sequence']
        infoTable += "|*" + JobTypeLabel + "*|" + d['JobType']
        infoTable += "|*" + DescriptionLabel + "*|" + d['JobDescription']
        infoTable += "|*" + CADLabel + "*|" + d['FileName']
        infoTable += "|*" + LastSaveLabel + "*|" + d['LastModifiedDate']
        infoTable += "|*" + CustomerLabel + "*|" + d['Customer']
        infoTable += "|*" + DesignerLabel + "*|" + d['Designer']

        # Generate the markup for the Run Summary Section

        runTable = ""
        opLabel = translate("Path_Sanity", "Operation")
        zMinLabel = translate("Path_Sanity", "Minimum Z Height")
        zMaxLabel = translate("Path_Sanity", "Maximum Z Height")
        cycleTimeLabel = translate("Path_Sanity", "Cycle Time")
        coolantLabel = translate("Path_Sanity", "Coolant")
        jobTotalLabel = translate("Path_Sanity", "TOTAL JOB")

        d = data['runData']

        runTable += "|*" + opLabel + "*|*" + zMinLabel + "*|*" + zMaxLabel + \
            "*|*" + coolantLabel + "*|*" + cycleTimeLabel + "*\n"

        for i in d['items']:
            runTable += "|{}".format(i['opName'])
            runTable += "|{}".format(i['minZ'])
            runTable += "|{}".format(i['maxZ'])
            runTable += "|{}".format(i['coolantMode'])
            runTable += "|{}".format(i['cycleTime'])

        runTable += "|*" + jobTotalLabel + "* |{} |{} |{}".format(
            d['jobMinZ'], d['jobMaxZ'], d['cycletotal'])

        # Generate the markup for the Tool Data Section
        toolTables = ""

        toolLabel = translate("Path_Sanity", "Tool Number")
        descriptionLabel = translate("Path_Sanity", "Description")
        manufLabel = translate("Path_Sanity", "Manufacturer")
        partNumberLabel = translate("Path_Sanity", "Part Number")
        urlLabel = translate("Path_Sanity", "URL")
        inspectionNotesLabel = translate("Path_Sanity", "Inspection Notes")
        opLabel = translate("Path_Sanity", "Operation")
        tcLabel = translate("Path_Sanity", "Tool Controller")
        feedLabel = translate("Path_Sanity", "Feed Rate")
        speedLabel = translate("Path_Sanity", "Spindle Speed")
        shapeLabel = translate("Path_Sanity", "Tool Shape")
        diameterLabel = translate("Path_Sanity", "Tool Diameter")

        d = data['toolData']

        for key, value in d.items():
            toolTables += "=== {}: T{}\n".format(toolLabel, key)

            toolTables += "|===\n"

            toolTables += "|*{}*| {} a| image::{}[{}]\n".format(
                descriptionLabel, value['description'], value['imagepath'],
                key)
            toolTables += "|*{}* 2+| {}\n".format(manufLabel,
                                                  value['manufacturer'])
            toolTables += "|*{}* 2+| {}\n".format(partNumberLabel,
                                                  value['partNumber'])
            toolTables += "|*{}* 2+| {}\n".format(urlLabel, value['url'])
            toolTables += "|*{}* 2+| {}\n".format(inspectionNotesLabel,
                                                  value['inspectionNotes'])
            toolTables += "|*{}* 2+| {}\n".format(shapeLabel, value['shape'])
            toolTables += "|*{}* 2+| {}\n".format(diameterLabel,
                                                  value['diameter'])
            toolTables += "|===\n"

            toolTables += "|===\n"
            toolTables += "|*" + opLabel + "*|*" + tcLabel + "*|*" + feedLabel + "*|*" + speedLabel + "*\n"
            for o in value['ops']:
                toolTables += "|" + o['Operation'] + "|" + o[
                    'ToolController'] + "|" + o['Feed'] + "|" + o[
                        'Speed'] + "\n"
            toolTables += "|===\n"

            toolTables += "\n"

        # Generate the markup for the Rough Stock Section
        stockTable = ""
        xDimLabel = translate("Path_Sanity", "X Size")
        yDimLabel = translate("Path_Sanity", "Y Size")
        zDimLabel = translate("Path_Sanity", "Z Size")
        materialLabel = translate("Path_Sanity", "Material")

        d = data['stockData']

        stockTable += "|*{}*|{} .4+a|image::{}[stock]\n".format(
            materialLabel, d['material'], d['stockImage'])
        stockTable += "|*{}*|{}".format(xDimLabel, d['xLen'])
        stockTable += "|*{}*|{}".format(yDimLabel, d['yLen'])
        stockTable += "|*{}*|{}".format(zDimLabel, d['zLen'])

        # Generate the markup for the Fixture Section

        fixtureTable = ""
        offsetsLabel = translate("Path_Sanity", "Work Offsets")
        orderByLabel = translate("Path_Sanity", "Order By")
        datumLabel = translate("Path_Sanity", "Part Datum")

        d = data['fixtureData']

        fixtureTable += "|*{}*|{}\n".format(offsetsLabel, d['fixtures'])
        fixtureTable += "|*{}*|{}\n".format(orderByLabel, d['orderBy'])
        fixtureTable += "|*{}* a| image::{}[]".format(datumLabel,
                                                      d['datumImage'])

        # Generate the markup for the Output Section

        outTable = ""
        d = data['outputData']

        gcodeFileLabel = translate("Path_Sanity", "Gcode File")
        lastpostLabel = translate("Path_Sanity", "Last Post Process Date")
        stopsLabel = translate("Path_Sanity", "Stops")
        programmerLabel = translate("Path_Sanity", "Programmer")
        machineLabel = translate("Path_Sanity", "Machine")
        postLabel = translate("Path_Sanity", "Postprocessor")
        flagsLabel = translate("Path_Sanity", "Post Processor Flags")
        fileSizeLabel = translate("Path_Sanity", "File Size (kbs)")
        lineCountLabel = translate("Path_Sanity", "Line Count")

        outTable += "|*{}*|{}\n".format(gcodeFileLabel, d['lastgcodefile'])
        outTable += "|*{}*|{}\n".format(lastpostLabel, d['lastpostprocess'])
        outTable += "|*{}*|{}\n".format(stopsLabel, d['optionalstops'])
        outTable += "|*{}*|{}\n".format(programmerLabel, d['programmer'])
        outTable += "|*{}*|{}\n".format(machineLabel, d['machine'])
        outTable += "|*{}*|{}\n".format(postLabel, d['postprocessor'])
        outTable += "|*{}*|{}\n".format(flagsLabel, d['postprocessorFlags'])
        outTable += "|*{}*|{}\n".format(fileSizeLabel, d['filesize'])
        outTable += "|*{}*|{}\n".format(lineCountLabel, d['linecount'])

        # Generate the markup for the Squawk Section

        noteLabel = translate("Path_Sanity", "Note")
        operatorLabel = translate("Path_Sanity", "Operator")
        dateLabel = translate("Path_Sanity", "Date")

        squawkTable = "|*{}*|*{}*|*{}*\n".format(noteLabel, operatorLabel,
                                                 dateLabel)

        d = data['squawkData']
        for i in d['items']:
            squawkTable += "a|{}: {}".format(i['squawkType'], i['Note'])
            squawkTable += "|{}".format(i['Operator'])
            squawkTable += "|{}".format(i['Date'])
            squawkTable += "\n"

        # merge template and custom markup

        report = reportTemplate.format(jobname=jobname,
                                       infoTable=infoTable,
                                       runTable=runTable,
                                       toolTables=toolTables,
                                       stockTable=stockTable,
                                       fixtureTable=fixtureTable,
                                       outTable=outTable,
                                       squawkTable=squawkTable)

        # Save the report

        reportraw = self.outputpath + '/setupreport.asciidoc'
        reporthtml = self.outputpath + '/setupreport.html'
        with open(reportraw, 'w') as fd:
            fd.write(report)
            fd.close()

        try:
            result = os.system('asciidoctor {} -o {}'.format(
                reportraw, reporthtml))
            if str(result) == "32512":
                print('asciidoctor not found')
                reporthtml = None

        except Exception as e:
            print(e)

        return reporthtml

    def __summarize(self, obj):
        """
        Top level function to summarize information for the report
        Returns a dictionary of sections
        """
        data = {}
        data['baseData'] = self.__baseObjectData(obj)
        data['designData'] = self.__designData(obj)
        data['toolData'] = self.__toolData(obj)
        data['runData'] = self.__runData(obj)
        data['outputData'] = self.__outputData(obj)
        data['fixtureData'] = self.__fixtureData(obj)
        data['stockData'] = self.__stockData(obj)
        data['squawkData'] = self.squawkData
        return data

    def squawk(self, operator, note, date=datetime.now(), squawkType="NOTE"):
        squawkType = squawkType if squawkType in \
            ["NOTE", "WARNING", "CAUTION", "TIP"] else "NOTE"

        self.squawkData['items'].append({
            "Date": str(date),
            "Operator": operator,
            "Note": note,
            "squawkType": squawkType
        })

    def __baseObjectData(self, obj):
        data = {'baseimage': '', 'bases': ''}
        try:
            bases = {}
            for name, count in \
                    PathUtil.keyValueIter(Counter([obj.Proxy.baseObject(obj,
                        o).Label for o in obj.Model.Group])):
                bases[name] = str(count)

            data['baseimage'] = self.__makePicture(obj.Model, "baseimage")
            data['bases'] = bases

        except Exception as e:
            data['errors'] = e
            self.squawk("PathSanity(__baseObjectData)",
                        e,
                        squawkType="CAUTION")

        return data

    def __designData(self, obj):
        """
        Returns header information about the design document
        Returns information about issues and concerns (squawks)
        """

        data = {
            'FileName': '',
            'LastModifiedDate': '',
            'Customer': '',
            'Designer': '',
            'JobDescription': '',
            'JobLabel': '',
            'Sequence': '',
            'JobType': ''
        }
        try:
            data['FileName'] = obj.Document.FileName
            data['LastModifiedDate'] = str(obj.Document.LastModifiedDate)
            data['Customer'] = obj.Document.Company
            data['Designer'] = obj.Document.LastModifiedBy
            data['JobDescription'] = obj.Description
            data['JobLabel'] = obj.Label

            n = 0
            m = 0
            for i in obj.Document.Objects:
                if hasattr(i, "Proxy"):
                    if isinstance(i.Proxy, PathScripts.PathJob.ObjectJob):
                        m += 1
                        if i is obj:
                            n = m
            data['Sequence'] = "{} of {}".format(n, m)
            data['JobType'] = "2.5D Milling"  # improve after job types added

        except Exception as e:
            data['errors'] = e
            self.squawk("PathSanity(__designData)", e, squawkType="CAUTION")

        return data

    def __toolData(self, obj):
        """
        Returns information about the tools used in the job, and associated
        toolcontrollers
        Returns information about issues and problems with the tools (squawks)
        """
        data = {}

        try:
            for TC in obj.ToolController:
                if not hasattr(TC.Tool, 'BitBody'):
                    self.squawk(
                        "PathSanity",
                        "Tool number {} is a legacy tool. Legacy tools not \
                        supported by Path-Sanity".format(TC.ToolNumber),
                        squawkType="WARNING")
                    continue  # skip old-style tools
                tooldata = data.setdefault(str(TC.ToolNumber), {})
                bitshape = tooldata.setdefault('BitShape', "")
                if bitshape not in ["", TC.Tool.BitShape]:
                    self.squawk("PathSanity",
                                "Tool number {} used by multiple tools".format(
                                    TC.ToolNumber),
                                squawkType="CAUTION")
                tooldata['bitShape'] = TC.Tool.BitShape
                tooldata['description'] = TC.Tool.Label
                tooldata['manufacturer'] = ""
                tooldata['url'] = ""
                tooldata['inspectionNotes'] = ""
                tooldata['diameter'] = str(TC.Tool.Diameter)
                tooldata['shape'] = TC.Tool.ShapeName

                tooldata['partNumber'] = ""
                imagedata = TC.Tool.Proxy.getBitThumbnail(TC.Tool)
                imagepath = '{}/T{}.png'.format(self.outputpath, TC.ToolNumber)
                tooldata['feedrate'] = str(TC.HorizFeed)
                if TC.HorizFeed.Value == 0.0:
                    self.squawk("PathSanity",
                                "Tool Controller '{}' has no feedrate".format(
                                    TC.Label),
                                squawkType="WARNING")

                tooldata['spindlespeed'] = str(TC.SpindleSpeed)
                if TC.SpindleSpeed == 0.0:
                    self.squawk(
                        "PathSanity",
                        "Tool Controller '{}' has no spindlespeed".format(
                            TC.Label),
                        squawkType="WARNING")

                with open(imagepath, 'wb') as fd:
                    fd.write(imagedata)
                    fd.close()
                tooldata['imagepath'] = imagepath

                used = False
                for op in obj.Operations.Group:
                    if hasattr(op,
                               "ToolController") and op.ToolController is TC:
                        used = True
                        tooldata.setdefault('ops', []).append({
                            "Operation":
                            op.Label,
                            "ToolController":
                            TC.Label,
                            "Feed":
                            str(TC.HorizFeed),
                            "Speed":
                            str(TC.SpindleSpeed)
                        })

                if used is False:
                    tooldata.setdefault('ops', [])
                    self.squawk("PathSanity",
                                "Tool Controller '{}' is not used".format(
                                    TC.Label),
                                squawkType="WARNING")

        except Exception as e:
            data['errors'] = e
            self.squawk("PathSanity(__toolData)", e, squawkType="CAUTION")

        return data

    def __runData(self, obj):
        data = {
            'cycletotal': '',
            'jobMinZ': '',
            'jobMaxZ': '',
            'jobDescription': '',
            'items': []
        }
        try:
            data['cycletotal'] = str(obj.CycleTime)
            data['jobMinZ'] = FreeCAD.Units.Quantity(
                obj.Path.BoundBox.ZMin, FreeCAD.Units.Length).UserString
            data['jobMaxZ'] = FreeCAD.Units.Quantity(
                obj.Path.BoundBox.ZMax, FreeCAD.Units.Length).UserString
            data['jobDescription'] = obj.Description

            data['items'] = []
            for op in obj.Operations.Group:

                oplabel = op.Label
                ctime = op.CycleTime if hasattr(op, "CycleTime") else 0.0
                cool = op.CoolantMode if hasattr(op, "CoolantMode") else "N/A"

                o = op
                while len(o.ViewObject.claimChildren()) != 0:  # dressup
                    oplabel = "{}:{}".format(oplabel, o.Base.Label)
                    o = o.Base
                    if hasattr(o, 'CycleTime'):
                        ctime = o.CycleTime
                    cool = o.CoolantMode if hasattr(o, "CoolantMode") else cool

                if hasattr(op, 'Active') and not op.Active:
                    oplabel = "{} (INACTIVE)".format(oplabel)
                    ctime = 0.0

                if op.Path.BoundBox.isValid():
                    zmin = FreeCAD.Units.Quantity(
                        op.Path.BoundBox.ZMin, FreeCAD.Units.Length).UserString
                    zmax = FreeCAD.Units.Quantity(
                        op.Path.BoundBox.ZMax, FreeCAD.Units.Length).UserString
                else:
                    zmin = ''
                    zmax = ''

                opdata = {
                    "opName": oplabel,
                    "minZ": zmin,
                    "maxZ": zmax,
                    "cycleTime": ctime,
                    "coolantMode": cool
                }
                data['items'].append(opdata)

        except Exception as e:
            data['errors'] = e
            self.squawk("PathSanity(__runData)", e, squawkType="CAUTION")

        return data

    def __stockData(self, obj):
        data = {
            'xLen': '',
            'yLen': '',
            'zLen': '',
            'material': '',
            'stockImage': ''
        }

        try:
            bb = obj.Stock.Shape.BoundBox
            data['xLen'] = FreeCAD.Units.Quantity(
                bb.XLength, FreeCAD.Units.Length).UserString
            data['yLen'] = FreeCAD.Units.Quantity(
                bb.YLength, FreeCAD.Units.Length).UserString
            data['zLen'] = FreeCAD.Units.Quantity(
                bb.ZLength, FreeCAD.Units.Length).UserString

            data['material'] = "Not Specified"
            if hasattr(obj.Stock, 'Material'):
                if obj.Stock.Material is not None:
                    data['material'] = obj.Stock.Material.Material['Name']

            if data['material'] == "Not Specified":
                self.squawk("PathSanity",
                            "Consider Specifying the Stock Material",
                            squawkType="TIP")

            data['stockImage'] = self.__makePicture(obj.Stock, "stockImage")
        except Exception as e:
            data['errors'] = e
            self.squawk("PathSanity(__stockData)", e, squawkType="CAUTION")

        return data

    def __fixtureData(self, obj):
        data = {'fixtures': '', 'orderBy': '', 'datumImage': ''}
        try:
            data['fixtures'] = str(obj.Fixtures)
            data['orderBy'] = str(obj.OrderOutputBy)

            aview = FreeCADGui.activeDocument().activeView()
            aview.setAnimationEnabled(False)

            obj.Visibility = False
            obj.Operations.Visibility = False

            mw = FreeCADGui.getMainWindow()
            mdi = mw.findChild(QtGui.QMdiArea)
            view = mdi.activeSubWindow()
            view.showNormal()
            view.resize(320, 320)

            imagepath = '{}/origin'.format(self.outputpath)

            FreeCADGui.Selection.clearSelection()
            FreeCADGui.SendMsgToActiveView("PerspectiveCamera")
            aview.viewIsometric()
            for i in obj.Model.Group:
                FreeCADGui.Selection.addSelection(i)
            FreeCADGui.SendMsgToActiveView("ViewSelection")
            FreeCADGui.Selection.clearSelection()
            obj.ViewObject.Proxy.editObject(obj)
            aview.saveImage('{}.png'.format(imagepath), 320, 320, 'Current')
            aview.saveImage('{}_t.png'.format(imagepath), 320, 320,
                            'Transparent')
            obj.ViewObject.Proxy.uneditObject(obj)
            obj.Visibility = True
            obj.Operations.Visibility = True

            view.showMaximized()

            aview.setAnimationEnabled(True)
            data['datumImage'] = '{}_t.png'.format(imagepath)

        except Exception as e:
            data['errors'] = e
            self.squawk("PathSanity(__fixtureData)", e, squawkType="CAUTION")

        return data

    def __outputData(self, obj):
        data = {
            'lastpostprocess': '',
            'lastgcodefile': '',
            'optionalstops': '',
            'programmer': '',
            'machine': '',
            'postprocessor': '',
            'postprocessorFlags': '',
            'filesize': '',
            'linecount': ''
        }
        try:
            data['lastpostprocess'] = str(obj.LastPostProcessDate)
            data['lastgcodefile'] = str(obj.LastPostProcessOutput)
            data['optionalstops'] = "False"
            data['programmer'] = ""
            data['machine'] = ""
            data['postprocessor'] = str(obj.PostProcessor)
            data['postprocessorFlags'] = str(obj.PostProcessorArgs)

            for op in obj.Operations.Group:
                if isinstance(op.Proxy,
                              PathScripts.PathStop.Stop) and op.Stop is True:
                    data['optionalstops'] = "True"

            if obj.LastPostProcessOutput == "":
                data['filesize'] = str(0.0)
                data['linecount'] = str(0)
                self.squawk("PathSanity",
                            "The Job has not been post-processed")
            else:
                data['filesize'] = str(
                    os.path.getsize(obj.LastPostProcessOutput))
                data['linecount'] = str(
                    sum(1 for line in open(obj.LastPostProcessOutput)))

        except Exception as e:
            data['errors'] = e
            self.squawk("PathSanity(__outputData)", e, squawkType="CAUTION")

        return data
예제 #9
0
def resolveFileName(job, subpartname, sequencenumber):
    PathLog.track(subpartname, sequencenumber)

    validPathSubstitutions = ["D", "d", "M", "j"]
    validFilenameSubstitutions = ["j", "d", "T", "t", "W", "O", "S"]

    # Look for preference default
    outputpath, filename = os.path.split(PathPreferences.defaultOutputFile())
    filename, ext = os.path.splitext(filename)

    # Override with document default if it exists
    if job.PostProcessorOutputFile:
        matchstring = job.PostProcessorOutputFile
        candidateOutputPath, candidateFilename = os.path.split(matchstring)

        if candidateOutputPath:
            outputpath = candidateOutputPath

        if candidateFilename:
            filename, ext = os.path.splitext(candidateFilename)

    # Strip any invalid substitutions from the ouputpath
    for match in re.findall("%(.)", outputpath):
        if match not in validPathSubstitutions:
            outputpath = outputpath.replace(f"%{match}", "")

    # if nothing else, use current directory
    if not outputpath:
        outputpath = "."

    # Strip any invalid substitutions from the filename
    for match in re.findall("%(.)", filename):
        if match not in validFilenameSubstitutions:
            filename = filename.replace(f"%{match}", "")

    # if no filename, use the active document label
    if not filename:
        filename = FreeCAD.ActiveDocument.Label

    # if no extension, use something sensible
    if not ext:
        ext = ".nc"

    # By now we should have a sanitized path, filename and extension to work with
    PathLog.track(f"path: {outputpath} name: {filename} ext: {ext}")

    fullPath = processFileNameSubstitutions(
        job,
        subpartname,
        sequencenumber,
        outputpath,
        filename,
        ext,
    )

    # This section determines whether user interaction is necessary
    policy = PathPreferences.defaultOutputPolicy()

    openDialog = policy == "Open File Dialog"
    # if os.path.isdir(filename) or not os.path.isdir(os.path.dirname(filename)):
    #     # Either the entire filename resolves into a directory or the parent
    #     # directory doesn't exist.
    #     # Either way I don't know what to do - ask for help
    #     openDialog = True

    if not FreeCAD.GuiUp:  # if testing, or scripting, never open dialog.
        policy = "Append Unique ID on conflict"
        openDialog = False

    if os.path.isfile(fullPath) and not openDialog:
        if policy == "Open File Dialog on conflict":
            openDialog = True
        elif policy == "Append Unique ID on conflict":
            fn, ext = os.path.splitext(fullPath)
            nr = fn[-3:]
            n = 1
            if nr.isdigit():
                n = int(nr)
            while os.path.isfile("%s%03d%s" % (fn, n, ext)):
                n = n + 1
            fullPath = "%s%03d%s" % (fn, n, ext)

    if openDialog:
        foo = QtGui.QFileDialog.getSaveFileName(
            QtGui.QApplication.activeWindow(), "Output File", filename
        )
        if foo[0]:
            fullPath = foo[0]
        else:
            fullPath = None

    # remove any unused substitution strings:
    for s in validPathSubstitutions + validFilenameSubstitutions:
        fullPath = fullPath.replace(f"%{s}", "")

    fullPath = os.path.normpath(fullPath)
    PathLog.track(fullPath)
    return fullPath
예제 #10
0
def resolveFileName(job, subpartname, sequencenumber):
    PathLog.track(subpartname, sequencenumber)

    validPathSubstitutions = ["D", "d", "M", "j"]
    validFilenameSubstitutions = ["j", "d", "T", "t", "W", "O", "S"]

    # Look for preference default
    outputpath, filename = os.path.split(PathPreferences.defaultOutputFile())
    filename, ext = os.path.splitext(filename)

    # Override with document default if it exists
    if job.PostProcessorOutputFile:
        matchstring = job.PostProcessorOutputFile
        candidateOutputPath, candidateFilename = os.path.split(matchstring)

        if candidateOutputPath:
            outputpath = candidateOutputPath

        if candidateFilename:
            filename, ext = os.path.splitext(candidateFilename)

    # Strip any invalid substitutions from the ouputpath
    for match in re.findall("%(.)", outputpath):
        if match not in validPathSubstitutions:
            outputpath = outputpath.replace(f"%{match}", "")

    # if nothing else, use current directory
    if not outputpath:
        outputpath = "."

    # Strip any invalid substitutions from the filename
    for match in re.findall("%(.)", filename):
        if match not in validFilenameSubstitutions:
            filename = filename.replace(f"%{match}", "")

    # if no filename, use the active document label
    if not filename:
        filename = FreeCAD.ActiveDocument.Label

    # if no extension, use something sensible
    if not ext:
        ext = ".nc"

    # By now we should have a sanitized path, filename and extension to work with
    PathLog.track(f"path: {outputpath} name: {filename} ext: {ext}")

    # The following section allows substitution within the path part
    PathLog.track(f"path before substitution: {outputpath}")

    if "%D" in outputpath:  # Directory of active document
        D = FreeCAD.ActiveDocument.FileName
        if D:
            D = os.path.dirname(D)
            # in case the document is in the current working directory
            if not D:
                D = "."
        else:
            FreeCAD.Console.PrintError(
                "Please save document in order to resolve output path!\n"
            )
            return None
        outputpath = outputpath.replace("%D", D)

    if "%M" in outputpath:
        M = FreeCAD.getUserMacroDir()
        outputpath = outputpath.replace("%M", M)

    # Use the file label
    if "%d" in outputpath:
        d = FreeCAD.ActiveDocument.Label
        outputpath = outputpath.replace("%d", d)

    # Use the name of the active job object
    if "%j" in outputpath:
        j = job.Label
        outputpath = outputpath.replace("%j", j)

    PathLog.track(f"path after substitution: {outputpath}")

    # The following section allows substitution within the filename part
    PathLog.track(f"filename before substitution: {filename}")

    # Use the file label
    if "%d" in filename:
        d = FreeCAD.ActiveDocument.Label
        filename = filename.replace("%d", d)

    # Use the name of the active job object
    if "%j" in filename:
        j = job.Label
        filename = filename.replace("%j", j)

    # Use the sequnce number if explicitly called
    if "%S" in filename:
        j = job.Label
        filename = filename.replace("%S", str(sequencenumber))

    # This section handles unique names for splitting output
    if job.SplitOutput:
        PathLog.track()
        if "%T" in filename and job.OrderOutputBy == "Tool":
            filename = filename.replace("%T", subpartname)

        if "%t" in filename and job.OrderOutputBy == "Tool":
            filename = filename.replace("%t", subpartname)

        if "%W" in filename and job.OrderOutputBy == "Fixture":
            filename = filename.replace("%W", subpartname)

        if "%O" in filename and job.OrderOutputBy == "Operation":
            filename = filename.replace("%O", subpartname)

        if (
            "%S" in filename
        ):  # We always add a sequence number but the user can say where
            filename = filename.replace("%S", str(sequencenumber))
        else:
            filename = f"{filename}-{sequencenumber}"

    PathLog.track(f"filename after substitution: {filename}")

    if not ext:
        ext = ".nc"
    PathLog.track(f"file extension: {ext}")

    fullPath = f"{outputpath}{os.path.sep}{filename}{ext}"

    PathLog.track(f"full filepath: {fullPath}")

    # This section determines whether user interaction is necessary
    policy = PathPreferences.defaultOutputPolicy()

    openDialog = policy == "Open File Dialog"
    # if os.path.isdir(filename) or not os.path.isdir(os.path.dirname(filename)):
    #     # Either the entire filename resolves into a directory or the parent directory doesn't exist.
    #     # Either way I don't know what to do - ask for help
    #     openDialog = True

    if not FreeCAD.GuiUp:  # if testing, or scripting, never open dialog.
        policy = "Append Unique ID on conflict"
        openDialog = False

    if os.path.isfile(fullPath) and not openDialog:
        if policy == "Open File Dialog on conflict":
            openDialog = True
        elif policy == "Append Unique ID on conflict":
            fn, ext = os.path.splitext(fullPath)
            nr = fn[-3:]
            n = 1
            if nr.isdigit():
                n = int(nr)
            while os.path.isfile("%s%03d%s" % (fn, n, ext)):
                n = n + 1
            fullPath = "%s%03d%s" % (fn, n, ext)

    if openDialog:
        foo = QtGui.QFileDialog.getSaveFileName(
            QtGui.QApplication.activeWindow(), "Output File", filename
        )
        if foo[0]:
            fullPath = foo[0]
        else:
            fullPath = None

    # remove any unused substitution strings:
    for s in validPathSubstitutions + validFilenameSubstitutions:
        fullPath = fullPath.replace(f"%{s}", "")

    fullPath = os.path.normpath(fullPath)
    PathLog.track(fullPath)
    return fullPath