feat(map): add min/max highlighting toggle and live safeguards

- Add "Highlight Min/Max" option to ReadoutPanel showing peak rings.
- Reset to review mode and blank wafer map when switching away from map tab.
- Disable hover highlights and sensor dialog triggers during live stream.
- Lower trendPane height and add top margin headroom for tick labels.
- Add test coverage for blanking states, toggle overrides, and interactions.
This commit is contained in:
jack
2026-07-10 16:28:55 -07:00
parent 034f13b717
commit 25fa7507ce
9 changed files with 103 additions and 13 deletions
+3
View File
@@ -23,6 +23,9 @@ Rectangle {
if (selectedTabIndex === 2) {
file_browser.refreshFiles();
streamController.unloadFile()
} else if (streamController.mode === "live") {
// Leaving the map tab mid-stream: stop and blank the live map.
streamController.setMode("review")
}
}
+6 -3
View File
@@ -144,7 +144,7 @@ Item {
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 8
spacing: 16
WaferMapView {
id: waferView
@@ -154,14 +154,15 @@ Item {
Layout.minimumHeight: 280
blend: readoutPanel.heatmapBlend
showLabels: readoutPanel.showLabels
showExtremes: readoutPanel.showExtremes
showThickness: readoutPanel.showThickness
}
Rectangle {
id: trendPane
Layout.fillWidth: true
Layout.fillHeight: true
Layout.fillHeight: false
Layout.preferredHeight: 220
Layout.minimumHeight: 160
Layout.minimumHeight: 120
visible: trendChart.hasData && streamController.mode === "live"
color: Theme.cardBackground
border.color: Theme.cardBorder
@@ -183,9 +184,11 @@ Item {
Item {
Layout.fillWidth: true
visible: transportBar.hasContent
implicitHeight: trendPane.visible ? 52 : 16
}
TransportBar {
id: transportBar
Layout.fillWidth: true
}
}
@@ -12,6 +12,7 @@ ColumnLayout {
spacing: 6
property var s: streamController.stats
property alias showLabels: labelsToggle.checked
property alias showExtremes: extremesToggle.checked
property alias heatmapBlend: heatmapSlider.value
property alias showThickness: thicknessToggle.checked
@@ -353,6 +354,13 @@ ColumnLayout {
font.pixelSize: Theme.fontSm
}
PanelCheckBox {
id: extremesToggle
text: "Highlight Min/Max"
checked: true
font.pixelSize: Theme.fontSm
}
PanelCheckBox {
id: clusterAverageToggle
text: "Average Clusters"
@@ -7,6 +7,7 @@ Item {
id: root
property real blend: 0.0
property bool showLabels: true
property alias showExtremes: map.showExtremes
property alias showThickness: map.showThickness
WaferMapItem {
@@ -39,7 +40,9 @@ Item {
HoverHandler {
id: hoverHandler
onPointChanged: {
map.hoveredIndex = map.which_marker(hoverHandler.point.position.x, hoverHandler.point.position.y);
map.hoveredIndex = streamController.mode === "live"
? -1
: map.which_marker(hoverHandler.point.position.x, hoverHandler.point.position.y);
}
onHoveredChanged: if (!hoverHandler.hovered) map.hoveredIndex = -1
cursorShape: map.hoveredIndex >= 0 ? Qt.PointingHandCursor : Qt.ArrowCursor
@@ -47,6 +50,8 @@ Item {
TapHandler {
onTapped: ev => {
if (streamController.mode === "live")
return;
var idx = map.which_marker(ev.position.x, ev.position.y);
if (idx >= 0)
replaceDialog.openFor(idx);
@@ -225,6 +225,12 @@ class SessionController(QObject):
return
if mode != "live":
self.stopStream()
if self._mode == "live":
# Leaving a live session: blank the wafer map instead of
# freezing the last streamed frame on screen.
self._last = None
self._last_raw_frame = None
self.frameUpdated.emit()
self._play_timer.stop()
self._mode = mode
self.modeChanged.emit()
@@ -58,6 +58,9 @@ class TrendChartItem(QQuickPaintedItem):
# `_padding` alone — that mismatch is what clipped y-axis text before.
self._margin_left: int = 48
self._margin_bottom: int = 30
# Top tick label is drawn centered on the plot's top edge (y_px - 7),
# so the plot needs headroom or the label kisses the card border.
self._margin_top: int = 10
self._line_color = QColor("#5B9DF5")
self._grid_color = QColor("#2A3441")
self._text_color = QColor("#CBD5E1")
@@ -178,10 +181,11 @@ class TrendChartItem(QQuickPaintedItem):
return
left = self._padding + self._margin_left
top = self._padding + self._margin_top
bottom_inset = self._padding + self._margin_bottom
plot_rect = QRectF(left, self._padding,
plot_rect = QRectF(left, top,
max(1, w - left - self._padding),
max(1, h - self._padding - bottom_inset))
max(1, h - top - bottom_inset))
self._draw_grid(painter, plot_rect)
@@ -62,6 +62,7 @@ class WaferMapItem(QQuickPaintedItem):
marginChanged = Signal()
blendChanged = Signal()
showLabelsChanged = Signal()
showExtremesChanged = Signal()
colorsChanged = Signal()
shapeChanged = Signal()
sizeChanged = Signal()
@@ -80,6 +81,7 @@ class WaferMapItem(QQuickPaintedItem):
self._margin: float = 1.0
self._blend: float = 0.0
self._show_labels: bool = True
self._show_extremes: bool = True
self._shape: str = "round"
self._size: float = 300.0
self._thickness_data: list[float] = []
@@ -199,6 +201,16 @@ class WaferMapItem(QQuickPaintedItem):
self.showLabelsChanged.emit()
self.update()
@Property(bool, notify=showExtremesChanged)
def showExtremes(self) -> bool:
return self._show_extremes
@showExtremes.setter # type: ignore[no-redef]
def showExtremes(self, val: bool) -> None:
self._show_extremes = bool(val)
self.showExtremesChanged.emit()
self.update()
@Property(str, notify=shapeChanged)
def shape(self) -> str:
return self._shape
@@ -340,7 +352,6 @@ class WaferMapItem(QQuickPaintedItem):
img.fill(0) # transparent background
painter = QPainter(img)
self.paint(painter)
self._paint_extremes(painter)
if lines:
painter.fillRect(QRectF(0, h, w, footer_h), QColor("#101014"))
painter.setPen(QColor("#CBD5E1"))
@@ -357,12 +368,8 @@ class WaferMapItem(QQuickPaintedItem):
# ── internal ─────────────────────────────────────────────────────────
def _paint_extremes(self, painter: QPainter) -> None:
"""Ring the hottest (high color) and coldest (low color) markers.
Export-only overlay paint() itself stays untouched so the live
view is unchanged.
"""
if not self._values or not self._markers:
"""Ring the hottest (high color) and coldest (low color) markers."""
if not self._show_extremes or not self._values or not self._markers:
return
s = compute_stats(self._values)
ring_r = self._marker_r + 5
@@ -606,6 +613,7 @@ class WaferMapItem(QQuickPaintedItem):
painter.setOpacity(1.0)
self._paint_markers(painter)
self._paint_extremes(painter)
def _paint_template(self, painter: QPainter, cx: int, cy: int, r_px: int) -> None:
ds = self._draw_size()
+23
View File
@@ -53,6 +53,29 @@ def test_pause_stop_timer(controller):
controller.stop()
assert controller.frameIndex == 0
def test_leaving_live_mode_blanks_wafer_map(controller):
"""Stopping a live session (tab switch → setMode) must not leave stale
sensor values on the wafer map it should read as blank."""
frame_signal = MagicMock()
controller.frameUpdated.connect(frame_signal)
controller.setMode("live")
controller._on_live_frame(Frame(seq=1, time=0.0, values=[100.0] * 22))
assert controller.sensorValues, "sanity: live frame should populate the map"
controller.setMode("review")
assert controller.sensorValues == []
assert controller.stats == {}
assert frame_signal.called, "QML needs frameUpdated to repaint blank"
def test_leaving_live_mode_stops_stream(controller):
controller.setMode("live")
controller.setMode("review")
assert controller._reader is None
assert not controller._repaint_timer.isActive()
def test_compare_files_integration(controller):
# Mock the comparisonResult signal
mock_slot = MagicMock()
+30
View File
@@ -161,6 +161,36 @@ def test_export_image_highlights_hottest_and_coldest(tmp_path):
assert "#5b9df5" in found # coldest ring (sensorLow)
def test_show_extremes_toggle_gates_peak_rings(tmp_path):
"""showExtremes (Display setting) hides the hot/cold rings when off.
Same pixel probe as the highlight test: with rings off, no high/low
colored pixels should appear anywhere."""
from PySide6.QtGui import QImage
from pygui.backend.visualization.wafer_map_item import WaferMapItem
item = WaferMapItem()
assert item.showExtremes is True # rings on by default
item.setWidth(200)
item.setHeight(200)
item.sensors = [
{"label": "1", "x": -50.0, "y": 0.0},
{"label": "2", "x": 0.0, "y": 0.0},
{"label": "3", "x": 50.0, "y": 0.0},
]
item.values = [10.0, 20.0, 30.0]
item.showExtremes = False
out = tmp_path / "wafer.png"
assert item.export_image(str(out)) is True
img = QImage(str(out))
found = {img.pixelColor(x, y).name()
for y in range(img.height()) for x in range(img.width())}
assert "#ef4444" not in found
assert "#5b9df5" not in found
def test_readout_text_full_stats():
"""Export footer readout carries every READOUT stat with sensor indices.