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:
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user