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