feat(wafer-map): add threshold legend overlay and scrollbar polish

- Add color legend (Low/In-range/High + Auto/Manual mode)
  as a floating overlay in WaferMapView
- Expose thresholdAuto on SessionController/SessionModel
  to drive the legend's Auto · / Manual · label
- Style ReadoutPanel scrollbar (always-visible, 8px thumb)
  and reserve 14px gap so cards don't overlap it
This commit is contained in:
jack
2026-07-11 21:57:57 -07:00
parent f9c6639a66
commit 35cee98579
6 changed files with 216 additions and 120 deletions
+28 -4
View File
@@ -193,18 +193,42 @@ Item {
} }
} }
// Readout panel — scrolls instead of clipping so the last card // Readout panel — scrolls instead of clipping so no card is lost
// (Thresholds) is always reachable even if content outgrows // if content outgrows the available height (e.g. once E-C delta
// the available height (e.g. once E-C delta rows appear). // rows appear). Thresholds sits first so it's visible unscrolled.
ScrollView { ScrollView {
id: readoutScroll
Layout.preferredWidth: Theme.rightRailWidth Layout.preferredWidth: Theme.rightRailWidth
Layout.fillHeight: true Layout.fillHeight: true
clip: true clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical: ScrollBar {
parent: readoutScroll
anchors.top: readoutScroll.top
anchors.right: readoutScroll.right
anchors.bottom: readoutScroll.bottom
anchors.leftMargin: 6
policy: ScrollBar.AlwaysOn
contentItem: Rectangle {
implicitWidth: 8
implicitHeight: 8
radius: Theme.radiusSm
color: Theme.trackBackground
}
background: Rectangle {
implicitWidth: 8
color: "transparent"
}
}
ReadoutPanel { ReadoutPanel {
id: readoutPanel id: readoutPanel
width: parent.width // Reserve a gap before the always-visible scrollbar so
// card content doesn't sit flush against it.
width: readoutScroll.availableWidth - 14
hasThicknessData: waferView.hasThickness hasThicknessData: waferView.hasThickness
onExportRequested: function(filePath, extra) { onExportRequested: function(filePath, extra) {
waferView.exportImage(filePath, extra); waferView.exportImage(filePath, extra);
+119 -116
View File
@@ -5,10 +5,13 @@ import QtQuick.Layouts
import ISC import ISC
// ===== Readout Panel ===== // ===== Readout Panel =====
// Right-rail panel with three bento-tile sections: READOUT, DISPLAY, // Right-rail panel with three bento-tile sections: THRESHOLDS, DISPLAY,
// THRESHOLDS. Uses the same PanelBox / SectionTitle / ReadoutStat / // READOUT. Thresholds comes first so Set Point/Margin/Auto are visible
// PanelCheckBox / PanelSlider components as the Data tab's // without scrolling on first use. The whole panel scrolls as one column
// ComparisonSidePanel, so both right rails share one visual language. // (see WaferMapTab.qml) when content outgrows the available height.
// Uses the same PanelBox / SectionTitle / ReadoutStat / PanelCheckBox /
// PanelSlider components as the Data tab's ComparisonSidePanel, so both
// right rails share one visual language.
ColumnLayout { ColumnLayout {
id: root id: root
spacing: 6 spacing: 6
@@ -47,63 +50,85 @@ ColumnLayout {
} }
} }
// ── READOUT ───────────────────────────────────────────────────── // ── THRESHOLDS ──────────────────────────────────────────────────
PanelBox { PanelBox {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: readoutCol.implicitHeight + 24 Layout.preferredHeight: thresholdCard.implicitHeight + 24
ColumnLayout { ColumnLayout {
id: readoutCol id: thresholdCard
anchors.fill: parent anchors.fill: parent
anchors.margins: 12 anchors.margins: 12
spacing: 8 spacing: 6
SectionTitle { text: "READOUT" } SectionTitle { text: "THRESHOLDS" }
ReadoutStat { Label {
label: "Min Temp" text: "Set Point (°C)"
value: s.min !== undefined color: Theme.bodyColor
? s.min + ((s.minIndex !== undefined && s.minIndex >= 0) ? " #" + s.minIndex : "") font.pixelSize: Theme.fontSm
: "—"
valueColor: Theme.sensorLow
} }
ReadoutStat { TextField {
label: "Max Temp" id: spField
value: s.max !== undefined Layout.fillWidth: true
? s.max + ((s.maxIndex !== undefined && s.maxIndex >= 0) ? " #" + s.maxIndex : "") text: "149.0"
: "—" font.pixelSize: Theme.fontSm
valueColor: Theme.sensorHigh inputMethodHints: Qt.ImhFormattedNumbersOnly
color: Theme.fieldText
placeholderTextColor: Theme.fieldPlaceholder
selectedTextColor: Theme.fieldBackground
selectionColor: Theme.fieldText
background: Rectangle {
radius: Theme.radiusXs
color: Theme.fieldBackground
border.width: spField.activeFocus ? Theme.borderStrong : Theme.borderThin
border.color: spField.activeFocus ? Theme.fieldBorderFocus : Theme.fieldBorder
Behavior on color {
ColorAnimation { duration: Theme.durationFast }
}
Behavior on border.color {
ColorAnimation { duration: Theme.durationFast }
}
}
onEditingFinished: pushThresholds()
} }
ReadoutStat {
label: "Diff" Label {
value: s.diff !== undefined ? s.diff : "—" text: "Margin (±°C)"
valueColor: Theme.diffAccent color: Theme.bodyColor
font.pixelSize: Theme.fontSm
} }
ReadoutStat { TextField {
label: "Average" id: mgField
value: s.avg !== undefined ? s.avg : "—" Layout.fillWidth: true
text: "1.0"
font.pixelSize: Theme.fontSm
inputMethodHints: Qt.ImhFormattedNumbersOnly
color: Theme.fieldText
placeholderTextColor: Theme.fieldPlaceholder
selectedTextColor: Theme.fieldBackground
selectionColor: Theme.fieldText
background: Rectangle {
radius: Theme.radiusXs
color: Theme.fieldBackground
border.width: mgField.activeFocus ? Theme.borderStrong : Theme.borderThin
border.color: mgField.activeFocus ? Theme.fieldBorderFocus : Theme.fieldBorder
Behavior on color {
ColorAnimation { duration: Theme.durationFast }
}
Behavior on border.color {
ColorAnimation { duration: Theme.durationFast }
}
}
onEditingFinished: pushThresholds()
} }
ReadoutStat {
label: "Sigma (Σ)" PanelCheckBox {
value: s.sigma !== undefined ? s.sigma : "—" id: autoCheck
} text: "Auto range (mean ± 1σ)"
ReadoutStat { checked: true
label: "3Σ Value" font.pixelSize: Theme.fontSm
value: s.threeSigma !== undefined ? s.threeSigma : "—" onCheckedChanged: pushThresholds()
}
ReadoutStat {
visible: s.ecMinDelta !== undefined
label: "E-C Δ Min"
value: s.ecMinDelta !== undefined
? s.ecMinDelta.toFixed(2) + " (#" + s.ecMinEdgeIndex + "→#" + s.ecMinCenterIndex + ")"
: "—"
}
ReadoutStat {
visible: s.ecMaxDelta !== undefined
label: "E-C Δ Max"
value: s.ecMaxDelta !== undefined
? s.ecMaxDelta.toFixed(2) + " (#" + s.ecMaxEdgeIndex + "→#" + s.ecMaxCenterIndex + ")"
: "—"
} }
} }
} }
@@ -203,85 +228,63 @@ ColumnLayout {
} }
} }
// ── THRESHOLDS ────────────────────────────────────────────────── // ── READOUT ─────────────────────────────────────────────────────
PanelBox { PanelBox {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: thresholdCard.implicitHeight + 24 Layout.preferredHeight: readoutCol.implicitHeight + 24
ColumnLayout { ColumnLayout {
id: thresholdCard id: readoutCol
anchors.fill: parent anchors.fill: parent
anchors.margins: 12 anchors.margins: 12
spacing: 6 spacing: 8
SectionTitle { text: "THRESHOLDS" } SectionTitle { text: "READOUT" }
Label { ReadoutStat {
text: "Set Point (°C)" label: "Min Temp"
color: Theme.bodyColor value: s.min !== undefined
font.pixelSize: Theme.fontSm ? s.min + ((s.minIndex !== undefined && s.minIndex >= 0) ? " #" + s.minIndex : "")
: "—"
valueColor: Theme.sensorLow
} }
TextField { ReadoutStat {
id: spField label: "Max Temp"
Layout.fillWidth: true value: s.max !== undefined
text: "149.0" ? s.max + ((s.maxIndex !== undefined && s.maxIndex >= 0) ? " #" + s.maxIndex : "")
font.pixelSize: Theme.fontSm : "—"
inputMethodHints: Qt.ImhFormattedNumbersOnly valueColor: Theme.sensorHigh
color: Theme.fieldText
placeholderTextColor: Theme.fieldPlaceholder
selectedTextColor: Theme.fieldBackground
selectionColor: Theme.fieldText
background: Rectangle {
radius: Theme.radiusXs
color: Theme.fieldBackground
border.width: spField.activeFocus ? Theme.borderStrong : Theme.borderThin
border.color: spField.activeFocus ? Theme.fieldBorderFocus : Theme.fieldBorder
Behavior on color {
ColorAnimation { duration: Theme.durationFast }
}
Behavior on border.color {
ColorAnimation { duration: Theme.durationFast }
}
}
onEditingFinished: pushThresholds()
} }
ReadoutStat {
Label { label: "Diff"
text: "Margin (±°C)" value: s.diff !== undefined ? s.diff : "—"
color: Theme.bodyColor valueColor: Theme.diffAccent
font.pixelSize: Theme.fontSm
} }
TextField { ReadoutStat {
id: mgField label: "Average"
Layout.fillWidth: true value: s.avg !== undefined ? s.avg : "—"
text: "1.0"
font.pixelSize: Theme.fontSm
inputMethodHints: Qt.ImhFormattedNumbersOnly
color: Theme.fieldText
placeholderTextColor: Theme.fieldPlaceholder
selectedTextColor: Theme.fieldBackground
selectionColor: Theme.fieldText
background: Rectangle {
radius: Theme.radiusXs
color: Theme.fieldBackground
border.width: mgField.activeFocus ? Theme.borderStrong : Theme.borderThin
border.color: mgField.activeFocus ? Theme.fieldBorderFocus : Theme.fieldBorder
Behavior on color {
ColorAnimation { duration: Theme.durationFast }
}
Behavior on border.color {
ColorAnimation { duration: Theme.durationFast }
}
}
onEditingFinished: pushThresholds()
} }
ReadoutStat {
PanelCheckBox { label: "Sigma (Σ)"
id: autoCheck value: s.sigma !== undefined ? s.sigma : "—"
text: "Auto range (mean ± 1σ)" }
checked: true ReadoutStat {
font.pixelSize: Theme.fontSm label: "3Σ Value"
onCheckedChanged: pushThresholds() value: s.threeSigma !== undefined ? s.threeSigma : "—"
}
ReadoutStat {
visible: s.ecMinDelta !== undefined
label: "E-C Δ Min"
value: s.ecMinDelta !== undefined
? s.ecMinDelta.toFixed(2) + " (#" + s.ecMinEdgeIndex + "→#" + s.ecMinCenterIndex + ")"
: "—"
}
ReadoutStat {
visible: s.ecMaxDelta !== undefined
label: "E-C Δ Max"
value: s.ecMaxDelta !== undefined
? s.ecMaxDelta.toFixed(2) + " (#" + s.ecMaxEdgeIndex + "→#" + s.ecMaxCenterIndex + ")"
: "—"
} }
} }
} }
@@ -5,6 +5,7 @@ import ISC
Dialog { Dialog {
id: root id: root
parent: Overlay.overlay
title: "Sensor Override" title: "Sensor Override"
modal: true modal: true
standardButtons: Dialog.NoButton standardButtons: Dialog.NoButton
@@ -1,5 +1,6 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts
import ISC import ISC
import ISC.Wafer import ISC.Wafer
@@ -64,6 +65,64 @@ Item {
id: replaceDialog id: replaceDialog
} }
// ── Threshold legend: what Low/In-range/High mean, and whether the
// band is auto-derived (frame mean ± 1σ) or the manual set point.
// Same computation in live and review, so no mode-specific text needed —
// the resolved values below just reflect whichever frame is current.
Rectangle {
id: thresholdLegend
visible: streamController.sensorValues.length > 0
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: 10
radius: Theme.radiusSm
color: Theme.sidePanelBackground
opacity: 0.9
border.color: Theme.sideBorder
border.width: Theme.borderThin
implicitWidth: legendRow.implicitWidth + 20
implicitHeight: legendRow.implicitHeight + 12
RowLayout {
id: legendRow
anchors.centerIn: parent
spacing: 14
Row {
spacing: 10
Row {
spacing: 8
Rectangle { width: 8; height: 8; radius: 4; color: Theme.sensorLow; anchors.verticalCenter: parent.verticalCenter }
Label { text: "Low"; font.pixelSize: Theme.fontXs; color: Theme.sideMutedText; anchors.verticalCenter: parent.verticalCenter }
}
Row {
spacing: 8
Rectangle { width: 8; height: 8; radius: 4; color: Theme.sensorInRange; anchors.verticalCenter: parent.verticalCenter }
Label { text: "In-range"; font.pixelSize: Theme.fontXs; color: Theme.sideMutedText; anchors.verticalCenter: parent.verticalCenter }
}
Row {
spacing: 8
Rectangle { width: 8; height: 8; radius: 4; color: Theme.sensorHigh; anchors.verticalCenter: parent.verticalCenter }
Label { text: "High"; font.pixelSize: Theme.fontXs; color: Theme.sideMutedText; anchors.verticalCenter: parent.verticalCenter }
}
}
Rectangle {
Layout.preferredWidth: Theme.borderThin
Layout.preferredHeight: 14
Layout.alignment: Qt.AlignVCenter
color: Theme.sideBorder
}
Label {
text: (streamController.thresholdAuto ? "Auto · " : "Manual · ")
+ streamController.target.toFixed(1) + " ± " + streamController.margin.toFixed(1) + "°C"
font.pixelSize: Theme.fontXs
color: Theme.headingColor
}
}
}
function exportImage(filePath, extra) { function exportImage(filePath, extra) {
return map.export_image(filePath, extra || ""); return map.export_image(filePath, extra || "");
} }
@@ -182,6 +182,11 @@ class SessionController(QObject):
"""Resolved band half-width (frame 1σ in auto mode, else margin).""" """Resolved band half-width (frame 1σ in auto mode, else margin)."""
return self._last.margin if self._last else 1.0 return self._last.margin if self._last else 1.0
@Property(bool, notify=frameUpdated)
def thresholdAuto(self) -> bool:
"""Whether the current band is auto-derived (frame mean ± 1σ) or manual."""
return self._model.auto
@Property(dict, notify=frameUpdated) @Property(dict, notify=frameUpdated)
def stats(self) -> dict: def stats(self) -> dict:
if not self._last: if not self._last:
@@ -26,6 +26,10 @@ class SessionModel:
def set_thresholds(self, config: ThresholdConfig) -> None: def set_thresholds(self, config: ThresholdConfig) -> None:
self._config = config self._config = config
@property
def auto(self) -> bool:
return self._config.auto
def reset(self) -> None: def reset(self) -> None:
self._stability.reset() self._stability.reset()