feat(map): add batch export and adjust layout alignment

- Implement synchronous offscreen wafer map PNG rendering and CSV summary.
- Add "Batch Export" button to file browser sidebar.
- Adjust trend pane bottom spacer to align with About button when visible.
- Fix type check errors and add pytest coverage for batch export.
This commit is contained in:
jack
2026-07-10 15:22:36 -07:00
parent 278a48a2e4
commit 69753e35f9
12 changed files with 422 additions and 187 deletions
@@ -14,7 +14,7 @@ import logging
import math
import numpy as np
from PySide6.QtCore import Property, QPoint, Qt, Signal, Slot
from PySide6.QtCore import Property, QPoint, QRectF, Qt, Signal, Slot
from PySide6.QtGui import (
QBrush,
QColor,
@@ -310,17 +310,42 @@ class WaferMapItem(QQuickPaintedItem):
Returns:
True on success, False on failure
"""
if not file_path:
if not file_path or not self._values:
# No file loaded / no stream played yet — only the empty template
# would render, which isn't a meaningful export.
return False
try:
result = self.grabToImage()
img = result.image() # type: ignore[attr-defined]
img.save(file_path, "PNG")
return True
# Render synchronously via paint() into our own image —
# grabToImage() resolves on a later render frame, so its result
# is null when read immediately (the old silent failure).
w, h = int(self.width()), int(self.height())
if w <= 0 or h <= 0:
return False
metrics = self._metrics_text()
footer_h = 28 if metrics else 0
img = QImage(w, h + footer_h, QImage.Format.Format_ARGB32)
img.fill(0) # transparent background
painter = QPainter(img)
self.paint(painter)
if metrics:
footer = QRectF(0, h, w, footer_h)
painter.fillRect(footer, QColor("#101014"))
painter.setPen(QColor("#CBD5E1"))
painter.drawText(footer, Qt.AlignmentFlag.AlignCenter, metrics)
painter.end()
return bool(img.save(file_path))
except Exception as e:
log.error("Export failed: %s", e)
return False
def _metrics_text(self) -> str:
"""One-line metric readout for the export footer; '' when no data."""
if not self._values:
return ""
vs = self._values
return (f"Sensors: {len(vs)} Min: {min(vs):.2f}°C "
f"Max: {max(vs):.2f}°C Avg: {sum(vs) / len(vs):.2f}°C")
# ── internal ─────────────────────────────────────────────────────────
def _rebuild_thickness(self) -> None: