refactor Sanity
Logic is more modular with many more unit tests. Reduced dependence on GUI Cleaner template structure
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
99
src/Mod/CAM/Path/Main/Gui/SanityCmd.py
Normal file
@@ -0,0 +1,99 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2016 sliptonic <shopinthewoods@gmail.com> *
|
||||
# * *
|
||||
# * 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())
|
||||
543
src/Mod/CAM/Path/Main/Sanity/HTMLTemplate.py
Normal file
@@ -0,0 +1,543 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2024 Ondsel <development@ondsel.com> *
|
||||
# * *
|
||||
# * This program is free software; you can redistribute it and/or modify *
|
||||
# * it under the terms of the GNU Lesser General Public License (LGPL) *
|
||||
# * as published by the Free Software Foundation; either version 2 of *
|
||||
# * the License, or (at your option) any later version. *
|
||||
# * for detail see the LICENCE text file. *
|
||||
# * *
|
||||
# * This program is distributed in the hope that it will be useful, *
|
||||
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
# * GNU Library General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Library General Public *
|
||||
# * License along with this program; if not, write to the Free Software *
|
||||
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
|
||||
# * USA *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
from string import Template
|
||||
"""
|
||||
This module contains the HTML template for the CAM Sanity report.
|
||||
"""
|
||||
|
||||
html_template = Template(
|
||||
"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
|
||||
<title>Setup Report for FreeCAD Job: Path Special</title>
|
||||
<style type="text/css">
|
||||
body {
|
||||
background-color: #FFFFFF;
|
||||
color: #000000;
|
||||
font-family: "Open Sans, DejaVu Sans, sans-serif";
|
||||
}
|
||||
h2.western, .ToC {
|
||||
font-size: 20pt;
|
||||
color: #ba3925;
|
||||
margin-bottom: 0.5cm;
|
||||
}
|
||||
a.customLink {
|
||||
color: #2156a5;
|
||||
text-decoration: none;
|
||||
font-size: 12pt;
|
||||
}
|
||||
ul {
|
||||
padding-left: 0;
|
||||
list-style: none;
|
||||
}
|
||||
ul.subList {
|
||||
padding-left: 20px;
|
||||
}
|
||||
li.subItem {
|
||||
padding-top: 5px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>${headingLabel}: ${JobLabel}</h1>
|
||||
<div id="toc">
|
||||
<h2 class="ToC">${tableOfContentsLabel}</h2>
|
||||
<ul>
|
||||
<li><a class="customLink" href="#_part_information">${partInformationLabel}</a></li>
|
||||
<li><a class="customLink" href="#_run_summary">${runSummaryLabel}</a></li>
|
||||
<li><a class="customLink" href="#_rough_stock">${roughStockLabel}</a></li>
|
||||
<li><a class="customLink" href="#_tool_data">${toolDataLabel}</a>
|
||||
<ul class="subList">
|
||||
${tool_list}
|
||||
</ul>
|
||||
</li>
|
||||
<li><a class="customLink" href="#_output">${outputLabel}</a></li>
|
||||
<li><a class="customLink" href="#_fixtures_and_workholding">${fixturesLabel}</a></li>
|
||||
<li><a class="customLink" href="#_squawks">${squawksLabel}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
<h2 class="western"><a name="_part_information"></a>${partInformationLabel}</h2>
|
||||
<table cellpadding="2" cellspacing="2" bgcolor="#ffffff" style="background: #ffffff;">
|
||||
<colgroup>
|
||||
<col width="200"/>
|
||||
<col width="525"/>
|
||||
<col width="250"/>
|
||||
</colgroup>
|
||||
<tr valign="top">
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm;">
|
||||
<strong>${PartLabel}</strong>
|
||||
</td>
|
||||
<td>
|
||||
<table style="background-color: #ffffff;">
|
||||
<colgroup>
|
||||
<col width="175"/>
|
||||
<col width="175"/>
|
||||
</colgroup>
|
||||
${bases}
|
||||
</table>
|
||||
</td>
|
||||
<td rowspan="7" style="border: 1px solid #dedede; padding: 0.05cm;">
|
||||
${baseimage}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm;">
|
||||
<strong>${SequenceLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm;">
|
||||
${Sequence}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm;">
|
||||
<strong>${JobTypeLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm;">
|
||||
${JobType}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm;">
|
||||
<strong>${CADLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm;">
|
||||
${FileName}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm;">
|
||||
<strong>${LastSaveLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm;">
|
||||
${LastModifiedDate}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm;">
|
||||
<strong>${CustomerLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm;">
|
||||
${Customer}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h2 class="western"><a name="_run_summary"></a>${runSummaryLabel}</h2>
|
||||
<table cellpadding="2" cellspacing="2" bgcolor="#ffffff" style="background: #ffffff;">
|
||||
<colgroup>
|
||||
<col width="210"/>
|
||||
<col width="210"/>
|
||||
<col width="210"/>
|
||||
<col width="210"/>
|
||||
<col width="210"/>
|
||||
</colgroup>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<strong>${opLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<strong>${jobMinZLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<strong>${jobMaxZLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<strong>${coolantLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<strong>${cycleTimeLabel}</strong>
|
||||
</td>
|
||||
</tr>
|
||||
${run_summary_ops}
|
||||
</table>
|
||||
|
||||
<h2 class="western"><a name="_rough_stock"></a>${roughStockLabel}</h2>
|
||||
<table cellpadding="2" cellspacing="2" bgcolor="#ffffff" style="background: #ffffff;">
|
||||
<colgroup>
|
||||
<col width="350"/>
|
||||
<col width="350"/>
|
||||
<col width="350"/>
|
||||
</colgroup>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<strong>${materialLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
${material}
|
||||
</td>
|
||||
<td rowspan="4" style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
${stockImage}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<strong>${xDimLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
${xLen}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<strong>${yDimLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
${yLen}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<strong>${zDimLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
${zLen}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
<h2 class="western"><a name="_tool_data"></a>${toolDataLabel}</h2>
|
||||
${tool_data}
|
||||
|
||||
|
||||
<h2 class="western"><a name="_output"></a>${outputLabel}</h2>
|
||||
<table cellpadding="2" cellspacing="2" bgcolor="#ffffff" style="background: #ffffff;">
|
||||
<colgroup>
|
||||
<col width="525"/>
|
||||
<col width="525"/>
|
||||
</colgroup>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<strong>${gcodeFileLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
${lastgcodefile}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<strong>${lastpostLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
${lastpostprocess}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<strong>${stopsLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
${optionalstops}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<strong>${programmerLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
${programmer}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<strong>${machineLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
${machine}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<strong>${postLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
${postprocessor}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<strong>${flagsLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
${postprocessorFlags}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<strong>${fileSizeLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
${filesize}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<strong>${lineCountLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
${linecount}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h2 class="western"><a name="_fixtures_and_workholding"></a>${fixturesLabel}</h2>
|
||||
<table cellpadding="2" cellspacing="2" bgcolor="#ffffff" style="background: #ffffff;">
|
||||
<colgroup>
|
||||
<col width="525"/>
|
||||
<col width="525"/>
|
||||
</colgroup>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<strong>${offsetsLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
${fixtures}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<strong>${orderByLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
${orderBy}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<strong>${datumLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
${datumImage}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h2 class="western"><a name="_squawks"></a>${squawksLabel}</h2>
|
||||
<table cellpadding="2" cellspacing="2" bgcolor="#ffffff" style="background-color: #ffffff;">
|
||||
<colgroup>
|
||||
<col width="100"/>
|
||||
<col width="250"/>
|
||||
<col width="250"/>
|
||||
<col width="550"/>
|
||||
</colgroup>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<strong>${noteLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<strong>${operatorLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<strong>${dateLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<strong>${noteLabel}</strong>
|
||||
</td>
|
||||
${squawks}
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<p style="line-height: 100%; margin-bottom: 0cm"><br/>
|
||||
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
)
|
||||
|
||||
base_template = Template(
|
||||
"""
|
||||
<tr>
|
||||
<td style='border: 1px solid #dedede; padding: 0.05cm;'>
|
||||
%{key}
|
||||
</td>
|
||||
<td style='border: 1px solid #dedede; padding: 0.05cm;'>
|
||||
%{val}
|
||||
</td>
|
||||
</tr>
|
||||
"""
|
||||
)
|
||||
|
||||
squawk_template = Template(
|
||||
"""
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
${squawkIcon}
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
${Operator}
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
${Date}
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm" colspan="3">
|
||||
${Note}
|
||||
</td>
|
||||
</tr>
|
||||
"""
|
||||
)
|
||||
|
||||
tool_template = Template(
|
||||
"""
|
||||
<table cellpadding="2" cellspacing="2" bgcolor="#ffffff" style="background: #ffffff;">
|
||||
<colgroup>
|
||||
<col width="350"/>
|
||||
<col width="350"/>
|
||||
<col width="350"/>
|
||||
</colgroup>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<strong>${descriptionLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
${description}
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
${imagepath}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<strong>${manufLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm" colspan="2">
|
||||
${manufacturer}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<strong>${partNumberLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm" colspan="2">
|
||||
${partNumber}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<strong>${urlLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm" colspan="2">
|
||||
${url}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<strong>${shapeLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm" colspan="2">
|
||||
${shape}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<strong>${inspectionNotesLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm" colspan="2">
|
||||
${inspectionNotes}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<strong>${diameterLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm" colspan="2">
|
||||
${diameter}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
${ops}
|
||||
"""
|
||||
)
|
||||
|
||||
op_tool_template = Template(
|
||||
"""
|
||||
<table cellpadding="2" cellspacing="2" bgcolor="#ffffff" style="background: #ffffff;">
|
||||
<colgroup>
|
||||
<col width="262"/>
|
||||
<col width="262"/>
|
||||
<col width="262"/>
|
||||
<col width="262"/>
|
||||
</colgroup>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<strong>${opLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<strong>${tcLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<strong>${feedLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<strong>${speedLabel}</strong>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
${Operation}
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
${ToolController}
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
${Feed}
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
${Speed}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
"""
|
||||
)
|
||||
|
||||
op_run_template = Template(
|
||||
"""
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
${opName}
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
${minZ}
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
${maxZ}
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
${coolantMode}
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
${cycleTime}
|
||||
</td>
|
||||
</tr>
|
||||
"""
|
||||
)
|
||||
|
||||
tool_item_template = Template(
|
||||
"""
|
||||
<li class="subItem"><a class="customLink" href="#_tool_data_T${toolNumber}">T${toolNumber}-${description}</a></li>
|
||||
"""
|
||||
)
|
||||
230
src/Mod/CAM/Path/Main/Sanity/ImageBuilder.py
Normal file
@@ -0,0 +1,230 @@
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2024 Ondsel <development@ondsel.com> *
|
||||
# * *
|
||||
# * 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
|
||||
255
src/Mod/CAM/Path/Main/Sanity/ReportGenerator.py
Normal file
@@ -0,0 +1,255 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2024 Ondsel <development@ondsel.com> *
|
||||
# * *
|
||||
# * This program is free software; you can redistribute it and/or modify *
|
||||
# * it under the terms of the GNU Lesser General Public License (LGPL) *
|
||||
# * as published by the Free Software Foundation; either version 2 of *
|
||||
# * the License, or (at your option) any later version. *
|
||||
# * for detail see the LICENCE text file. *
|
||||
# * *
|
||||
# * This program is distributed in the hope that it will be useful, *
|
||||
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
# * GNU Library General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Library General Public *
|
||||
# * License along with this program; if not, write to the Free Software *
|
||||
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
|
||||
# * USA *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
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"<img src={val} name='Image' alt={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"<img src={toolAttributes['imagepath']} name='Image' alt={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"<img src={squawk['squawkIcon']} name='Image' alt='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'<img src="data:{mime_type};base64,{encoded_string}">'
|
||||
elif extension in [".gcode", ".nc", ".tap", ".cnc" ]:
|
||||
html_tag = f'<a href="data:{mime_type};base64,{encoded_string}" download="{os.path.basename(file_path)}">Download G-code File</a>'
|
||||
else:
|
||||
html_tag = f'<a href="data:{mime_type};base64,{encoded_string}" download="{os.path.basename(file_path)}">Download File</a>'
|
||||
|
||||
return encoded_string, html_tag
|
||||
except FileNotFoundError:
|
||||
Path.Log.error(f"File not found: {file_path}")
|
||||
return "", ""
|
||||
480
src/Mod/CAM/Path/Main/Sanity/Sanity.py
Normal file
@@ -0,0 +1,480 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2016 sliptonic <shopinthewoods@gmail.com> *
|
||||
# * *
|
||||
# * 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
|
||||
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
@@ -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
|
||||
|
||||
332
src/Mod/CAM/Tests/TestCAMSanity.py
Normal file
@@ -0,0 +1,332 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2024 Ondsel <development@ondsel.com> *
|
||||
# * *
|
||||
# * This program is free software; you can redistribute it and/or modify *
|
||||
# * it under the terms of the GNU Lesser General Public License (LGPL) *
|
||||
# * as published by the Free Software Foundation; either version 2 of *
|
||||
# * the License, or (at your option) any later version. *
|
||||
# * for detail see the LICENCE text file. *
|
||||
# * *
|
||||
# * This program is distributed in the hope that it will be useful, *
|
||||
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
# * GNU Library General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Library General Public *
|
||||
# * License along with this program; if not, write to the Free Software *
|
||||
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
|
||||
# * USA *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
import 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)
|
||||