diff --git a/src/Mod/CAM/CMakeLists.txt b/src/Mod/CAM/CMakeLists.txt index f60574ded5..bb763cfcd9 100644 --- a/src/Mod/CAM/CMakeLists.txt +++ b/src/Mod/CAM/CMakeLists.txt @@ -94,14 +94,21 @@ SET(PathPythonMainGui_SRCS Path/Main/Gui/JobCmd.py Path/Main/Gui/JobDlg.py Path/Main/Gui/PreferencesJob.py - Path/Main/Gui/Sanity.py - Path/Main/Gui/Sanity_Bulb.svg - Path/Main/Gui/Sanity_Caution.svg - Path/Main/Gui/Sanity_Note.svg - Path/Main/Gui/Sanity_Warning.svg + Path/Main/Gui/SanityCmd.py Path/Main/Gui/Simulator.py ) +SET(PathPythonMainSanity_SRCS + Path/Main/Sanity/Sanity.py + Path/Main/Sanity/ImageBuilder.py + Path/Main/Sanity/ReportGenerator.py + Path/Main/Sanity/HTMLTemplate.py + Path/Main/Sanity/Sanity_Bulb.svg + Path/Main/Sanity/Sanity_Caution.svg + Path/Main/Sanity/Sanity_Note.svg + Path/Main/Sanity/Sanity_Warning.svg +) + SET(PathPythonTools_SRCS Path/Tool/__init__.py Path/Tool/Bit.py @@ -289,6 +296,7 @@ SET(Tests_SRCS Tests/test_filenaming.fcstd Tests/test_geomop.fcstd Tests/test_holes00.fcstd + Tests/TestCAMSanity.py Tests/TestCentroidPost.py Tests/TestGrblPost.py Tests/TestLinuxCNCPost.py @@ -376,6 +384,7 @@ SET(all_files ${PathPythonDressupGui_SRCS} ${PathPythonMain_SRCS} ${PathPythonMainGui_SRCS} + ${PathPythonMainSanity_SRCS} ${PathPythonOp_SRCS} ${PathPythonOpGui_SRCS} ${PathPythonPost_SRCS} @@ -468,6 +477,12 @@ INSTALL( DESTINATION Mod/CAM/Path/Main/Gui ) +INSTALL( + FILES + ${PathPythonMainSanity_SRCS} + DESTINATION + Mod/CAM/Path/Main/Sanity +) INSTALL( FILES diff --git a/src/Mod/CAM/InitGui.py b/src/Mod/CAM/InitGui.py index d8b7979f45..515ac7adec 100644 --- a/src/Mod/CAM/InitGui.py +++ b/src/Mod/CAM/InitGui.py @@ -75,6 +75,7 @@ class CAMWorkbench(Workbench): import Path.GuiInit from Path.Main.Gui import JobCmd as PathJobCmd + from Path.Main.Gui import SanityCmd as SanityCmd from Path.Tool.Gui import BitCmd as PathToolBitCmd from Path.Tool.Gui import BitLibraryCmd as PathToolBitLibraryCmd diff --git a/src/Mod/CAM/Path/GuiInit.py b/src/Mod/CAM/Path/GuiInit.py index 545bcf8ece..2655ef5818 100644 --- a/src/Mod/CAM/Path/GuiInit.py +++ b/src/Mod/CAM/Path/GuiInit.py @@ -52,9 +52,10 @@ def Startup(): from Path.Main.Gui import Fixture from Path.Main.Gui import Inspect - from Path.Main.Gui import Sanity from Path.Main.Gui import Simulator + from Path.Main.Sanity import Sanity + from Path.Op.Gui import Adaptive from Path.Op.Gui import Array from Path.Op.Gui import Comment diff --git a/src/Mod/CAM/Path/Main/Gui/Job.py b/src/Mod/CAM/Path/Main/Gui/Job.py index 67ecb9a147..f4037e3003 100644 --- a/src/Mod/CAM/Path/Main/Gui/Job.py +++ b/src/Mod/CAM/Path/Main/Gui/Job.py @@ -121,29 +121,91 @@ class ViewProvider: if not hasattr(self, "stockVisibility"): self.stockVisibility = False - # setup the axis display at the origin + # Setup the axis display at the origin self.switch = coin.SoSwitch() self.sep = coin.SoSeparator() self.axs = coin.SoType.fromName("SoAxisCrossKit").createInstance() - self.axs.set("xHead.transform", "scaleFactor 2 3 2") - self.axs.set("yHead.transform", "scaleFactor 2 3 2") - self.axs.set("zHead.transform", "scaleFactor 2 3 2") + + #Adjust the axis heads if needed, the scale here is just for the head + self.axs.set("xHead.transform", "scaleFactor 1.5 1.5 1") + self.axs.set("yHead.transform", "scaleFactor 1.5 1.5 1") + self.axs.set("zHead.transform", "scaleFactor 1.5 1.5 1") + + # Adjust the axis heads if needed, the scale here is just for the head + self.axs.set("xHead.transform", "translation 50 0 0") + self.axs.set("yHead.transform", "translation 0 50 0") + self.axs.set("zHead.transform", "translation 0 0 50") + + # Adjust the axis line width if needed + self.axs.set("xAxis.transform", "scaleFactor 0.5 0.5 1") + self.axs.set("xAxis.appearance.drawStyle", "lineWidth 9") + self.axs.set("yAxis.transform", "scaleFactor 0.5 0.5 1") + self.axs.set("yAxis.appearance.drawStyle", "lineWidth 9") + self.axs.set("zAxis.transform", "scaleFactor 0.5 0.5 1") + self.axs.set("zAxis.appearance.drawStyle", "lineWidth 9") + self.sca = coin.SoType.fromName("SoShapeScale").createInstance() self.sca.setPart("shape", self.axs) - self.sca.scaleFactor.setValue(0.5) + self.sca.scaleFactor.setValue(1) # Keep or adjust if needed + self.mat = coin.SoMaterial() - self.mat.diffuseColor = coin.SbColor(0.9, 0, 0.9) - self.mat.transparency = 0.85 + # Set sphere color to bright yellow + self.mat.diffuseColor = coin.SbColor(1, 1, 0) + self.mat.transparency = 0.35 # Keep or adjust if needed + self.sph = coin.SoSphere() self.scs = coin.SoType.fromName("SoShapeScale").createInstance() self.scs.setPart("shape", self.sph) - self.scs.scaleFactor.setValue(10) + # Increase the scaleFactor to make the sphere larger + self.scs.scaleFactor.setValue(10) # Adjust this value as needed + self.sep.addChild(self.sca) self.sep.addChild(self.mat) self.sep.addChild(self.scs) + self.switch.addChild(self.sep) + self.switch.addChild(self.sep) vobj.RootNode.addChild(self.switch) - self.showOriginAxis(False) + self.showOriginAxis(True) + + for base in self.obj.Model.Group: + Path.Log.debug(f"{base.Name}: {base.ViewObject.Visibility}") + + + def onChanged(self, vobj, prop): + if prop == "Visibility": + self.showOriginAxis(vobj.Visibility) + if vobj.Visibility: + self.rememberStockVisibility() + self.obj.Stock.ViewObject.Visibility = True + + self.KeepBaseVisibility() + for base in self.obj.Model.Group: + base.ViewObject.Visibility = True + else: + self.restoreStockVisibility() + self.RestoreBaseVisibility() + + def rememberStockVisibility(self): + self.stockVisibility = self.obj.Stock.ViewObject.Visibility + + def restoreStockVisibility(self): + self.obj.Stock.ViewObject.Visibility = self.stockVisibility + + def KeepBaseVisibility(self): + Path.Log.debug("KeepBaseVisibility") + self.visibilitystate = {} + for base in self.obj.Model.Group: + Path.Log.debug(f"{base.Name}: {base.ViewObject.Visibility}") + self.visibilitystate[base.Name] = base.ViewObject.Visibility + Path.Log.debug(self.visibilitystate) + + def RestoreBaseVisibility(self): + Path.Log.debug("RestoreBaseVisibility") + for base in self.obj.Model.Group: + base.ViewObject.Visibility = self.visibilitystate[base.Name] + Path.Log.debug(self.visibilitystate) + def showOriginAxis(self, yes): sw = coin.SO_SWITCH_ALL if yes else coin.SO_SWITCH_NONE @@ -252,11 +314,11 @@ class ViewProvider: def forgetBaseVisibility(self, obj, base): Path.Log.track() - if self.baseVisibility.get(base.Name): - visibility = self.baseVisibility[base.Name] - visibility[0].ViewObject.Visibility = visibility[1] - visibility[2].ViewObject.Visibility = visibility[3] - del self.baseVisibility[base.Name] + # if self.baseVisibility.get(base.Name): + # visibility = self.baseVisibility[base.Name] + # visibility[0].ViewObject.Visibility = visibility[1] + # visibility[2].ViewObject.Visibility = visibility[3] + # del self.baseVisibility[base.Name] def setupEditVisibility(self, obj): Path.Log.track() diff --git a/src/Mod/CAM/Path/Main/Gui/Sanity.py b/src/Mod/CAM/Path/Main/Gui/Sanity.py deleted file mode 100644 index 8eb9e8e888..0000000000 --- a/src/Mod/CAM/Path/Main/Gui/Sanity.py +++ /dev/null @@ -1,1622 +0,0 @@ -# -*- coding: utf-8 -*- -# *************************************************************************** -# * Copyright (c) 2016 sliptonic * -# * * -# * This file is part of the FreeCAD CAx development system. * -# * * -# * 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. * -# * * -# * FreeCAD 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 Lesser General Public License for more details. * -# * * -# * You should have received a copy of the GNU Library General Public * -# * License along with FreeCAD; if not, write to the Free Software * -# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * -# * USA * -# * * -# *************************************************************************** - -""" -This file has utilities for checking and catching common errors in FreeCAD -Path projects. Ideally, the user could execute these utilities from an icon -to make sure tools are selected and configured and defaults have been revised -""" - -from PySide import QtGui -import FreeCAD -import FreeCADGui -import Path -import Path.Log -from collections import Counter -from datetime import datetime -import codecs -import os -import time -import webbrowser -import subprocess -from PySide.QtCore import QT_TRANSLATE_NOOP - -translate = FreeCAD.Qt.translate - -if False: - 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 CommandCAMSanity: - def resolveOutputFile(self, job): - if job.PostProcessorOutputFile != "": - filepath = job.PostProcessorOutputFile - elif Path.Preferences.defaultOutputFile() != "": - filepath = Path.Preferences.defaultOutputFile() - else: - filepath = Path.Preferences.macroFilePath() - - Path.Log.debug(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 + os.path.sep) - - filepath = filepath.replace("%d", FreeCAD.ActiveDocument.Label) - - filepath = filepath.replace("%j", job.Label) - - if "%M" in filepath: - pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Macro") - M = pref.GetString("MacroPath", FreeCAD.getUserAppDataDir()) - filepath = filepath.replace("%M", M + os.path.sep) - - # strip out all substitutions related to output splitting - for elem in ["%O", "%W", "%T", "%t", "%S"]: - filepath = filepath.replace(elem, "") - - Path.Log.debug("filepath: {}".format(filepath)) - - # if there's no basename left, use the activedocument basename - fname = os.path.splitext(os.path.basename(filepath)) - if fname[0] == "": - final = os.path.splitext(os.path.basename(FreeCAD.ActiveDocument.FileName))[ - 0 - ] - filepath = os.path.dirname(filepath) + os.path.sep + final + fname[1] - - Path.Log.debug("filepath: {}".format(filepath)) - - return filepath - - def resolveOutputPath(self, filepath): - - # Make sure the filepath is fully qualified - if os.path.basename(filepath) == filepath: - filepath = f"{os.path.dirname(FreeCAD.ActiveDocument.FileName)}/{filepath}" - - Path.Log.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) - - Path.Log.debug("filepath: {}".format(filepath)) - return filepath + os.sep - - def GetResources(self): - return { - "Pixmap": "CAM_Sanity", - "MenuText": QT_TRANSLATE_NOOP( - "CAM_Sanity", "Check the CAM job for common errors" - ), - "Accel": "P, S", - "ToolTip": QT_TRANSLATE_NOOP( - "CAM_Sanity", "Check the CAM job for common errors" - ), - } - - def IsActive(self): - obj = FreeCADGui.Selection.getSelectionEx()[0].Object - return isinstance(obj.Proxy, Path.Main.Job.ObjectJob) - - def Activated(self): - # if everything is ok, execute - - if FreeCAD.GuiUp: - currentCamera = FreeCADGui.ActiveDocument.ActiveView.getCameraType() - if currentCamera != "Perspective": - FreeCADGui.SendMsgToActiveView("PerspectiveCamera") - FreeCADGui.updateGui() - time.sleep(4) - FreeCAD.Console.PrintLog( - "Path - Sanity - Changing to Perspective Camera temporarily\n" - ) - FreeCADGui.addIconPath(":/icons") - self.squawkData = {"items": []} - obj = FreeCADGui.Selection.getSelectionEx()[0].Object - self.outputFile = self.resolveOutputFile(obj) - self.outputpath = self.resolveOutputPath(self.outputFile) - Path.Log.debug(f"outputstring: {self.outputpath}") - data = self.__summarize(obj) - html = self.__report(data, obj) - if html is not None: - webbrowser.open(html) - if FreeCAD.GuiUp: - if currentCamera != "Perspective": - FreeCADGui.SendMsgToActiveView("OrthographicCamera") - FreeCADGui.updateGui() - time.sleep(0.5) - FreeCAD.Console.PrintLog( - "Path - Sanity - Changing back to Orthographic Camera\n" - ) - - 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, obj): - """ - generates an asciidoc file with the report information - """ - Title = translate("CAM_Sanity", "Setup Report for FreeCAD Job") - ToC = translate("CAM_Sanity", "Table of Contents") - PartInfoHeading = translate("CAM_Sanity", "Part Information") - RunSumHeading = translate("CAM_Sanity", "Run Summary") - RoughStkHeading = translate("CAM_Sanity", "Rough Stock") - ToolDataHeading = translate("CAM_Sanity", "Tool Data") - OutputHeading = translate("CAM_Sanity", "Output") - FixturesHeading = translate("CAM_Sanity", "Fixtures") - SquawksHeading = translate("CAM_Sanity", "Squawks") - - PartLabel = translate("CAM_Sanity", "Base Object(s)") - SequenceLabel = translate("CAM_Sanity", "Job Sequence") - DescriptionLabel = translate("CAM_Sanity", "Job Description") - JobTypeLabel = translate("CAM_Sanity", "Job Type") - CADLabel = translate("CAM_Sanity", "CAD File Name") - LastSaveLabel = translate("CAM_Sanity", "Last Save Date") - CustomerLabel = translate("CAM_Sanity", "Customer") - DesignerLabel = translate("CAM_Sanity", "Designer") - - b = data["baseData"] - d = data["designData"] - jobname = d["JobLabel"] - - opLabel = translate("CAM_Sanity", "Operation") - zMinLabel = translate("CAM_Sanity", "Minimum Z Height") - zMaxLabel = translate("CAM_Sanity", "Maximum Z Height") - cycleTimeLabel = translate("CAM_Sanity", "Cycle Time") - - coolantLabel = translate("CAM_Sanity", "Coolant") - jobTotalLabel = translate("CAM_Sanity", "TOTAL JOB") - d = data["toolData"] - toolLabel = translate("CAM_Sanity", "Tool Number") - imageCounter = 1 - reportHtmlTemplate = """ - - - - - - Setup Report for FreeCAD Job: Path Special - - - -

-""" - reportHtmlTemplate += Title + ": " + jobname - reportHtmlTemplate += """ -

-

-
- -

-

- -""" - reportHtmlTemplate += ToC - reportHtmlTemplate += """ -

-
-
-

- - -""" - reportHtmlTemplate += PartInfoHeading - reportHtmlTemplate += """ -

- -

-
-
- -

-

- -""" - reportHtmlTemplate += PartInfoHeading - reportHtmlTemplate += """ -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- """ - reportHtmlTemplate += PartLabel - reportHtmlTemplate += """ -

-
- - - - """ - d = data["designData"] - for key, val in b["bases"].items(): - reportHtmlTemplate += """ - - - - """ - reportHtmlTemplate += """ -

- """ - reportHtmlTemplate += key - reportHtmlTemplate += """ -

-

- """ - reportHtmlTemplate += val - reportHtmlTemplate += """ -

-
-


- -

-

- Base Object(s)

-

- """ - reportHtmlTemplate += SequenceLabel - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += d["Sequence"] - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += JobTypeLabel - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += d["JobType"] - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += DescriptionLabel - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += d["JobDescription"] - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += CADLabel - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += d["FileName"] - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += LastSaveLabel - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += d["LastModifiedDate"] - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += CustomerLabel - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += d["Customer"] - reportHtmlTemplate += """

-
""" - - descriptionLabel = translate("CAM_Sanity", "Description") - manufLabel = translate("CAM_Sanity", "Manufacturer") - partNumberLabel = translate("CAM_Sanity", "Part Number") - urlLabel = translate("CAM_Sanity", "URL") - inspectionNotesLabel = translate("CAM_Sanity", "Inspection Notes") - opLabel = translate("CAM_Sanity", "Operation") - tcLabel = translate("CAM_Sanity", "Tool Controller") - feedLabel = translate("CAM_Sanity", "Feed Rate") - speedLabel = translate("CAM_Sanity", "Spindle Speed") - shapeLabel = translate("CAM_Sanity", "Tool Shape") - diameterLabel = translate("CAM_Sanity", "Tool Diameter") - - xDimLabel = translate("CAM_Sanity", "X Size") - yDimLabel = translate("CAM_Sanity", "Y Size") - zDimLabel = translate("CAM_Sanity", "Z Size") - materialLabel = translate("CAM_Sanity", "Material") - - offsetsLabel = translate("CAM_Sanity", "Work Offsets") - orderByLabel = translate("CAM_Sanity", "Order By") - datumLabel = translate("CAM_Sanity", "Part Datum") - - gcodeFileLabel = translate("CAM_Sanity", "G-code File") - lastpostLabel = translate("CAM_Sanity", "Last Post Process Date") - stopsLabel = translate("CAM_Sanity", "Stops") - programmerLabel = translate("CAM_Sanity", "Programmer") - machineLabel = translate("CAM_Sanity", "Machine") - postLabel = translate("CAM_Sanity", "Postprocessor") - flagsLabel = translate("CAM_Sanity", "Post Processor Flags") - fileSizeLabel = translate("CAM_Sanity", "File Size (kB)") - lineCountLabel = translate("CAM_Sanity", "Line Count") - - noteLabel = translate("CAM_Sanity", "Note") - operatorLabel = translate("CAM_Sanity", "Operator") - dateLabel = translate("CAM_Sanity", "Date") - - d = data["runData"] - reportHtmlTemplate += """ -

- -""" - reportHtmlTemplate += RunSumHeading - reportHtmlTemplate += """ -

""" - for i in d["items"]: - reportHtmlTemplate += """ - - - - - - - - - - - - - - - - - - - - - - - - - - -

- """ - reportHtmlTemplate += opLabel - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += zMinLabel - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += zMaxLabel - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += coolantLabel - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += cycleTimeLabel - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += i["opName"] - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += i["minZ"] - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += i["maxZ"] - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += i["coolantMode"] - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += i["cycleTime"] - reportHtmlTemplate += """ -

-
""" - d = data["stockData"] - reportHtmlTemplate += """ -

- -""" - reportHtmlTemplate += RoughStkHeading - reportHtmlTemplate += """ -

- - - - - - - - - - - - - - - - - - - - - - - -

- """ - reportHtmlTemplate += materialLabel - reportHtmlTemplate += """ -

-

- """ - reportHtmlTemplate += d["material"] + "

" - reportHtmlTemplate += """ -

- stock" - ) - imageCounter += 1 - reportHtmlTemplate += """ -

-

- """ - reportHtmlTemplate += xDimLabel - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += d["xLen"] - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += yDimLabel - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += d["yLen"] - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += zDimLabel - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += d["zLen"] - reportHtmlTemplate += """

-
""" - d = data["toolData"] - reportHtmlTemplate += """ -

- -""" - reportHtmlTemplate += ToolDataHeading - reportHtmlTemplate += """ -

""" - for key, value in d.items(): - reportHtmlTemplate += """ -

" - reportHtmlTemplate += """ - -""" - reportHtmlTemplate += toolLabel + ": T" + key - reportHtmlTemplate += """

""" - reportHtmlTemplate += """ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- """ - reportHtmlTemplate += descriptionLabel - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += value["description"] - reportHtmlTemplate += """ -

-

- 2" - ) - reportHtmlTemplate += """ -

-

- """ - reportHtmlTemplate += manufLabel - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += value["manufacturer"] - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += partNumberLabel - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += value["partNumber"] - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += urlLabel - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += value["url"] - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += shapeLabel - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += value["shape"] - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += inspectionNotesLabel - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += value["inspectionNotes"] - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += diameterLabel - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += value["diameter"] - imageCounter += 1 - reportHtmlTemplate += """

-
""" - for o in value["ops"]: - reportHtmlTemplate += """ - - - - - - - - - - - - - - - - - - - - - -

- """ - reportHtmlTemplate += opLabel - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += tcLabel - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += feedLabel - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += speedLabel - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += o["Operation"] - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += o["ToolController"] - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += o["Feed"] - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += o["Speed"] - reportHtmlTemplate += """

-
""" - d = data["outputData"] - reportHtmlTemplate += """ -

- -""" - reportHtmlTemplate += OutputHeading + " (Gcode)" - reportHtmlTemplate += """ -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- """ - reportHtmlTemplate += gcodeFileLabel - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += d["lastgcodefile"] - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += lastpostLabel - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += d["lastpostprocess"] - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += stopsLabel - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += d["optionalstops"] - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += programmerLabel - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += d["programmer"] - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += machineLabel - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += d["machine"] - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += postLabel - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += d["postprocessor"] - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += flagsLabel - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += d["postprocessorFlags"] - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += fileSizeLabel - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += d["filesize"] - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += lineCountLabel - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += d["linecount"] - reportHtmlTemplate += """

-
""" - d = data["fixtureData"] - reportHtmlTemplate += """ -

- -""" - reportHtmlTemplate += FixturesHeading - reportHtmlTemplate += """ -

- - - - - - - - - - - - - - - - - -

- """ - reportHtmlTemplate += offsetsLabel - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += d["fixtures"] - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += orderByLabel - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += d["orderBy"] - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += datumLabel - reportHtmlTemplate += """

-

- origin t

-
""" - d = data["squawkData"] - TIPIcon = FreeCAD.getHomePath() + "Mod/CAM/Path/Main/Gui/Sanity_Bulb.svg" - NOTEIcon = FreeCAD.getHomePath() + "Mod/CAM/Path/Main/Gui/Sanity_Note.svg" - WARNINGIcon = FreeCAD.getHomePath() + "Mod/CAM/Path/Main/Gui/Sanity_Warning.svg" - CAUTIONIcon = FreeCAD.getHomePath() + "Mod/CAM/Path/Main/Gui/Sanity_Stop.svg" - - reportHtmlTemplate += """ -

- -""" - reportHtmlTemplate += SquawksHeading - reportHtmlTemplate += """ -

- - - - - - - - - - - - """ - for i in d["items"]: - reportHtmlTemplate += """ - - - - - """ - reportHtmlTemplate += """ -

- """ - reportHtmlTemplate += noteLabel - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += operatorLabel - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += dateLabel - reportHtmlTemplate += """

-
- - - - -

- """ - if str(i["squawkType"]) == "TIP": - reportHtmlTemplate += ( - "TIP" - + "

" - + str(i["Note"]) - ) - if str(i["squawkType"]) == "NOTE": - reportHtmlTemplate += ( - "NOTE" - + "

" - + str(i["Note"]) - ) - if str(i["squawkType"]) == "WARNING": - reportHtmlTemplate += ( - "WARNING" - + "

" - + str(i["Note"]) - ) - if str(i["squawkType"]) == "CAUTION": - reportHtmlTemplate += ( - "CAUTION" - + "

" - + str(i["Note"]) - ) - imageCounter += 1 - reportHtmlTemplate += """

-
-

- """ - reportHtmlTemplate += i["Operator"] - reportHtmlTemplate += """

-

- """ - reportHtmlTemplate += i["Date"] - reportHtmlTemplate += """

-
-


- -

- - -""" - - # Save the report - subsLookup = os.path.splitext(os.path.basename(obj.PostProcessorOutputFile))[0] - foundSub = False - - for elem in ["%D", "%d", "%M", "%j"]: - if elem in subsLookup: - foundSub = True - break - - if foundSub: - filepath = self.resolveOutputFile(obj) - Path.Log.debug("filepath: {}".format(filepath)) - - # Make sure the filepath is fully qualified - if os.path.basename(filepath) == filepath: - filepath = ( - f"{os.path.dirname(FreeCAD.ActiveDocument.FileName)}/{filepath}" - ) - Path.Log.debug("filepath: {}".format(filepath)) - base_name = os.path.splitext(filepath)[0] - reporthtml = base_name + ".html" - else: - reporthtml = ( - self.outputpath + data["outputData"]["outputfilename"] + ".html" - ) - - # Python 3.11 aware - with codecs.open(reporthtml, encoding="utf-8", mode="w") as fd: - fd.write(reportHtmlTemplate) - fd.close() - FreeCAD.Console.PrintMessage("html file written to {}\n".format(reporthtml)) - - 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 Counter( - [obj.Proxy.baseObject(obj, o).Label for o in obj.Model.Group] - ).items(): - bases[name] = str(count) - - data["baseimage"] = self.__makePicture(obj.Model, "baseimage") - data["bases"] = bases - - except Exception as e: - data["errors"] = e - self.squawk("CAMSanity(__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, Path.Main.Job.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("CAMSanity(__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.Tools.Group: - if not hasattr(TC.Tool, "BitBody"): - self.squawk( - "CAMSanity", - translate( - "CAM_Sanity", - "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( - "CAMSanity", - translate( - "CAM_Sanity", "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"] = "" - - if os.path.isfile(TC.Tool.BitShape): - imagedata = TC.Tool.Proxy.getBitThumbnail(TC.Tool) - else: - imagedata = None - self.squawk( - "CAMSanity", - translate( - "CAM_Sanity", "Toolbit Shape for TC: {} not found" - ).format(TC.ToolNumber), - squawkType="WARNING", - ) - imagepath = "{}T{}.png".format(self.outputpath, TC.ToolNumber) - tooldata["feedrate"] = str(TC.HorizFeed) - if TC.HorizFeed.Value == 0.0: - self.squawk( - "CAMSanity", - translate( - "CAM_Sanity", "Tool Controller '{}' has no feedrate" - ).format(TC.Label), - squawkType="WARNING", - ) - - tooldata["spindlespeed"] = str(TC.SpindleSpeed) - if TC.SpindleSpeed == 0.0: - self.squawk( - "CAMSanity", - translate( - "CAM_Sanity", "Tool Controller '{}' has no spindlespeed" - ).format(TC.Label), - squawkType="WARNING", - ) - - if imagedata is not None: - 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( - "CAMSanity", - translate( - "CAM_Sanity", "Tool Controller '{}' is not used" - ).format(TC.Label), - squawkType="WARNING", - ) - - except Exception as e: - raise e - data["errors"] = e - self.squawk("CAMSanity(__toolData)", e, squawkType="CAUTION") - - print(data) - 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 "00:00:00" - 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 = "00:00:00" - - 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("CAMSanity(__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( - "CAMSanity", - translate("CAM_Sanity", "Consider Specifying the Stock Material"), - squawkType="TIP", - ) - - data["stockImage"] = self.__makePicture(obj.Stock, "stockImage") - except Exception as e: - data["errors"] = e - self.squawk("CAMSanity(__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) - Path.Log.debug(imagepath) - 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("CAMSanity(__fixtureData)", e, squawkType="CAUTION") - - return data - - def __outputData(self, obj): - data = { - "lastpostprocess": "", - "lastgcodefile": "", - "optionalstops": "", - "programmer": "", - "machine": "", - "postprocessor": "", - "postprocessorFlags": "", - "filesize": "", - "linecount": "", - "outputfilename": "setupreport", - } - 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) - - if obj.PostProcessorOutputFile != "": - fname = obj.PostProcessorOutputFile - data["outputfilename"] = os.path.splitext(os.path.basename(fname))[0] - for op in obj.Operations.Group: - if isinstance(op.Proxy, Path.Op.Gui.Stop.Stop) and op.Stop is True: - data["optionalstops"] = "True" - - if obj.LastPostProcessOutput == "": - data["filesize"] = str(0.0) - data["linecount"] = str(0) - self.squawk( - "CAMSanity", - translate("CAM_Sanity", "The Job has not been post-processed"), - ) - else: - data["filesize"] = str( - os.path.getsize(obj.LastPostProcessOutput) / 1000 - ) - data["linecount"] = str( - sum(1 for line in open(obj.LastPostProcessOutput)) - ) - - except Exception as e: - data["errors"] = e - self.squawk("CAMSanity(__outputData)", e, squawkType="CAUTION") - - return data - - -if FreeCAD.GuiUp: - # register the FreeCAD command - FreeCADGui.addCommand("CAM_Sanity", CommandCAMSanity()) diff --git a/src/Mod/CAM/Path/Main/Gui/SanityCmd.py b/src/Mod/CAM/Path/Main/Gui/SanityCmd.py new file mode 100644 index 0000000000..469cc44a39 --- /dev/null +++ b/src/Mod/CAM/Path/Main/Gui/SanityCmd.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2016 sliptonic * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * 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. * +# * * +# * FreeCAD 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 Lesser General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with FreeCAD; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +""" +This file has the GUI command for checking and catching common errors in FreeCAD +CAM projects. +""" + +from Path.Main.Sanity import Sanity +from PySide.QtCore import QT_TRANSLATE_NOOP +from PySide.QtGui import QFileDialog +import FreeCAD +import FreeCADGui +import Path +import Path.Log +import os +import webbrowser + +translate = FreeCAD.Qt.translate + +if False: + 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 CommandCAMSanity: + def GetResources(self): + return { + "Pixmap": "CAM_Sanity", + "MenuText": QT_TRANSLATE_NOOP( + "CAM_Sanity", "Check the CAM job for common errors" + ), + "Accel": "P, S", + "ToolTip": QT_TRANSLATE_NOOP( + "CAM_Sanity", "Check the CAM job for common errors" + ), + } + + def IsActive(self): + obj = FreeCADGui.Selection.getSelectionEx()[0].Object + return isinstance(obj.Proxy, Path.Main.Job.ObjectJob) + + def Activated(self): + FreeCADGui.addIconPath(":/icons") + obj = FreeCADGui.Selection.getSelectionEx()[0].Object + + + # Ask the user for a filename to save the report to + + file_location = QFileDialog.getSaveFileName( + None, + translate("Path", "Save Sanity Check Report"), + os.path.expanduser("~"), + "HTML files (*.html)", + )[0] + + sanity_checker = Sanity.CAMSanity(obj, file_location) + html = sanity_checker.get_output_report() + + if html is None: + Path.Log.error("Sanity check failed. No report generated.") + return + + with open(file_location, "w") as fp: + fp.write(html) + + FreeCAD.Console.PrintMessage( + "Sanity check report written to: {}\n".format(file_location) + ) + + webbrowser.open_new_tab(file_location) + + +if FreeCAD.GuiUp: + # register the FreeCAD command + FreeCADGui.addCommand("CAM_Sanity", CommandCAMSanity()) diff --git a/src/Mod/CAM/Path/Main/Sanity/HTMLTemplate.py b/src/Mod/CAM/Path/Main/Sanity/HTMLTemplate.py new file mode 100644 index 0000000000..cdbe59223d --- /dev/null +++ b/src/Mod/CAM/Path/Main/Sanity/HTMLTemplate.py @@ -0,0 +1,543 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2024 Ondsel * +# * * +# * 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 * +# * * +# *************************************************************************** +from string import Template +""" +This module contains the HTML template for the CAM Sanity report. +""" + +html_template = Template( + """ + + + + + Setup Report for FreeCAD Job: Path Special + + + +

${headingLabel}: ${JobLabel}

+ + + +

${partInformationLabel}

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ${PartLabel} + + + + + + + ${bases} +
+
+ ${baseimage} +
+ ${SequenceLabel} + + ${Sequence} +
+ ${JobTypeLabel} + + ${JobType} +
+ ${CADLabel} + + ${FileName} +
+ ${LastSaveLabel} + + ${LastModifiedDate} +
+ ${CustomerLabel} + + ${Customer} +
+ +

${runSummaryLabel}

+ + + + + + + + + + + + + + + + ${run_summary_ops} +
+ ${opLabel} + + ${jobMinZLabel} + + ${jobMaxZLabel} + + ${coolantLabel} + + ${cycleTimeLabel} +
+ +

${roughStockLabel}

+ + + + + + + + + + + + + + + + + + + + + + + +
+ ${materialLabel} + + ${material} + + ${stockImage} +
+ ${xDimLabel} + + ${xLen} +
+ ${yDimLabel} + + ${yLen} +
+ ${zDimLabel} + + ${zLen} +
+ + +

${toolDataLabel}

+${tool_data} + + +

${outputLabel}

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ${gcodeFileLabel} + + ${lastgcodefile} +
+ ${lastpostLabel} + + ${lastpostprocess} +
+ ${stopsLabel} + + ${optionalstops} +
+ ${programmerLabel} + + ${programmer} +
+ ${machineLabel} + + ${machine} +
+ ${postLabel} + + ${postprocessor} +
+ ${flagsLabel} + + ${postprocessorFlags} +
+ ${fileSizeLabel} + + ${filesize} +
+ ${lineCountLabel} + + ${linecount} +
+ +

${fixturesLabel}

+ + + + + + + + + + + + + + + + + +
+ ${offsetsLabel} + + ${fixtures} +
+ ${orderByLabel} + + ${orderBy} +
+ ${datumLabel} + + ${datumImage} +
+ +

${squawksLabel}

+ + + + + + + + + + + + + ${squawks} + +
+ ${noteLabel} + + ${operatorLabel} + + ${dateLabel} + + ${noteLabel} +
+ +


+ +

+ + +""" +) + +base_template = Template( + """ + + + %{key} + + + %{val} + + + """ +) + +squawk_template = Template( + """ + + + ${squawkIcon} + + + ${Operator} + + + ${Date} + + + ${Note} + + + """ +) + +tool_template = Template( + """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ${descriptionLabel} + + ${description} + + ${imagepath} +
+ ${manufLabel} + + ${manufacturer} +
+ ${partNumberLabel} + + ${partNumber} +
+ ${urlLabel} + + ${url} +
+ ${shapeLabel} + + ${shape} +
+ ${inspectionNotesLabel} + + ${inspectionNotes} +
+ ${diameterLabel} + + ${diameter} +
+${ops} + """ +) + +op_tool_template = Template( + """ + + + + + + + + + + + + + + + + + + + +
+ ${opLabel} + + ${tcLabel} + + ${feedLabel} + + ${speedLabel} +
+ ${Operation} + + ${ToolController} + + ${Feed} + + ${Speed} +
+ """ +) + +op_run_template = Template( + """ + + + ${opName} + + + ${minZ} + + + ${maxZ} + + + ${coolantMode} + + + ${cycleTime} + + + """ +) + +tool_item_template = Template( + """ +
  • T${toolNumber}-${description}
  • + """ +) diff --git a/src/Mod/CAM/Path/Main/Sanity/ImageBuilder.py b/src/Mod/CAM/Path/Main/Sanity/ImageBuilder.py new file mode 100644 index 0000000000..7fef4ae885 --- /dev/null +++ b/src/Mod/CAM/Path/Main/Sanity/ImageBuilder.py @@ -0,0 +1,230 @@ +# *************************************************************************** +# * Copyright (c) 2024 Ondsel * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * 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. * +# * * +# * FreeCAD 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 Lesser General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with FreeCAD; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +from PySide import QtGui +import FreeCAD +import FreeCADGui +import Path.Log +import os +import time + +if False: + 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 ImageBuilder: + def __init__(self, file_path): + self.file_path = file_path + + def build_image(self, obj, image_name): + raise NotImplementedError("Subclass must implement abstract method") + + def save_image(self, image): + raise NotImplementedError("Subclass must implement abstract method") + + +class ImageBuilderFactory: + @staticmethod + def get_image_builder(file_path, **kwargs): + + # return DummyImageBuilder(file_path, **kwargs) + if FreeCAD.GuiUp: + return GuiImageBuilder(file_path, **kwargs) + else: + return DummyImageBuilder(file_path, **kwargs) + # return NonGuiImageBuilder(file_path, **kwargs) + + +class DummyImageBuilder(ImageBuilder): + def __init__(self, file_path): + Path.Log.debug("Initializing dummyimagebuilder") + super().__init__(file_path) + + def build_image(self, obj, imageName): + return self.file_path + + +class GuiImageBuilder(ImageBuilder): + """ + A class for generating images of 3D objects in a FreeCAD GUI environment. + """ + + def __init__(self, file_path): + super().__init__(file_path) + + Path.Log.debug("Initializing GuiImageBuilder") + self.file_path = file_path + self.currentCamera = FreeCADGui.ActiveDocument.ActiveView.getCameraType() + + self.doc = FreeCADGui.ActiveDocument + + def __del__(self): + Path.Log.debug("Destroying GuiImageBuilder") + self.restore_visibility() + + def prepare_view(self, obj): + # Create a new view + Path.Log.debug("CAM - Preparing view\n") + FreeCADGui.runCommand("Std_ViewCreate", 0) + + # Get the activeview and configure it + aview = FreeCADGui.ActiveDocument.ActiveView + aview.setAnimationEnabled(False) + aview.viewIsometric() + + FreeCADGui.SendMsgToActiveView("PerspectiveCamera") + + # resize the window + mw = FreeCADGui.getMainWindow() + mdi = mw.findChild(QtGui.QMdiArea) + view_window = mdi.activeSubWindow() + view_window.resize(500, 500) + view_window.showMaximized() + + FreeCADGui.Selection.clearSelection() + + self.record_visibility() + obj.Visibility = True + + def record_visibility(self): + self.visible = [o for o in self.doc.Document.Objects if o.Visibility] + for o in self.doc.Document.Objects: + o.Visibility = False + + def destroy_view(self): + Path.Log.debug("CAM - destroying view\n") + mw = FreeCADGui.getMainWindow() + windows = mw.getWindows() + toRemove = windows[1] + mw.removeWindow(toRemove) + + def restore_visibility(self): + Path.Log.debug("CAM - Restoring visibility\n") + for o in self.visible: + o.Visibility = True + + def build_image(self, obj, image_name): + Path.Log.debug("CAM - Building image\n") + """ + Makes an image of the target object. Returns filename. + """ + + file_path = os.path.join(self.file_path, image_name) + + self.prepare_view(obj) + + self.capture_image(file_path) + self.destroy_view() + + result = f"{file_path}_t.png" + + Path.Log.debug(f"Saving image to: {file_path}") + Path.Log.debug(f"Image saved to: {result}") + return result + + def capture_image(self, file_path): + + FreeCADGui.updateGui() + Path.Log.debug("CAM - capture image\n") + a_view = FreeCADGui.activeDocument().activeView() + a_view.saveImage(file_path + ".png", 500, 500, "Current") + a_view.saveImage(file_path + "_t.png", 500, 500, "Transparent") + a_view.setAnimationEnabled(True) + + +class NonGuiImageBuilder(ImageBuilder): + def __init__(self, file_path): + super().__init__(file_path) + Path.Log.debug("nonguiimagebuilder") + + def build_image(self, obj, image_name): + """ + Generates a headless picture of a 3D object and saves it as a PNG and optionally a PostScript file. + + Args: + - obj: The 3D object to generate an image for. + - image_name: Base name for the output image file without extension. + + Returns: + - A boolean indicating the success of the operation. + """ + # Ensure the 'Part' and 'coin' modules are available, along with necessary attributes/methods. + if not hasattr(obj, "Shape") or not hasattr(obj.Shape, "writeInventor"): + Path.Log.debug("Object does not have the required attributes.") + return False + + try: + # Generate Inventor data from the object's shape + iv = obj.Shape.writeInventor() + + # Prepare Inventor data for rendering + inp = coin.SoInput() + inp.setBuffer(iv) + data = coin.SoDB.readAll(inp) + + if data is None: + Path.Log.debug("Failed to read Inventor data.") + return False + + # Setup the scene + base = coin.SoBaseColor() + base.rgb.setValue(0.6, 0.7, 1.0) + data.insertChild(base, 0) + + root = coin.SoSeparator() + light = coin.SoDirectionalLight() + cam = coin.SoOrthographicCamera() + root.addChild(cam) + root.addChild(light) + root.addChild(data) + + # Camera and rendering setup + axo = coin.SbRotation(-0.353553, -0.146447, -0.353553, -0.853553) + viewport = coin.SbViewportRegion(400, 400) + cam.orientation.setValue(axo) + cam.viewAll(root, viewport) + off = coin.SoOffscreenRenderer(viewport) + root.ref() + ret = off.render(root) + root.unref() + + # Saving the rendered image + if off.isWriteSupported("PNG"): + file_path = f"{self.file_path}{os.path.sep}{imageName}.png" + off.writeToFile(file_path, "PNG") + else: + Path.Log.debug("PNG format is not supported.") + # return False + + # Optionally save as PostScript if supported + file_path = f"{self.file_path}{os.path.sep}{imageName}.ps" + off.writeToPostScript(ps_file_path) + + return file_path + + except Exception as e: + print(f"An error occurred: {e}") + return False diff --git a/src/Mod/CAM/Path/Main/Sanity/ReportGenerator.py b/src/Mod/CAM/Path/Main/Sanity/ReportGenerator.py new file mode 100644 index 0000000000..7eb182e1b1 --- /dev/null +++ b/src/Mod/CAM/Path/Main/Sanity/ReportGenerator.py @@ -0,0 +1,255 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2024 Ondsel * +# * * +# * 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 * +# * * +# *************************************************************************** + +from string import Template +import FreeCAD +import Path.Log +import base64 +import os + +from Path.Main.Sanity.HTMLTemplate import ( + html_template, + base_template, + squawk_template, + tool_template, + op_run_template, + op_tool_template, + tool_item_template, +) + +translate = FreeCAD.Qt.translate + +if False: + 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 ReportGenerator: + def __init__(self, data, embed_images=False): + self.embed_images = embed_images + self.squawks = "" + self.tools = "" + self.run_summary_ops = "" + self.formatted_data = {} + self.translated_labels = { + "dateLabel": translate("CAM_Sanity", "Date"), + "datumLabel": translate("CAM_Sanity", "Part Datum"), + "descriptionLabel": translate("CAM_Sanity", "Description"), + "diameterLabel": translate("CAM_Sanity", "Tool Diameter"), + "feedLabel": translate("CAM_Sanity", "Feed Rate"), + "fileSizeLabel": translate("CAM_Sanity", "File Size (kB)"), + "fixturesLabel": translate("CAM_Sanity", "Fixtures"), + "flagsLabel": translate("CAM_Sanity", "Post Processor Flags"), + "gcodeFileLabel": translate("CAM_Sanity", "G-code File"), + "headingLabel": translate("CAM_Sanity", "Setup Report for CAM Job"), + "inspectionNotesLabel": translate("CAM_Sanity", "Inspection Notes"), + "lastpostLabel": translate("CAM_Sanity", "Last Post Process Date"), + "lineCountLabel": translate("CAM_Sanity", "Line Count"), + "machineLabel": translate("CAM_Sanity", "Machine"), + "manufLabel": translate("CAM_Sanity", "Manufacturer"), + "materialLabel": translate("CAM_Sanity", "Material"), + "noteLabel": translate("CAM_Sanity", "Note"), + "offsetsLabel": translate("CAM_Sanity", "Work Offsets"), + "opLabel": translate("CAM_Sanity", "Operation"), + "operatorLabel": translate("CAM_Sanity", "Operator"), + "orderByLabel": translate("CAM_Sanity", "Order By"), + "outputLabel": translate("CAM_Sanity", "Output (Gcode)"), + "partInformationLabel": translate("CAM_Sanity", "Part Information"), + "partNumberLabel": translate("CAM_Sanity", "Part Number"), + "postLabel": translate("CAM_Sanity", "Postprocessor"), + "programmerLabel": translate("CAM_Sanity", "Programmer"), + "roughStockLabel": translate("CAM_Sanity", "Rough Stock"), + "runSummaryLabel": translate("CAM_Sanity", "Run Summary"), + "shapeLabel": translate("CAM_Sanity", "Tool Shape"), + "speedLabel": translate("CAM_Sanity", "Spindle Speed"), + "squawksLabel": translate("CAM_Sanity", "Squawks"), + "stopsLabel": translate("CAM_Sanity", "Stops"), + "tableOfContentsLabel": translate("CAM_Sanity", "Table of Contents"), + "tcLabel": translate("CAM_Sanity", "Tool Controller"), + "toolDataLabel": translate("CAM_Sanity", "Tool Data"), + "toolNumberLabel": translate("CAM_Sanity", "Tool Number"), + "urlLabel": translate("CAM_Sanity", "URL"), + "xDimLabel": translate("CAM_Sanity", "X Size"), + "yDimLabel": translate("CAM_Sanity", "Y Size"), + "zDimLabel": translate("CAM_Sanity", "Z Size"), + "jobMinZLabel": translate("CAM_Sanity", "Minimum Z"), + "jobMaxZLabel": translate("CAM_Sanity", "Maximum Z"), + "coolantLabel": translate("CAM_Sanity", "Coolant Mode"), + "cycleTimeLabel": translate("CAM_Sanity", "Cycle Time"), + "PartLabel": translate("CAM_Sanity", "Part"), + "SequenceLabel": translate("CAM_Sanity", "Sequence"), + "JobTypeLabel": translate("CAM_Sanity", "Job Type"), + "CADLabel": translate("CAM_Sanity", "CAD File"), + "LastSaveLabel": translate("CAM_Sanity", "Last Save"), + "CustomerLabel": translate("CAM_Sanity", "Customer"), + } + + # format the data for all blocks except tool + for block in [ + "baseData", + "designData", + "runData", + "outputData", + "fixtureData", + "stockData", + ]: + for key, val in data[block].items(): + Path.Log.debug(f"key: {key} val: {val}") + if key == "squawkData": + self._format_squawks(val) + elif key == "bases": + self._format_bases(val) + elif key == "operations": + self._format_run_summary_ops(val) + elif key in ["baseimage", "imagepath", "datumImage", "stockImage"]: + Path.Log.debug(f"key: {key} val: {val}") + if self.embed_images: + Path.Log.debug("Embedding images") + encoded_image, tag = self.file_to_base64_with_tag(val) + else: + Path.Log.debug("Not Embedding images") + tag = f"{key}" + self.formatted_data[key] = tag + else: + self.formatted_data[key] = val + + # format the data for the tool block + for key, val in data["toolData"].items(): + if key == "squawkData": + self._format_squawks(val) + # else: + # self._format_tool(key, val) + + else: + toolNumber = key + toolAttributes = val + if "imagepath" in toolAttributes and toolAttributes["imagepath"] != "": + if self.embed_images: + encoded_image, tag = self.file_to_base64_with_tag(toolAttributes["imagepath"]) + else: + tag = f"{key}" + toolAttributes["imagepath"] = tag + + self._format_tool(key, val) + + self.formatted_data["squawks"] = self.squawks + self.formatted_data["run_summary_ops"] = self.run_summary_ops + self.formatted_data["tool_data"] = self.tools + self.formatted_data["tool_list"] = self._format_tool_list(data["toolData"]) + + # Path.Log.debug(self.formatted_data) + def _format_tool_list(self, tool_data): + + tool_list = "" + for key, val in tool_data.items(): + if key == "squawkData": + continue + else: + val["toolNumber"] = key + tool_list += tool_item_template.substitute(val) + return tool_list + + def _format_run_summary_ops(self, op_data): + for op in op_data: + self.run_summary_ops += op_run_template.substitute(op) + + def _format_tool(self, tool_number, tool_data): + td = {} + for key, val in tool_data.items(): + if key == "squawkData": + self._format_squawks(val) + if key == "ops": + opslist = "" + for op in val: + op.update(self.translated_labels) + opslist += op_tool_template.substitute(op) + td[key] = opslist + else: + td[key] = val + + td.update(self.translated_labels) + Path.Log.debug(f"Tool data: {td}") + + self.tools += tool_template.substitute(td) + + def _format_bases(self, base_data): + bases = "" + for base in base_data: + bases += base_template.substitute(base) + self.formatted_data["bases"] = bases + + def _format_squawks(self, squawk_data): + for squawk in squawk_data: + if self.embed_images: + data, tag = self.file_to_base64_with_tag(squawk["squawkIcon"]) + else: + tag = f"TIP" + + squawk["squawkIcon"] = tag + self.squawks += squawk_template.substitute(squawk) + + def generate_html(self): + self.formatted_data.update(self.translated_labels) + html_content = html_template.substitute(self.formatted_data) + return html_content + + def encode_gcode_to_base64(filepath): + with open(filepath, "rb") as file: + encoded_string = base64.b64encode(file.read()).decode('utf-8') + return encoded_string + + + def file_to_base64_with_tag(self, file_path): + # Determine MIME type based on the file extension + mime_types = { + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".png": "image/png", + ".svg": "image/svg+xml", + ".gcode": "application/octet-stream", # MIME type for G-code files + } + extension = os.path.splitext(file_path)[1] + mime_type = mime_types.get(extension, "application/octet-stream") # Default to binary data type if unknown + + if not os.path.exists(file_path): + Path.Log.error(f"File not found: {file_path}") + return "", "" + + try: + # Encode file to base64 + with open(file_path, "rb") as file: + encoded_string = base64.b64encode(file.read()).decode() + + # Generate HTML tag based on file type + if extension in [".jpg", ".jpeg", ".png", ".svg"]: + html_tag = f'' + elif extension in [".gcode", ".nc", ".tap", ".cnc" ]: + html_tag = f'Download G-code File' + else: + html_tag = f'Download File' + + return encoded_string, html_tag + except FileNotFoundError: + Path.Log.error(f"File not found: {file_path}") + return "", "" diff --git a/src/Mod/CAM/Path/Main/Sanity/Sanity.py b/src/Mod/CAM/Path/Main/Sanity/Sanity.py new file mode 100644 index 0000000000..d41203c41c --- /dev/null +++ b/src/Mod/CAM/Path/Main/Sanity/Sanity.py @@ -0,0 +1,480 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2016 sliptonic * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * 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. * +# * * +# * FreeCAD 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 Lesser General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with FreeCAD; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +""" +This file has utilities for checking and catching common errors in FreeCAD +CAM projects. Ideally, the user could execute these utilities from an icon +to make sure tools are selected and configured and defaults have been revised +""" + +from collections import Counter +from datetime import datetime +import FreeCAD +import Path +import Path.Log +import Path.Main.Sanity.ImageBuilder as ImageBuilder +import Path.Main.Sanity.ReportGenerator as ReportGenerator +import os +import tempfile + +translate = FreeCAD.Qt.translate + +if False: + 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 CAMSanity: + """ + This class has the functionality to harvest data from a CAM Job + and export it in a format that is useful to the user. + """ + + def __init__(self, job, output_file): + self.job = job + self.output_file = output_file + self.filelocation = os.path.dirname(output_file) + + # set the filelocation to the parent of the output filename + if not os.path.isdir(self.filelocation): + raise ValueError( + translate( + "CAM_Sanity", + "output location {} doesn't exist".format(os.path.dirname(output_file)), + ) + ) + + self.image_builder = ImageBuilder.ImageBuilderFactory.get_image_builder( + self.filelocation + ) + self.data = self.summarize() + + def summarize(self): + """ + Gather all the incremental parts of the analysis + """ + + data = {} + data["baseData"] = self._baseObjectData() + data["designData"] = self._designData() + data["toolData"] = self._toolData() + data["runData"] = self._runData() + data["outputData"] = self._outputData() + data["fixtureData"] = self._fixtureData() + data["stockData"] = self._stockData() + # 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" + ) + + if squawkType == "TIP": + squawk_icon = "Sanity_Bulb" + elif squawkType == "NOTE": + squawk_icon = "Sanity_Note" + elif squawkType == "WARNING": + squawk_icon = "Sanity_Warning" + elif squawkType == "CAUTION": + squawk_icon = "Sanity_Caution" + + path = f"{FreeCAD.getHomePath()}Mod/CAM/Path/Main/Sanity/{squawk_icon}.svg" + + squawk = { + "Date": str(date), + "Operator": operator, + "Note": note, + "squawkType": squawkType, + "squawkIcon": path, + } + + return squawk + + def _baseObjectData(self): + data = {"baseimage": "", "bases": "", "squawkData": []} + obj = self.job + bases = {} + for name, count in Counter( + [obj.Proxy.baseObject(obj, o).Label for o in obj.Model.Group] + ).items(): + bases[name] = str(count) + data["baseimage"] = self.image_builder.build_image(obj.Model, "baseimage") + data["bases"] = bases + + return data + + def _designData(self): + """ + Returns header information about the design document + Returns information about issues and concerns (squawks) + """ + + obj = self.job + data = { + "FileName": "", + "LastModifiedDate": "", + "Customer": "", + "Designer": "", + "JobDescription": "", + "JobLabel": "", + "Sequence": "", + "JobType": "", + "squawkData": [], + } + 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, Path.Main.Job.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 + + return data + + def _fixtureData(self): + obj = self.job + data = {"fixtures": "", "orderBy": "", "datumImage": "", "squawkData": []} + + data["fixtures"] = str(obj.Fixtures) + data["orderBy"] = str(obj.OrderOutputBy) + + data["datumImage"] = self.image_builder.build_image(obj, "datumImage") + + return data + + def _outputData(self): + obj = self.job + data = { + "lastpostprocess": "", + "lastgcodefile": "", + "optionalstops": "", + "programmer": "", + "machine": "", + "postprocessor": "", + "postprocessorFlags": "", + "filesize": "", + "linecount": "", + "outputfilename": "setupreport", + "squawkData": [], + } + + 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) + + if obj.PostProcessorOutputFile != "": + fname = obj.PostProcessorOutputFile + data["outputfilename"] = os.path.splitext(os.path.basename(fname))[0] + + for op in obj.Operations.Group: + if "Stop" in op.Name and hasattr(op, "Stop") and op.Stop is True: + data["optionalstops"] = "True" + + if obj.LastPostProcessOutput == "": + data["filesize"] = str(0.0) + data["linecount"] = str(0) + data["squawkData"].append( + self.squawk( + "CAMSanity", + translate("CAM_Sanity", "The Job has not been post-processed"), + ) + ) + else: + if os.path.isfile(obj.LastPostProcessOutput): + data["filesize"] = str(os.path.getsize(obj.LastPostProcessOutput) / 1000) + data["linecount"] = str(sum(1 for line in open(obj.LastPostProcessOutput))) + else: + data["filesize"] = str(0.0) + data["linecount"] = str(0) + data["squawkData"].append( + self.squawk( + "CAMSanity", + translate( + "CAM_Sanity", + "The Job's last post-processed file is missing", + ), + ) + ) + + return data + + def _runData(self): + obj = self.job + data = { + "cycletotal": "", + "jobMinZ": "", + "jobMaxZ": "", + "jobDescription": "", + "operations": [], + "squawkData": [], + } + + 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["operations"] = [] + for op in obj.Operations.Group: + oplabel = op.Label + Path.Log.debug(oplabel) + ctime = op.CycleTime if hasattr(op, "CycleTime") else "00:00:00" + cool = op.CoolantMode if hasattr(op, "CoolantMode") else "N/A" + + o = op + while "Dressup" in o.Name: + 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 = "00:00:00" + + 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["operations"].append(opdata) + + return data + + def _stockData(self): + obj = self.job + data = { + "xLen": "", + "yLen": "", + "zLen": "", + "material": "", + "stockImage": "", + "squawkData": [], + } + + 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": + data["squawkData"].append( + self.squawk( + "CAMSanity", + translate("CAM_Sanity", "Consider Specifying the Stock Material"), + squawkType="TIP", + ) + ) + + data["stockImage"] = self.image_builder.build_image(obj.Stock, "stockImage") + + return data + + def _toolData(self): + """ + Returns information about the tools used in the job, and associated + toolcontrollers + Returns information about issues and problems with the tools (squawks) + """ + + obj = self.job + data = {"squawkData": []} + + for TC in obj.Tools.Group: + if not hasattr(TC.Tool, "BitBody"): + data["squawkData"].append( + data["squawkData"].append( + self.squawk( + "CAMSanity", + translate( + "CAM_Sanity", + "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]: + data["squawkData"].append( + self.squawk( + "CAMSanity", + translate( + "CAM_Sanity", "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"] = "" + + if os.path.isfile(TC.Tool.BitShape): + imagedata = TC.Tool.Proxy.getBitThumbnail(TC.Tool) + else: + imagedata = None + data["squawkData"].append( + self.squawk( + "CAMSanity", + translate( + "CAM_Sanity", "Toolbit Shape for TC: {} not found" + ).format(TC.ToolNumber), + squawkType="WARNING", + ) + ) + tooldata["image"] = "" + imagepath = "" #os.path.join(self.filelocation, f"T{TC.ToolNumber}.png") + tooldata["imagepath"] = imagepath + Path.Log.debug(imagepath) + if imagedata is not None: + with open(imagepath, "wb") as fd: + fd.write(imagedata) + fd.close() + + tooldata["feedrate"] = str(TC.HorizFeed) + if TC.HorizFeed.Value == 0.0: + data["squawkData"].append( + self.squawk( + "CAMSanity", + translate( + "CAM_Sanity", "Tool Controller '{}' has no feedrate" + ).format(TC.Label), + squawkType="WARNING", + ) + ) + + tooldata["spindlespeed"] = str(TC.SpindleSpeed) + if TC.SpindleSpeed == 0.0: + data["squawkData"].append( + self.squawk( + "CAMSanity", + translate( + "CAM_Sanity", "Tool Controller '{}' has no spindlespeed" + ).format(TC.Label), + squawkType="WARNING", + ) + ) + + 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", []) + data["squawkData"].append( + self.squawk( + "CAMSanity", + translate( + "CAM_Sanity", "Tool Controller '{}' is not used" + ).format(TC.Label), + squawkType="WARNING", + ) + ) + + return data + + def serialize(self, obj): + """A function to serialize non-serializable objects.""" + if isinstance(obj, type(Exception)): + # Convert an exception to its string representation + return str(obj) + # You might need to handle more types depending on your needs + return str( + obj + ) # Fallback to convert any other non-serializable types to string + + def get_output_report(self): + Path.Log.debug("get_output_url") + + generator = ReportGenerator.ReportGenerator(self.data, embed_images=True) + html = generator.generate_html() + generator = None + return html diff --git a/src/Mod/CAM/Path/Main/Gui/Sanity_Bulb.svg b/src/Mod/CAM/Path/Main/Sanity/Sanity_Bulb.svg similarity index 100% rename from src/Mod/CAM/Path/Main/Gui/Sanity_Bulb.svg rename to src/Mod/CAM/Path/Main/Sanity/Sanity_Bulb.svg diff --git a/src/Mod/CAM/Path/Main/Gui/Sanity_Caution.svg b/src/Mod/CAM/Path/Main/Sanity/Sanity_Caution.svg similarity index 100% rename from src/Mod/CAM/Path/Main/Gui/Sanity_Caution.svg rename to src/Mod/CAM/Path/Main/Sanity/Sanity_Caution.svg diff --git a/src/Mod/CAM/Path/Main/Gui/Sanity_Note.svg b/src/Mod/CAM/Path/Main/Sanity/Sanity_Note.svg similarity index 100% rename from src/Mod/CAM/Path/Main/Gui/Sanity_Note.svg rename to src/Mod/CAM/Path/Main/Sanity/Sanity_Note.svg diff --git a/src/Mod/CAM/Path/Main/Gui/Sanity_Warning.svg b/src/Mod/CAM/Path/Main/Sanity/Sanity_Warning.svg similarity index 100% rename from src/Mod/CAM/Path/Main/Gui/Sanity_Warning.svg rename to src/Mod/CAM/Path/Main/Sanity/Sanity_Warning.svg diff --git a/src/Mod/CAM/TestCAMApp.py b/src/Mod/CAM/TestCAMApp.py index 413d73a076..491ca80769 100644 --- a/src/Mod/CAM/TestCAMApp.py +++ b/src/Mod/CAM/TestCAMApp.py @@ -22,6 +22,7 @@ import TestApp +from Tests.TestCAMSanity import TestCAMSanity from Tests.TestPathProfile import TestPathProfile from Tests.TestPathAdaptive import TestPathAdaptive @@ -76,6 +77,7 @@ from Tests.TestRefactoredTestPostGCodes import TestRefactoredTestPostGCodes from Tests.TestRefactoredTestPostMCodes import TestRefactoredTestPostMCodes # dummy usage to get flake8 and lgtm quiet +False if TestCAMSanity.__name__ else True False if depthTestCases.__name__ else True False if TestApp.__name__ else True False if TestBuildPostList.__name__ else True diff --git a/src/Mod/CAM/Tests/TestCAMSanity.py b/src/Mod/CAM/Tests/TestCAMSanity.py new file mode 100644 index 0000000000..6dde6a3b10 --- /dev/null +++ b/src/Mod/CAM/Tests/TestCAMSanity.py @@ -0,0 +1,332 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2024 Ondsel * +# * * +# * 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 FreeCAD +import Path +from Path.Main.Sanity import ReportGenerator, Sanity +from Path.Main.Sanity.ImageBuilder import ( + DummyImageBuilder, + ImageBuilderFactory, + ImageBuilder, +) +import os +import Path.Post.Command as PathPost +from Path.Post.Processor import PostProcessor +import imghdr +import unittest +from unittest.mock import patch, MagicMock +import urllib +import tempfile +from Tests.PathTestUtils import PathTestBase + +class TestCAMSanity(PathTestBase): + @classmethod + def setUpClass(cls): + cls.doc = FreeCAD.open(FreeCAD.getHomePath() + "/Mod/CAM/Tests/boxtest.fcstd") + cls.job = cls.doc.getObject("Job") + + @classmethod + def tearDownClass(cls): + FreeCAD.closeDocument("boxtest") + + def setUp(self): + self.temp_file = tempfile.NamedTemporaryFile() + + def tearDown(self): + pass + + def test00(self): + """Test no output location""" + with self.assertRaises(TypeError): + S = Sanity.CAMSanity(self.job) + + def test010(self): + # """Test CAMSanity using a DummyImageBuilder""" + with patch( + "Path.Main.Sanity.ImageBuilder.ImageBuilderFactory.get_image_builder" + ) as mock_factory: + dummy_builder = DummyImageBuilder(self.temp_file.name) + mock_factory.return_value = dummy_builder + S = Sanity.CAMSanity(self.job, output_file=self.temp_file.name) + self.assertTrue(os.path.isdir(S.filelocation)) + self.assertEqual(self.temp_file.name, S.output_file) + + def test020(self): + """Test erroneous output location""" + filename = "/tmpXXXX/THIS_DOESN'T_EXIST" + with self.assertRaises(ValueError): + S = Sanity.CAMSanity(self.job, output_file=filename) + + # This test fails A headless image generation routine is needed. + # def test40(self): + # """Test image generation""" + # path = FreeCAD.getUserMacroDir() + # image_builder = ImageBuilder.ImageBuilderFactory.get_image_builder(path) + # file_name = image_builder.build_image(self.doc.getObject("Box"), "theBox") + # print(f"filename:{file_name}") + + # # This test shouldn't be enabled in production as is. + # # it writes to the user macro directory as a persistent location. + # # writing to tempdir fails + # # the tempfile get cleaned up before we can do any asserts. + + # # Need to find a way to write semi-persistently for the tests to pass + + # with open(file_name, 'rb') as f: + # header = f.read(32) # Read the first 32 bytes + # self.assertTrue(imghdr.what(None, header) is not None) + + def test050(self): + """Test base data""" + with patch( + "Path.Main.Sanity.ImageBuilder.ImageBuilderFactory.get_image_builder" + ) as mock_factory: + dummy_builder = DummyImageBuilder(self.temp_file.name) + mock_factory.return_value = dummy_builder + S = Sanity.CAMSanity(self.job, output_file=self.temp_file.name) + data = S._baseObjectData() + self.assertIsInstance(data, dict) + self.assertIn("baseimage", data) + self.assertIn("bases", data) + + def test060(self): + """Test design data""" + with patch( + "Path.Main.Sanity.ImageBuilder.ImageBuilderFactory.get_image_builder" + ) as mock_factory: + dummy_builder = DummyImageBuilder(self.temp_file.name) + mock_factory.return_value = dummy_builder + S = Sanity.CAMSanity(self.job, output_file=self.temp_file.name) + data = S._designData() + self.assertIsInstance(data, dict) + self.assertIn("FileName", data) + self.assertIn("LastModifiedDate", data) + self.assertIn("Customer", data) + self.assertIn("Designer", data) + self.assertIn("JobDescription", data) + self.assertIn("JobLabel", data) + self.assertIn("Sequence", data) + self.assertIn("JobType", data) + + def test070(self): + """Test tool data""" + with patch( + "Path.Main.Sanity.ImageBuilder.ImageBuilderFactory.get_image_builder" + ) as mock_factory: + dummy_builder = DummyImageBuilder(self.temp_file.name) + mock_factory.return_value = dummy_builder + S = Sanity.CAMSanity(self.job, output_file=self.temp_file.name) + data = S._toolData() + self.assertIn("squawkData", data) + + for key in data.keys(): + if isinstance(key, int): + val = data["key"] + self.assertIsInstance(val, dict) + self.assertIn("bitShape", val) + self.assertIn("description", val) + self.assertIn("manufacturer", val) + self.assertIn("url", val) + self.assertIn("inspectionNotes", val) + self.assertIn("diameter", val) + self.assertIn("shape", val) + self.assertIn("partNumber", val) + + def test080(self): + """Test run data""" + with patch( + "Path.Main.Sanity.ImageBuilder.ImageBuilderFactory.get_image_builder" + ) as mock_factory: + dummy_builder = DummyImageBuilder(self.temp_file.name) + mock_factory.return_value = dummy_builder + S = Sanity.CAMSanity(self.job, output_file=self.temp_file.name) + data = S._runData() + self.assertIsInstance(data, dict) + self.assertIn("cycletotal", data) + self.assertIn("jobMinZ", data) + self.assertIn("jobMaxZ", data) + self.assertIn("jobDescription", data) + self.assertIn("squawkData", data) + self.assertIn("operations", data) + + def test090(self): + """Test output data""" + with patch( + "Path.Main.Sanity.ImageBuilder.ImageBuilderFactory.get_image_builder" + ) as mock_factory: + dummy_builder = DummyImageBuilder(self.temp_file.name) + mock_factory.return_value = dummy_builder + S = Sanity.CAMSanity(self.job, output_file=self.temp_file.name) + data = S._outputData() + self.assertIsInstance(data, dict) + self.assertIn("lastpostprocess", data) + self.assertIn("lastgcodefile", data) + self.assertIn("optionalstops", data) + self.assertIn("programmer", data) + self.assertIn("machine", data) + self.assertIn("postprocessor", data) + self.assertIn("postprocessorFlags", data) + self.assertIn("filesize", data) + self.assertIn("linecount", data) + self.assertIn("outputfilename", data) + self.assertIn("squawkData", data) + + def test100(self): + """Test fixture data""" + with patch( + "Path.Main.Sanity.ImageBuilder.ImageBuilderFactory.get_image_builder" + ) as mock_factory: + dummy_builder = DummyImageBuilder(self.temp_file.name) + mock_factory.return_value = dummy_builder + S = Sanity.CAMSanity(self.job, output_file=self.temp_file.name) + data = S._fixtureData() + self.assertIsInstance(data, dict) + self.assertIn("fixtures", data) + self.assertIn("orderBy", data) + self.assertIn("datumImage", data) + self.assertIn("squawkData", data) + + def test110(self): + """Test stock data""" + with patch( + "Path.Main.Sanity.ImageBuilder.ImageBuilderFactory.get_image_builder" + ) as mock_factory: + dummy_builder = DummyImageBuilder(self.temp_file.name) + mock_factory.return_value = dummy_builder + S = Sanity.CAMSanity(self.job, output_file=self.temp_file.name) + data = S._stockData() + self.assertIsInstance(data, dict) + self.assertIn("xLen", data) + self.assertIn("yLen", data) + self.assertIn("zLen", data) + self.assertIn("material", data) + self.assertIn("stockImage", data) + self.assertIn("squawkData", data) + + def test120(self): + """Test squawk data""" + with patch( + "Path.Main.Sanity.ImageBuilder.ImageBuilderFactory.get_image_builder" + ) as mock_factory: + dummy_builder = DummyImageBuilder(self.temp_file.name) + mock_factory.return_value = dummy_builder + S = Sanity.CAMSanity(self.job, output_file=self.temp_file.name) + data = S.squawk( + "CAMSanity", + "Test Message", + squawkType="TIP", + ) + self.assertIsInstance(data, dict) + self.assertIn("Date", data) + self.assertIn("Operator", data) + self.assertIn("Note", data) + self.assertIn("squawkType", data) + self.assertIn("squawkIcon", data) + + def test130(self): + """Test tool data""" + with patch( + "Path.Main.Sanity.ImageBuilder.ImageBuilderFactory.get_image_builder" + ) as mock_factory: + dummy_builder = DummyImageBuilder(self.temp_file.name) + mock_factory.return_value = dummy_builder + S = Sanity.CAMSanity(self.job, output_file=self.temp_file.name) + data = S._toolData() + self.assertIsInstance(data, dict) + self.assertIn("squawkData", data) + + for key, val in data.items(): + if key == "squawkData": + continue + else: + self.assertIsInstance(val, dict) + self.assertIn("bitShape", val) + self.assertIn("description", val) + self.assertIn("manufacturer", val) + self.assertIn("url", val) + self.assertIn("inspectionNotes", val) + self.assertIn("diameter", val) + self.assertIn("shape", val) + self.assertIn("partNumber", val) + + # def test140(self): + # """Test Generate Report""" + # with patch('Path.Main.Sanity.ImageBuilder.ImageBuilderFactory.get_image_builder') as mock_factory: + # dummy_builder = DummyImageBuilder(self.temp_file.name) + # mock_factory.return_value = dummy_builder + # S = Sanity.CAMSanity(self.job, output_file= self.temp_file.name) + # html_content = S.get_output_report() + # self.assertIsInstance(html_content, str) + + # def test150(self): + # """Test Post Processing a File""" + + # def exportObjectsWith(objs, partname, job, sequence, postname): + # Path.Log.track(partname, sequence) + # Path.Log.track(objs) + + # Path.Log.track(objs, partname) + + # postArgs = Path.Preferences.defaultPostProcessorArgs() + # if hasattr(job, "PostProcessorArgs") and job.PostProcessorArgs: + # postArgs = job.PostProcessorArgs + # elif hasattr(job, "PostProcessor") and job.PostProcessor: + # postArgs = "" + + # Path.Log.track(postArgs) + + # filename = f"output-{partname}-{sequence}.ngc" + + # processor = PostProcessor.load(postname) + # gcode = processor.export(objs, filename, postArgs) + # return (gcode, filename) + + # doc = FreeCAD.open(FreeCAD.getHomePath() + "/Mod/CAM/Tests/boxtest.fcstd") + # job = self.doc.getObject("Job") + + # postlist = PathPost.buildPostList(job) + + # filenames = [] + # for idx, section in enumerate(postlist): + # partname = section[0] + # sublist = section[1] + + # gcode, name = exportObjectsWith(sublist, partname, job, idx, "linuxcnc") + # filenames.append(name) + + # with tempfile.TemporaryDirectory() as temp_dir: + # output_file_path = temp_dir + + # output_file_name = os.path.join(output_file_path, "test.ngc") + + # def test200(self): + # """Generate Report This test is disabled but useful during development to see the report""" + # with tempfile.NamedTemporaryFile() as temp_file: + # S= Sanity.CAMSanity(self.job, output_file=temp_file.name) + # shape = self.doc.getObject("Box") + # html_content = S.get_output_report() + + # encoded_html = urllib.parse.quote(html_content) + + # data_uri = f"data:text/html;charset=utf-8,{encoded_html}" + # import webbrowser + # webbrowser.open(data_uri)