refactor Sanity

Logic is more modular with many more unit tests.
Reduced dependence on GUI
Cleaner template structure
This commit is contained in:
Brad Collette
2024-04-08 17:27:14 -05:00
committed by sliptonic
parent 4d2e124f11
commit e22cde251c
16 changed files with 2040 additions and 1642 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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()

File diff suppressed because it is too large Load Diff

View 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())

View 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>
"""
)

View 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

View 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 "", ""

View 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

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -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

View 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)