CAM: Fix CAM Sanity Report
Fix base template substitution and improve tool/op formatting in sanity report, also update HTML/CSS and image handling.
- Major HTML/CSS refactor for CAM Sanity Report template:
- Rewrote HTMLTemplate.py with modern, responsive CSS, semantic HTML, and accessibility improvements.
- Added CSS reset, responsive image handling, and improved table/list styling.
- Introduced .heading-container and .top-link for right-aligned "Top" navigation links on all major sections and tool headers (hidden in print).
- Updated all section and tool headers to use new navigation and layout.
- Cleaned up legacy markup, removed inline styles, and standardized variable substitution using string.Template syntax (${key}, ${val}).
- Updated base_template in HTMLTemplate.py to use string.Template syntax (${key}, ${val}) instead of %{key}, %{val} for correct variable substitution.
- Enhanced image generation and embedding:
- Updated ImageBuilder to support high-DPI (800x800) images and direct byte output for embedding.
- All report images (base, stock, datum, tool) now use in-memory bytes for embedding when possible.
- Tool images support a toggle for using toolbit thumbnails or fallback head-on renders.
- ReportGenerator now embeds images as base64 when requested, with correct HTML tags.
- Improved squawk, tool, and operation data formatting:
- Squawk dates now use localized string formatting.
- Tool diameter and feedrate now use .UserString for better display.
- Spindle speed now formatted as integer with "rpm" suffix.
- Operation feed and speed values also use .UserString and "rpm" formatting.
- Fixed _format_bases in ReportGenerator.py to iterate over base_data.items() and pass {"key": key, "val": val} to the template, ensuring all bases are listed correctly.
- General code cleanup and improved maintainability throughout the CAM Sanity reporting stack.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2024 Ondsel <development@ondsel.com> *
|
||||
# * Copyright (c) 2025 Billy Huddleston <billy@ivdc.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) *
|
||||
@@ -33,304 +34,627 @@ html_template = Template(
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
|
||||
<title>Setup Report for FreeCAD Job: Path Special</title>
|
||||
<style type="text/css">
|
||||
/* Reset margins and padding */
|
||||
div, dl, dt, dd, ul, ol, li, h1, h2, h3, #toctitle, .sidebarblock>.content>.title, h4, h5, h6, pre, form, p, blockquote, th, td {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Responsive image handling */
|
||||
img, object, embed {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* Base styling */
|
||||
body {
|
||||
background-color: #FFFFFF;
|
||||
color: #000000;
|
||||
font-family: "Open Sans, DejaVu Sans, sans-serif";
|
||||
background: #fff;
|
||||
color: rgba(0,0,0,.8);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: "Noto Serif", "DejaVu Serif", serif;
|
||||
line-height: 1;
|
||||
position: relative;
|
||||
cursor: auto;
|
||||
word-wrap: anywhere;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
max-width: 62.5em;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding: 20px;
|
||||
}
|
||||
h2.western, .ToC {
|
||||
font-size: 20pt;
|
||||
|
||||
/* Container styling */
|
||||
.center-container {
|
||||
max-width: 62.5em;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* Headings */
|
||||
h1, h2, h3, #toctitle, h4, h5, h6 {
|
||||
font-family: "Open Sans", "DejaVu Sans", sans-serif;
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
color: #ba3925;
|
||||
margin-bottom: 0.5cm;
|
||||
text-rendering: optimizeLegibility;
|
||||
margin-top: 1em;
|
||||
margin-bottom: .5em;
|
||||
line-height: 1.0125em;
|
||||
}
|
||||
a.customLink {
|
||||
|
||||
h1 {
|
||||
font-size: 2.125em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.6875em;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.375em;
|
||||
}
|
||||
|
||||
h4, h5 {
|
||||
font-size: 1.125em;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
#toctitle {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
#toctitle {
|
||||
font-size: 1.375em;
|
||||
}
|
||||
}
|
||||
|
||||
/* Media query for larger screens */
|
||||
@media screen and (min-width: 768px) {
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.75em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 2.3125em;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.6875em;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.4375em;
|
||||
}
|
||||
}
|
||||
|
||||
/* Links */
|
||||
a {
|
||||
color: #2156a5;
|
||||
text-decoration: underline;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
/* TOC styling */
|
||||
#toc {
|
||||
border-bottom: 1px solid #e7e7e9;
|
||||
padding-bottom: .5em;
|
||||
}
|
||||
|
||||
#header>h1:first-child+#toc {
|
||||
margin-top: 8px;
|
||||
border-top: 1px solid #dddddf;
|
||||
}
|
||||
|
||||
#toc>ul {
|
||||
margin-left: .125em;
|
||||
}
|
||||
|
||||
#toc ul {
|
||||
font-family: "Open Sans", "DejaVu Sans", sans-serif;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
#toc li {
|
||||
line-height: 1.3334;
|
||||
margin-top: .3334em;
|
||||
}
|
||||
|
||||
#toc a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover, a:focus {
|
||||
color: #1d4b8f;
|
||||
}
|
||||
|
||||
/* Lists */
|
||||
ul, ol, dl {
|
||||
line-height: 1.6;
|
||||
margin-bottom: 1.25em;
|
||||
list-style-position: outside;
|
||||
font-family: inherit;
|
||||
margin-left: 1.5em;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: disc;
|
||||
}
|
||||
|
||||
ul li ul, ul li ol {
|
||||
margin-left: 1.25em;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* TOC section levels */
|
||||
#toc ul.sectlevel0>li>a {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
#toc ul.sectlevel0 ul.sectlevel1 {
|
||||
margin: .5em 0;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
#toc.toc2 ul ul {
|
||||
margin-left: 0;
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
#toc.toc2 ul.sectlevel0 ul.sectlevel1 {
|
||||
padding-left: 0;
|
||||
margin-top: .5em;
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1280px) {
|
||||
#toc.toc2 ul ul {
|
||||
padding-left: 1.25em;
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive image handling at different screen sizes */
|
||||
@media screen and (max-width: 768px) {
|
||||
td.image-container {
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
margin: 1em auto;
|
||||
}
|
||||
|
||||
table td[rowspan] {
|
||||
display: table-cell;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tables - more detailed styling */
|
||||
table {
|
||||
background: #fff;
|
||||
margin-bottom: 1.25em;
|
||||
border: 1px solid #dedede;
|
||||
word-wrap: normal;
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table thead, table tfoot {
|
||||
background: #f7f8f7;
|
||||
}
|
||||
|
||||
table thead tr th, table thead tr td, table tfoot tr th, table tfoot tr td {
|
||||
padding: .5em .625em .625em;
|
||||
font-size: inherit;
|
||||
color: rgba(0,0,0,.8);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
table tr th, table tr td {
|
||||
padding: .5625em .625em;
|
||||
font-size: inherit;
|
||||
color: rgba(0,0,0,.8);
|
||||
line-height: 1.6;
|
||||
border: 1px solid #dedede;
|
||||
}
|
||||
|
||||
/* Anchor links styling */
|
||||
#content h1>a.anchor,h2>a.anchor,h3>a.anchor,#toctitle>a.anchor,.sidebarblock>.content>.title>a.anchor,h4>a.anchor,h5>a.anchor,h6>a.anchor{
|
||||
position:absolute;
|
||||
z-index:1001;
|
||||
width:1.5ex;
|
||||
margin-left:-1.5ex;
|
||||
display:block;
|
||||
text-decoration:none!important;
|
||||
visibility:hidden;
|
||||
text-align:center;
|
||||
font-weight:400
|
||||
}
|
||||
|
||||
table tr.even, table tr.alt {
|
||||
background: #f8f8f7;
|
||||
}
|
||||
|
||||
/* Image container styling */
|
||||
td.image-container {
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
width: 40%;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
td.image-container img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* Text styling */
|
||||
p {
|
||||
line-height: 1.6;
|
||||
margin-bottom: 1.25em;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
strong, b {
|
||||
font-weight: bold;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
em, i {
|
||||
font-style: italic;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
/* Top navigation links - base style */
|
||||
.top-link {
|
||||
display: inline-block;
|
||||
float: right;
|
||||
font-size: 0.8em;
|
||||
font-weight: normal;
|
||||
text-transform: uppercase;
|
||||
color: #2156a5;
|
||||
text-decoration: none;
|
||||
font-size: 12pt;
|
||||
position: relative;
|
||||
}
|
||||
ul {
|
||||
padding-left: 0;
|
||||
list-style: none;
|
||||
|
||||
/* Specific positioning for h2 headings */
|
||||
.heading-container h2 + .top-link {
|
||||
margin-top: 4.75em;
|
||||
bottom: 0.3em;
|
||||
}
|
||||
ul.subList {
|
||||
padding-left: 20px;
|
||||
|
||||
/* Specific positioning for h3 headings */
|
||||
.heading-container h3 + .top-link {
|
||||
margin-top: 3.5em;
|
||||
bottom: 0.3em;
|
||||
}
|
||||
li.subItem {
|
||||
padding-top: 5px;
|
||||
|
||||
/* Fallbacks in case the adjacent sibling selector doesn't work as expected */
|
||||
.heading-container:has(h2) .top-link {
|
||||
margin-top: 4.75em;
|
||||
}
|
||||
|
||||
.heading-container:has(h3) .top-link {
|
||||
margin-top: 3.5em;
|
||||
}
|
||||
|
||||
/* Clearfix for headings with top links */
|
||||
.heading-container {
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.heading-container h2,
|
||||
.heading-container h3 {
|
||||
float: left;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Hide top links when printing */
|
||||
@media print {
|
||||
.top-link {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="header" class="center-container">
|
||||
<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">
|
||||
<div id="toc" class="toc">
|
||||
<div id="toctitle">${tableOfContentsLabel}</div>
|
||||
<ul class="sectlevel1">
|
||||
<li><a href="#_part_information">${partInformationLabel}</a></li>
|
||||
<li><a href="#_run_summary">${runSummaryLabel}</a></li>
|
||||
<li><a href="#_rough_stock">${roughStockLabel}</a></li>
|
||||
<li><a href="#_tool_data">${toolDataLabel}</a>
|
||||
<ul class="sectlevel2">
|
||||
${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>
|
||||
<li><a href="#_output">${outputLabel}</a></li>
|
||||
<li><a href="#_fixtures_and_workholding">${fixturesLabel}</a></li>
|
||||
<li><a 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;">
|
||||
<div class="heading-container">
|
||||
<h2 id="_part_information"><a name="_part_information"></a>${partInformationLabel}</h2><a href="#header" class="top-link">Top</a>
|
||||
</div>
|
||||
<table>
|
||||
<colgroup>
|
||||
<col width="200"/>
|
||||
<col width="525"/>
|
||||
<col width="250"/>
|
||||
<col width="20%"/>
|
||||
<col width="50%"/>
|
||||
<col width="30%"/>
|
||||
</colgroup>
|
||||
<tr valign="top">
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm;">
|
||||
<tr>
|
||||
<td>
|
||||
<strong>${PartLabel}</strong>
|
||||
</td>
|
||||
<td>
|
||||
<table style="background-color: #ffffff;">
|
||||
<table>
|
||||
<colgroup>
|
||||
<col width="175"/>
|
||||
<col width="175"/>
|
||||
<col width="50%"/>
|
||||
<col width="50%"/>
|
||||
</colgroup>
|
||||
${bases}
|
||||
</table>
|
||||
</td>
|
||||
<td rowspan="7" style="border: 1px solid #dedede; padding: 0.05cm;">
|
||||
<td rowspan="7" class="image-container">
|
||||
${baseimage}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm;">
|
||||
<td>
|
||||
<strong>${SequenceLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm;">
|
||||
<td>
|
||||
${Sequence}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm;">
|
||||
<td>
|
||||
<strong>${JobTypeLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm;">
|
||||
<td>
|
||||
${JobType}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm;">
|
||||
<td>
|
||||
<strong>${CADLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm;">
|
||||
<td>
|
||||
${FileName}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm;">
|
||||
<td>
|
||||
<strong>${LastSaveLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm;">
|
||||
<td>
|
||||
${LastModifiedDate}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm;">
|
||||
<td>
|
||||
<strong>${CustomerLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm;">
|
||||
<td>
|
||||
${Customer}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h2 class="western"><a name="_run_summary"></a>${runSummaryLabel}</h2>
|
||||
<table cellpadding="2" cellspacing="2" bgcolor="#ffffff" style="background: #ffffff;">
|
||||
<div class="heading-container">
|
||||
<h2 id="_run_summary"><a name="_run_summary"></a>${runSummaryLabel}</h2><a href="#header" class="top-link">Top</a>
|
||||
</div>
|
||||
<table>
|
||||
<colgroup>
|
||||
<col width="210"/>
|
||||
<col width="210"/>
|
||||
<col width="210"/>
|
||||
<col width="210"/>
|
||||
<col width="210"/>
|
||||
<col width="20%"/>
|
||||
<col width="20%"/>
|
||||
<col width="20%"/>
|
||||
<col width="20%"/>
|
||||
<col width="20%"/>
|
||||
</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}
|
||||
<thead>
|
||||
<tr>
|
||||
<th><strong>${opLabel}</strong></th>
|
||||
<th><strong>${jobMinZLabel}</strong></th>
|
||||
<th><strong>${jobMaxZLabel}</strong></th>
|
||||
<th><strong>${coolantLabel}</strong></th>
|
||||
<th><strong>${cycleTimeLabel}</strong></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${run_summary_ops}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2 class="western"><a name="_rough_stock"></a>${roughStockLabel}</h2>
|
||||
<div class="heading-container">
|
||||
<h2 id="_rough_stock"><a name="_rough_stock"></a>${roughStockLabel}</h2><a href="#header" class="top-link">Top</a>
|
||||
</div>
|
||||
|
||||
<table cellpadding="2" cellspacing="2" bgcolor="#ffffff" style="background: #ffffff;">
|
||||
<table>
|
||||
<colgroup>
|
||||
<col width="350"/>
|
||||
<col width="350"/>
|
||||
<col width="350"/>
|
||||
<col width="30%"/>
|
||||
<col width="30%"/>
|
||||
<col width="40%"/>
|
||||
</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="7" style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
${stockImage}
|
||||
</td>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>${materialLabel}</strong></td>
|
||||
<td>${material}</td>
|
||||
<td rowspan="7" class="image-container">
|
||||
${stockImage}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>${sSpeedHSSLabel}</strong></td>
|
||||
<td>${surfaceSpeedHSS}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>${sSpeedCarbideLabel}</strong></td>
|
||||
<td>${surfaceSpeedCarbide}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm"><strong>${sSpeedHSSLabel}</strong></td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">${surfaceSpeedHSS}</td>
|
||||
<td><strong>${xDimLabel}</strong></td>
|
||||
<td>${xLen}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm"><strong>${sSpeedCarbideLabel}</strong></td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">${surfaceSpeedCarbide}</td>
|
||||
<td><strong>${yDimLabel}</strong></td>
|
||||
<td>${yLen}</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>
|
||||
<td><strong>${zDimLabel}</strong></td>
|
||||
<td>${zLen}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
<h2 class="western"><a name="_tool_data"></a>${toolDataLabel}</h2>
|
||||
<div class="heading-container">
|
||||
<h2 id="_tool_data"><a name="_tool_data"></a>${toolDataLabel}</h2><a href="#header" class="top-link">Top</a>
|
||||
</div>
|
||||
${tool_data}
|
||||
|
||||
|
||||
<h2 class="western"><a name="_output"></a>${outputLabel}</h2>
|
||||
<div class="heading-container">
|
||||
<h2 id="_output"><a name="_output"></a>${outputLabel}</h2><a href="#header" class="top-link">Top</a>
|
||||
</div>
|
||||
<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">
|
||||
<td>
|
||||
<strong>${gcodeFileLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<td>
|
||||
${lastgcodefile}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<td>
|
||||
<strong>${lastpostLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<td>
|
||||
${lastpostprocess}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<td>
|
||||
<strong>${stopsLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<td>
|
||||
${optionalstops}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<td>
|
||||
<strong>${programmerLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<td>
|
||||
${programmer}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<td>
|
||||
<strong>${machineLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<td>
|
||||
${machine}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<td>
|
||||
<strong>${postLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<td>
|
||||
${postprocessor}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<td>
|
||||
<strong>${flagsLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<td>
|
||||
${postprocessorFlags}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<td>
|
||||
<strong>${fileSizeLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<td>
|
||||
${filesize}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<td>
|
||||
<strong>${lineCountLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<td>
|
||||
${linecount}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h2 class="western"><a name="_fixtures_and_workholding"></a>${fixturesLabel}</h2>
|
||||
<div class="heading-container">
|
||||
<h2 id="_fixtures_and_workholding"><a name="_fixtures_and_workholding"></a>${fixturesLabel}</h2><a href="#header" class="top-link">Top</a>
|
||||
</div>
|
||||
<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">
|
||||
<td>
|
||||
<strong>${offsetsLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<td>
|
||||
${fixtures}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<td>
|
||||
<strong>${orderByLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<td>
|
||||
${orderBy}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<td>
|
||||
<strong>${datumLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<td class="image-container">
|
||||
${datumImage}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h2 class="western"><a name="_squawks"></a>${squawksLabel}</h2>
|
||||
<div class="heading-container">
|
||||
<h2 id="_squawks"><a name="_squawks"></a>${squawksLabel}</h2><a href="#header" class="top-link">Top</a>
|
||||
</div>
|
||||
<table cellpadding="2" cellspacing="2" bgcolor="#ffffff" style="background-color: #ffffff;">
|
||||
<colgroup>
|
||||
<col width="100"/>
|
||||
@@ -339,16 +663,16 @@ ${tool_data}
|
||||
<col width="550"/>
|
||||
</colgroup>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<td>
|
||||
<strong>${noteLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<td>
|
||||
<strong>${operatorLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<td>
|
||||
<strong>${dateLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<td>
|
||||
<strong>${noteLabel}</strong>
|
||||
</td>
|
||||
${squawks}
|
||||
@@ -356,8 +680,8 @@ ${tool_data}
|
||||
</table>
|
||||
|
||||
<p style="line-height: 100%; margin-bottom: 0cm"><br/>
|
||||
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
@@ -366,11 +690,11 @@ ${tool_data}
|
||||
base_template = Template(
|
||||
"""
|
||||
<tr>
|
||||
<td style='border: 1px solid #dedede; padding: 0.05cm;'>
|
||||
%{key}
|
||||
<td>
|
||||
${key}
|
||||
</td>
|
||||
<td style='border: 1px solid #dedede; padding: 0.05cm;'>
|
||||
%{val}
|
||||
<td>
|
||||
${val}
|
||||
</td>
|
||||
</tr>
|
||||
"""
|
||||
@@ -379,16 +703,16 @@ base_template = Template(
|
||||
squawk_template = Template(
|
||||
"""
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<td>
|
||||
${squawkIcon}
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<td>
|
||||
${Operator}
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<td>
|
||||
${Date}
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm" colspan="3">
|
||||
<td colspan="3">
|
||||
${Note}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -397,68 +721,71 @@ squawk_template = Template(
|
||||
|
||||
tool_template = Template(
|
||||
"""
|
||||
<table cellpadding="2" cellspacing="2" bgcolor="#ffffff" style="background: #ffffff;">
|
||||
<div class="heading-container">
|
||||
<h3 id="_tool_data_T${toolNumber}">Tool Number: T${toolNumber}</h3><a href="#header" class="top-link">Top</a>
|
||||
</div>
|
||||
<table>
|
||||
<colgroup>
|
||||
<col width="350"/>
|
||||
<col width="350"/>
|
||||
<col width="350"/>
|
||||
<col width="30%"/>
|
||||
<col width="30%"/>
|
||||
<col width="40%"/>
|
||||
</colgroup>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<td>
|
||||
<strong>${descriptionLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<td>
|
||||
${description}
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<td class="image-container">
|
||||
${imagepath}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<td>
|
||||
<strong>${manufLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm" colspan="2">
|
||||
<td colspan="2">
|
||||
${manufacturer}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<td>
|
||||
<strong>${partNumberLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm" colspan="2">
|
||||
<td colspan="2">
|
||||
${partNumber}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<td>
|
||||
<strong>${urlLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm" colspan="2">
|
||||
<td colspan="2">
|
||||
${url}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<td>
|
||||
<strong>${shapeLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm" colspan="2">
|
||||
<td colspan="2">
|
||||
${shape}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<td>
|
||||
<strong>${inspectionNotesLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm" colspan="2">
|
||||
<td colspan="2">
|
||||
${inspectionNotes}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<td>
|
||||
<strong>${diameterLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm" colspan="2">
|
||||
<td colspan="2">
|
||||
${diameter}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -477,30 +804,30 @@ op_tool_template = Template(
|
||||
<col width="262"/>
|
||||
</colgroup>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<td>
|
||||
<strong>${opLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<td>
|
||||
<strong>${tcLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<td>
|
||||
<strong>${feedLabel}</strong>
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<td>
|
||||
<strong>${speedLabel}</strong>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<td>
|
||||
${Operation}
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<td>
|
||||
${ToolController}
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<td>
|
||||
${Feed}
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<td>
|
||||
${Speed}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -511,19 +838,19 @@ op_tool_template = Template(
|
||||
op_run_template = Template(
|
||||
"""
|
||||
<tr>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<td>
|
||||
${opName}
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<td>
|
||||
${minZ}
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<td>
|
||||
${maxZ}
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<td>
|
||||
${coolantMode}
|
||||
</td>
|
||||
<td style="border: 1px solid #dedede; padding: 0.05cm">
|
||||
<td>
|
||||
${cycleTime}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -532,6 +859,6 @@ op_run_template = Template(
|
||||
|
||||
tool_item_template = Template(
|
||||
"""
|
||||
<li class="subItem"><a class="customLink" href="#_tool_data_T${toolNumber}">T${toolNumber}-${description}</a></li>
|
||||
<li><a href="#_tool_data_T${toolNumber}">Tool Number: T${toolNumber}</a></li>
|
||||
"""
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2024 Ondsel <development@ondsel.com> *
|
||||
# * Copyright (c) 2025 Billy Huddleston <billy@ivdc.com> *
|
||||
# * *
|
||||
# * This file is part of the FreeCAD CAx development system. *
|
||||
# * *
|
||||
@@ -21,12 +22,13 @@
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
from PySide import QtGui
|
||||
from PySide import QtGui, QtCore
|
||||
import FreeCAD
|
||||
import FreeCADGui
|
||||
import Path.Log
|
||||
import os
|
||||
import time
|
||||
import tempfile
|
||||
|
||||
|
||||
if False:
|
||||
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
|
||||
@@ -39,7 +41,7 @@ class ImageBuilder:
|
||||
def __init__(self, file_path):
|
||||
self.file_path = file_path
|
||||
|
||||
def build_image(self, obj, image_name):
|
||||
def build_image(self, obj, image_name, as_bytes=False, view="default"):
|
||||
raise NotImplementedError("Subclass must implement abstract method")
|
||||
|
||||
def save_image(self, image):
|
||||
@@ -49,7 +51,6 @@ class ImageBuilder:
|
||||
class ImageBuilderFactory:
|
||||
@staticmethod
|
||||
def get_image_builder(file_path, **kwargs):
|
||||
|
||||
# return DummyImageBuilder(file_path, **kwargs)
|
||||
if FreeCAD.GuiUp:
|
||||
return GuiImageBuilder(file_path, **kwargs)
|
||||
@@ -63,7 +64,9 @@ class DummyImageBuilder(ImageBuilder):
|
||||
Path.Log.debug("Initializing dummyimagebuilder")
|
||||
super().__init__(file_path)
|
||||
|
||||
def build_image(self, obj, imageName):
|
||||
def build_image(self, obj, imageName, as_bytes=False, view="default"):
|
||||
if as_bytes:
|
||||
return b""
|
||||
return self.file_path
|
||||
|
||||
|
||||
@@ -85,7 +88,7 @@ class GuiImageBuilder(ImageBuilder):
|
||||
Path.Log.debug("Destroying GuiImageBuilder")
|
||||
self.restore_visibility()
|
||||
|
||||
def prepare_view(self, obj):
|
||||
def prepare_view(self, obj, view="default"):
|
||||
# Create a new view
|
||||
Path.Log.debug("CAM - Preparing view\n")
|
||||
|
||||
@@ -93,10 +96,13 @@ class GuiImageBuilder(ImageBuilder):
|
||||
num_windows = len(mw.getWindows())
|
||||
|
||||
# Create and configure the view
|
||||
view = FreeCADGui.ActiveDocument.createView("Gui::View3DInventor")
|
||||
view.setAnimationEnabled(False)
|
||||
view.viewIsometric()
|
||||
view.setCameraType("Perspective")
|
||||
view_obj = FreeCADGui.ActiveDocument.createView("Gui::View3DInventor")
|
||||
view_obj.setAnimationEnabled(False)
|
||||
view_obj.setCameraType("Orthographic")
|
||||
if view == "headon":
|
||||
view_obj.viewFront()
|
||||
else:
|
||||
view_obj.viewIsometric()
|
||||
|
||||
# Resize the window
|
||||
mdi = mw.findChild(QtGui.QMdiArea)
|
||||
@@ -104,10 +110,24 @@ class GuiImageBuilder(ImageBuilder):
|
||||
view_window.resize(500, 500)
|
||||
view_window.showMaximized()
|
||||
|
||||
FreeCADGui.Selection.clearSelection()
|
||||
|
||||
# First make everything invisible
|
||||
self.record_visibility()
|
||||
|
||||
# Then make only our target object visible and select it
|
||||
obj.Visibility = True
|
||||
FreeCADGui.Selection.clearSelection()
|
||||
FreeCADGui.Selection.addSelection(obj)
|
||||
FreeCADGui.Selection.clearSelection() # Clear so the selection highlight does not appear in the image
|
||||
|
||||
# Get the active view and fit to selection
|
||||
a_view = FreeCADGui.activeDocument().activeView()
|
||||
try:
|
||||
a_view.fitAll() # First fit all to ensure the object is in view
|
||||
FreeCADGui.updateGui()
|
||||
a_view.fitSelection() # Then try to fit to the selection
|
||||
except Exception:
|
||||
# If fitSelection fails, we already called fitAll
|
||||
pass
|
||||
|
||||
# Return the index of the new window (= old number of windows)
|
||||
return num_windows
|
||||
@@ -128,41 +148,90 @@ class GuiImageBuilder(ImageBuilder):
|
||||
for o in self.visible:
|
||||
o.Visibility = True
|
||||
|
||||
def build_image(self, obj, image_name):
|
||||
def build_image(self, obj, image_name, as_bytes=False, view="default"):
|
||||
Path.Log.debug("CAM - Building image\n")
|
||||
"""
|
||||
Makes an image of the target object. Returns filename.
|
||||
Makes an image of the target object. Returns either the image as bytes or a filename.
|
||||
"""
|
||||
|
||||
file_path = os.path.join(self.file_path, image_name)
|
||||
idx = self.prepare_view(obj, view=view)
|
||||
|
||||
idx = self.prepare_view(obj)
|
||||
|
||||
self.capture_image(file_path)
|
||||
self.destroy_view(idx)
|
||||
|
||||
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
|
||||
if as_bytes:
|
||||
# Capture directly to memory without writing to disk
|
||||
img_bytes = self.capture_image_to_bytes()
|
||||
self.destroy_view(idx)
|
||||
return img_bytes
|
||||
else:
|
||||
# Write to disk as before
|
||||
file_path = os.path.join(self.file_path, image_name)
|
||||
self.capture_image(file_path)
|
||||
self.destroy_view(idx)
|
||||
result = f"{file_path}_t.png"
|
||||
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")
|
||||
Path.Log.debug("CAM - capture image to file\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")
|
||||
# Generate higher resolution images - 800x800 pixels for better quality on high-DPI displays
|
||||
a_view.saveImage(file_path + ".png", 800, 800, "Current")
|
||||
a_view.saveImage(file_path + "_t.png", 800, 800, "Transparent")
|
||||
a_view.setAnimationEnabled(True)
|
||||
|
||||
def capture_image_to_bytes(self):
|
||||
"""Capture the current view directly to bytes without writing to disk"""
|
||||
FreeCADGui.updateGui()
|
||||
Path.Log.debug("CAM - capture image to bytes\n")
|
||||
a_view = FreeCADGui.activeDocument().activeView()
|
||||
|
||||
try:
|
||||
# Use FreeCAD's built-in method for getting a QImage directly
|
||||
# This approach is based on the same method the viewport uses internally
|
||||
qimg = a_view.grabFramebuffer()
|
||||
|
||||
# Convert QImage to bytes using QBuffer (purely in memory)
|
||||
buffer = QtCore.QBuffer()
|
||||
buffer.open(QtCore.QIODevice.WriteOnly)
|
||||
qimg.save(buffer, "PNG")
|
||||
img_bytes = buffer.data().data()
|
||||
buffer.close()
|
||||
|
||||
a_view.setAnimationEnabled(True)
|
||||
return img_bytes
|
||||
|
||||
except Exception as e:
|
||||
# Fallback to temporary file approach if the direct method fails
|
||||
Path.Log.debug(f"Direct image capture failed: {e}, using fallback method")
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as temp:
|
||||
temp_path = temp.name
|
||||
|
||||
# saveImage doesn't write to memory directly, so we need to use a temporary file
|
||||
# Generate higher resolution images - 800x800 pixels for better quality on high-DPI displays
|
||||
a_view.saveImage(temp_path, 800, 800, "Transparent")
|
||||
|
||||
# Read the temporary file into memory
|
||||
with open(temp_path, "rb") as f:
|
||||
img_bytes = f.read()
|
||||
|
||||
# Clean up the temporary file
|
||||
try:
|
||||
os.unlink(temp_path)
|
||||
except Exception:
|
||||
# Ignore errors during temporary file cleanup, as failure to delete is non-critical
|
||||
pass
|
||||
|
||||
a_view.setAnimationEnabled(True)
|
||||
return img_bytes
|
||||
|
||||
|
||||
class NonGuiImageBuilder(ImageBuilder):
|
||||
def __init__(self, file_path):
|
||||
super().__init__(file_path)
|
||||
Path.Log.debug("nonguiimagebuilder")
|
||||
|
||||
def build_image(self, obj, image_name):
|
||||
def build_image(self, obj, image_name, as_bytes=False, view="default"):
|
||||
"""
|
||||
Generates a headless picture of a 3D object and saves it as a PNG and optionally a PostScript file.
|
||||
|
||||
@@ -212,20 +281,20 @@ class NonGuiImageBuilder(ImageBuilder):
|
||||
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")
|
||||
if as_bytes:
|
||||
qimg = off.getQImage()
|
||||
buffer = QtCore.QBuffer()
|
||||
buffer.open(QtCore.QIODevice.WriteOnly)
|
||||
qimg.save(buffer, "PNG")
|
||||
return buffer.data().data()
|
||||
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
|
||||
if off.isWriteSupported("PNG"):
|
||||
file_path = f"{self.file_path}{os.path.sep}{image_name}.png"
|
||||
off.writeToFile(file_path, "PNG")
|
||||
return file_path
|
||||
else:
|
||||
Path.Log.debug("PNG format is not supported.")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"An error occurred: {e}")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2024 Ondsel <development@ondsel.com> *
|
||||
# * Copyright (c) 2025 Billy Huddleston <billy@ivdc.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) *
|
||||
@@ -46,6 +47,18 @@ else:
|
||||
|
||||
|
||||
class ReportGenerator:
|
||||
def bytes_to_base64_with_tag(self, image_bytes, mime_type="image/png", alt="Image"):
|
||||
"""
|
||||
Takes image bytes and returns (base64_string, <img> tag) for embedding in HTML.
|
||||
Default mime_type is image/png.
|
||||
"""
|
||||
|
||||
if not image_bytes:
|
||||
return "", ""
|
||||
encoded_string = base64.b64encode(image_bytes).decode()
|
||||
html_tag = f'<img src="data:{mime_type};base64,{encoded_string}" alt="{alt}" />'
|
||||
return encoded_string, html_tag
|
||||
|
||||
def __init__(self, data, embed_images=False):
|
||||
self.embed_images = embed_images
|
||||
self.squawks = ""
|
||||
@@ -128,7 +141,12 @@ class ReportGenerator:
|
||||
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)
|
||||
if isinstance(val, bytes):
|
||||
encoded_image, tag = self.bytes_to_base64_with_tag(
|
||||
val, mime_type="image/png", alt=key
|
||||
)
|
||||
else:
|
||||
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} />"
|
||||
@@ -140,13 +158,20 @@ class ReportGenerator:
|
||||
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"] != "":
|
||||
# Prefer imagebytes for embedding if present
|
||||
if (
|
||||
self.embed_images
|
||||
and "imagebytes" in toolAttributes
|
||||
and toolAttributes["imagebytes"]
|
||||
):
|
||||
_, tag = self.bytes_to_base64_with_tag(
|
||||
toolAttributes["imagebytes"], mime_type="image/png", alt=key
|
||||
)
|
||||
toolAttributes["imagepath"] = tag
|
||||
elif "imagepath" in toolAttributes and toolAttributes["imagepath"] != "":
|
||||
if self.embed_images:
|
||||
encoded_image, tag = self.file_to_base64_with_tag(
|
||||
toolAttributes["imagepath"]
|
||||
@@ -165,7 +190,6 @@ class ReportGenerator:
|
||||
# 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":
|
||||
@@ -181,6 +205,8 @@ class ReportGenerator:
|
||||
|
||||
def _format_tool(self, tool_number, tool_data):
|
||||
td = {}
|
||||
td["toolNumber"] = tool_number
|
||||
|
||||
for key, val in tool_data.items():
|
||||
if key == "squawkData":
|
||||
self._format_squawks(val)
|
||||
@@ -200,8 +226,8 @@ class ReportGenerator:
|
||||
|
||||
def _format_bases(self, base_data):
|
||||
bases = ""
|
||||
for base in base_data:
|
||||
bases += base_template.substitute(base)
|
||||
for key, val in base_data.items():
|
||||
bases += base_template.substitute({"key": key, "val": val})
|
||||
self.formatted_data["bases"] = bases
|
||||
|
||||
def _format_squawks(self, squawk_data):
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2016 sliptonic <shopinthewoods@gmail.com> *
|
||||
# * Copyright (c) 2025 Billy Huddleston <billy@ivdc.com> *
|
||||
# * *
|
||||
# * This file is part of the FreeCAD CAx development system. *
|
||||
# * *
|
||||
@@ -36,7 +37,6 @@ import Path.Log
|
||||
import Path.Main.Sanity.ImageBuilder as ImageBuilder
|
||||
import Path.Main.Sanity.ReportGenerator as ReportGenerator
|
||||
import os
|
||||
import tempfile
|
||||
import Path.Dressup.Utils as PathDressup
|
||||
|
||||
translate = FreeCAD.Qt.translate
|
||||
@@ -49,6 +49,9 @@ else:
|
||||
|
||||
|
||||
class CAMSanity:
|
||||
# Toggle: True = use thumbnail, False = always use fallback image
|
||||
USE_TOOL_THUMBNAIL = False
|
||||
|
||||
"""
|
||||
This class has the functionality to harvest data from a CAM Job
|
||||
and export it in a format that is useful to the user.
|
||||
@@ -102,8 +105,9 @@ class CAMSanity:
|
||||
|
||||
path = f"{FreeCAD.getHomePath()}Mod/CAM/Path/Main/Sanity/{squawk_icon}.svg"
|
||||
|
||||
local_date_str = date.strftime("%c")
|
||||
squawk = {
|
||||
"Date": str(date),
|
||||
"Date": local_date_str,
|
||||
"Operator": operator,
|
||||
"Note": note,
|
||||
"squawkType": squawkType,
|
||||
@@ -120,7 +124,9 @@ class CAMSanity:
|
||||
[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["baseimage"] = self.image_builder.build_image(
|
||||
obj.Model, "baseimage", as_bytes=True
|
||||
)
|
||||
data["bases"] = bases
|
||||
|
||||
return data
|
||||
@@ -146,7 +152,15 @@ class CAMSanity:
|
||||
data["FileName"] = obj.Document.FileName
|
||||
data["LastModifiedDate"] = str(obj.Document.LastModifiedDate)
|
||||
data["Customer"] = obj.Document.Company
|
||||
data["Designer"] = obj.Document.LastModifiedBy
|
||||
lastmod = obj.Document.LastModifiedDate
|
||||
if lastmod:
|
||||
try:
|
||||
# Parse ISO 8601 string and format
|
||||
data["LastModifiedDate"] = datetime.fromisoformat(str(lastmod)).strftime("%c")
|
||||
except Exception:
|
||||
data["LastModifiedDate"] = str(lastmod)
|
||||
else:
|
||||
data["LastModifiedDate"] = ""
|
||||
data["JobDescription"] = obj.Description
|
||||
data["JobLabel"] = obj.Label
|
||||
|
||||
@@ -170,7 +184,7 @@ class CAMSanity:
|
||||
data["fixtures"] = str(obj.Fixtures)
|
||||
data["orderBy"] = str(obj.OrderOutputBy)
|
||||
|
||||
data["datumImage"] = self.image_builder.build_image(obj, "datumImage")
|
||||
data["datumImage"] = self.image_builder.build_image(obj, "datumImage", as_bytes=True)
|
||||
|
||||
return data
|
||||
|
||||
@@ -337,7 +351,7 @@ class CAMSanity:
|
||||
)
|
||||
)
|
||||
|
||||
data["stockImage"] = self.image_builder.build_image(obj.Stock, "stockImage")
|
||||
data["stockImage"] = self.image_builder.build_image(obj.Stock, "stockImage", as_bytes=True)
|
||||
|
||||
return data
|
||||
|
||||
@@ -384,34 +398,49 @@ class CAMSanity:
|
||||
tooldata["manufacturer"] = ""
|
||||
tooldata["url"] = ""
|
||||
tooldata["inspectionNotes"] = ""
|
||||
tooldata["diameter"] = str(TC.Tool.Diameter)
|
||||
tooldata["diameter"] = str(TC.Tool.Diameter.UserString)
|
||||
tooldata["shape"] = TC.Tool.ShapeType
|
||||
|
||||
tooldata["partNumber"] = ""
|
||||
|
||||
if os.path.isfile(TC.Tool.ShapeType):
|
||||
imagedata = TC.Tool.Proxy.get_thumbnail()
|
||||
else:
|
||||
imagedata = None
|
||||
data["squawkData"].append(
|
||||
self.squawk(
|
||||
"CAMSanity",
|
||||
translate("CAM_Sanity", "Toolbit Shape for TC: {} not found").format(
|
||||
TC.ToolNumber
|
||||
),
|
||||
squawkType="WARNING",
|
||||
# Use the toggle to determine which image to use
|
||||
imagebytes = None
|
||||
if self.USE_TOOL_THUMBNAIL:
|
||||
# Try to get the thumbnail
|
||||
thumb_bytes = None
|
||||
if hasattr(TC.Tool, "Proxy") and hasattr(TC.Tool.Proxy, "get_thumbnail"):
|
||||
try:
|
||||
thumb_bytes = TC.Tool.Proxy.get_thumbnail()
|
||||
except Exception:
|
||||
thumb_bytes = None
|
||||
if thumb_bytes:
|
||||
imagebytes = thumb_bytes
|
||||
else:
|
||||
# Warn and use fallback head-on image
|
||||
data["squawkData"].append(
|
||||
self.squawk(
|
||||
"CAMSanity",
|
||||
translate("CAM_Sanity", "Toolbit Shape for TC: {} not found").format(
|
||||
TC.ToolNumber
|
||||
),
|
||||
squawkType="WARNING",
|
||||
)
|
||||
)
|
||||
imagebytes = self.image_builder.build_image(
|
||||
TC.Tool, f"T{TC.ToolNumber}", as_bytes=True, view="headon"
|
||||
)
|
||||
else:
|
||||
# Always use fallback head-on image
|
||||
imagebytes = self.image_builder.build_image(
|
||||
TC.Tool, f"T{TC.ToolNumber}", as_bytes=True, view="headon"
|
||||
)
|
||||
tooldata["image"] = ""
|
||||
tooldata["imagebytes"] = imagebytes
|
||||
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()
|
||||
# No longer writing imagedata to disk; handled by imagebytes logic above
|
||||
|
||||
tooldata["feedrate"] = str(TC.HorizFeed)
|
||||
tooldata["feedrate"] = str(TC.HorizFeed.UserString)
|
||||
if TC.HorizFeed.Value == 0.0:
|
||||
data["squawkData"].append(
|
||||
self.squawk(
|
||||
@@ -423,7 +452,7 @@ class CAMSanity:
|
||||
)
|
||||
)
|
||||
|
||||
tooldata["spindlespeed"] = str(TC.SpindleSpeed)
|
||||
tooldata["spindlespeed"] = f"{int(TC.SpindleSpeed)} rpm"
|
||||
if TC.SpindleSpeed == 0.0:
|
||||
data["squawkData"].append(
|
||||
self.squawk(
|
||||
@@ -444,8 +473,8 @@ class CAMSanity:
|
||||
{
|
||||
"Operation": base_op.Label,
|
||||
"ToolController": TC.Label,
|
||||
"Feed": str(TC.HorizFeed),
|
||||
"Speed": str(TC.SpindleSpeed),
|
||||
"Feed": str(TC.HorizFeed.UserString),
|
||||
"Speed": f"{int(TC.SpindleSpeed)} rpm",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user