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
// (Thresholds) is always reachable even if content outgrows
// the available height (e.g. once E-C delta rows appear).
// Readout panel — scrolls instead of clipping so no card is lost
// if content outgrows the available height (e.g. once E-C delta
// rows appear). Thresholds sits first so it's visible unscrolled.
ScrollView {
id: readoutScroll
Layout.preferredWidth: Theme.rightRailWidth
Layout.fillHeight: true
clip: true
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 {
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
onExportRequested: function(filePath, extra) {
waferView.exportImage(filePath, extra);
+113 -110
View File
@@ -5,10 +5,13 @@ import QtQuick.Layouts
import ISC
// ===== Readout Panel =====
// Right-rail panel with three bento-tile sections: READOUT, DISPLAY,
// THRESHOLDS. Uses the same PanelBox / SectionTitle / ReadoutStat /
// PanelCheckBox / PanelSlider components as the Data tab's
// ComparisonSidePanel, so both right rails share one visual language.
// Right-rail panel with three bento-tile sections: THRESHOLDS, DISPLAY,
// READOUT. Thresholds comes first so Set Point/Margin/Auto are visible
// without scrolling on first use. The whole panel scrolls as one column
// (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 {
id: root
spacing: 6
@@ -47,63 +50,85 @@ ColumnLayout {
}
}
// ── READOUT ─────────────────────────────────────────────────────
// ── THRESHOLDS ──────────────────────────────────────────────────
PanelBox {
Layout.fillWidth: true
Layout.preferredHeight: readoutCol.implicitHeight + 24
Layout.preferredHeight: thresholdCard.implicitHeight + 24
ColumnLayout {
id: readoutCol
id: thresholdCard
anchors.fill: parent
anchors.margins: 12
spacing: 8
spacing: 6
SectionTitle { text: "READOUT" }
SectionTitle { text: "THRESHOLDS" }
ReadoutStat {
label: "Min Temp"
value: s.min !== undefined
? s.min + ((s.minIndex !== undefined && s.minIndex >= 0) ? " #" + s.minIndex : "")
: "—"
valueColor: Theme.sensorLow
Label {
text: "Set Point (°C)"
color: Theme.bodyColor
font.pixelSize: Theme.fontSm
}
ReadoutStat {
label: "Max Temp"
value: s.max !== undefined
? s.max + ((s.maxIndex !== undefined && s.maxIndex >= 0) ? " #" + s.maxIndex : "")
: "—"
valueColor: Theme.sensorHigh
TextField {
id: spField
Layout.fillWidth: true
text: "149.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: spField.activeFocus ? Theme.borderStrong : Theme.borderThin
border.color: spField.activeFocus ? Theme.fieldBorderFocus : Theme.fieldBorder
Behavior on color {
ColorAnimation { duration: Theme.durationFast }
}
ReadoutStat {
label: "Diff"
value: s.diff !== undefined ? s.diff : "—"
valueColor: Theme.diffAccent
Behavior on border.color {
ColorAnimation { duration: Theme.durationFast }
}
ReadoutStat {
label: "Average"
value: s.avg !== undefined ? s.avg : "—"
}
ReadoutStat {
label: "Sigma (Σ)"
value: s.sigma !== undefined ? s.sigma : "—"
onEditingFinished: pushThresholds()
}
ReadoutStat {
label: "3Σ Value"
value: s.threeSigma !== undefined ? s.threeSigma : "—"
Label {
text: "Margin (±°C)"
color: Theme.bodyColor
font.pixelSize: Theme.fontSm
}
ReadoutStat {
visible: s.ecMinDelta !== undefined
label: "E-C Δ Min"
value: s.ecMinDelta !== undefined
? s.ecMinDelta.toFixed(2) + " (#" + s.ecMinEdgeIndex + "→#" + s.ecMinCenterIndex + ")"
: "—"
TextField {
id: mgField
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 }
}
ReadoutStat {
visible: s.ecMaxDelta !== undefined
label: "E-C Δ Max"
value: s.ecMaxDelta !== undefined
? s.ecMaxDelta.toFixed(2) + " (#" + s.ecMaxEdgeIndex + "→#" + s.ecMaxCenterIndex + ")"
: "—"
Behavior on border.color {
ColorAnimation { duration: Theme.durationFast }
}
}
onEditingFinished: pushThresholds()
}
PanelCheckBox {
id: autoCheck
text: "Auto range (mean ± 1σ)"
checked: true
font.pixelSize: Theme.fontSm
onCheckedChanged: pushThresholds()
}
}
}
@@ -203,85 +228,63 @@ ColumnLayout {
}
}
// ── THRESHOLDS ──────────────────────────────────────────────────
// ── READOUT ─────────────────────────────────────────────────────
PanelBox {
Layout.fillWidth: true
Layout.preferredHeight: thresholdCard.implicitHeight + 24
Layout.preferredHeight: readoutCol.implicitHeight + 24
ColumnLayout {
id: thresholdCard
id: readoutCol
anchors.fill: parent
anchors.margins: 12
spacing: 6
spacing: 8
SectionTitle { text: "THRESHOLDS" }
SectionTitle { text: "READOUT" }
Label {
text: "Set Point (°C)"
color: Theme.bodyColor
font.pixelSize: Theme.fontSm
ReadoutStat {
label: "Min Temp"
value: s.min !== undefined
? s.min + ((s.minIndex !== undefined && s.minIndex >= 0) ? " #" + s.minIndex : "")
: "—"
valueColor: Theme.sensorLow
}
TextField {
id: spField
Layout.fillWidth: true
text: "149.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: spField.activeFocus ? Theme.borderStrong : Theme.borderThin
border.color: spField.activeFocus ? Theme.fieldBorderFocus : Theme.fieldBorder
Behavior on color {
ColorAnimation { duration: Theme.durationFast }
ReadoutStat {
label: "Max Temp"
value: s.max !== undefined
? s.max + ((s.maxIndex !== undefined && s.maxIndex >= 0) ? " #" + s.maxIndex : "")
: "—"
valueColor: Theme.sensorHigh
}
Behavior on border.color {
ColorAnimation { duration: Theme.durationFast }
ReadoutStat {
label: "Diff"
value: s.diff !== undefined ? s.diff : "—"
valueColor: Theme.diffAccent
}
ReadoutStat {
label: "Average"
value: s.avg !== undefined ? s.avg : "—"
}
onEditingFinished: pushThresholds()
ReadoutStat {
label: "Sigma (Σ)"
value: s.sigma !== undefined ? s.sigma : "—"
}
Label {
text: "Margin (±°C)"
color: Theme.bodyColor
font.pixelSize: Theme.fontSm
ReadoutStat {
label: "3Σ Value"
value: s.threeSigma !== undefined ? s.threeSigma : "—"
}
TextField {
id: mgField
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 }
ReadoutStat {
visible: s.ecMinDelta !== undefined
label: "E-C Δ Min"
value: s.ecMinDelta !== undefined
? s.ecMinDelta.toFixed(2) + " (#" + s.ecMinEdgeIndex + "→#" + s.ecMinCenterIndex + ")"
: "—"
}
Behavior on border.color {
ColorAnimation { duration: Theme.durationFast }
}
}
onEditingFinished: pushThresholds()
}
PanelCheckBox {
id: autoCheck
text: "Auto range (mean ± 1σ)"
checked: true
font.pixelSize: Theme.fontSm
onCheckedChanged: pushThresholds()
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 {
id: root
parent: Overlay.overlay
title: "Sensor Override"
modal: true
standardButtons: Dialog.NoButton
@@ -1,5 +1,6 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import ISC
import ISC.Wafer
@@ -64,6 +65,64 @@ Item {
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) {
return map.export_image(filePath, extra || "");
}
@@ -182,6 +182,11 @@ class SessionController(QObject):
"""Resolved band half-width (frame 1σ in auto mode, else margin)."""
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)
def stats(self) -> dict:
if not self._last:
@@ -26,6 +26,10 @@ class SessionModel:
def set_thresholds(self, config: ThresholdConfig) -> None:
self._config = config
@property
def auto(self) -> bool:
return self._config.auto
def reset(self) -> None:
self._stability.reset()