diff --git a/src/pygui/ISC/Tabs/WaferMapTab.qml b/src/pygui/ISC/Tabs/WaferMapTab.qml index 885913b..1847d8c 100644 --- a/src/pygui/ISC/Tabs/WaferMapTab.qml +++ b/src/pygui/ISC/Tabs/WaferMapTab.qml @@ -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); diff --git a/src/pygui/ISC/Tabs/components/ReadoutPanel.qml b/src/pygui/ISC/Tabs/components/ReadoutPanel.qml index 9f33009..9366fd8 100644 --- a/src/pygui/ISC/Tabs/components/ReadoutPanel.qml +++ b/src/pygui/ISC/Tabs/components/ReadoutPanel.qml @@ -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 } + } + Behavior on border.color { + ColorAnimation { duration: Theme.durationFast } + } + } + onEditingFinished: pushThresholds() } - ReadoutStat { - label: "Diff" - value: s.diff !== undefined ? s.diff : "—" - valueColor: Theme.diffAccent + + Label { + text: "Margin (±°C)" + color: Theme.bodyColor + font.pixelSize: Theme.fontSm } - ReadoutStat { - label: "Average" - value: s.avg !== undefined ? s.avg : "—" + 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 } + } + Behavior on border.color { + ColorAnimation { duration: Theme.durationFast } + } + } + onEditingFinished: pushThresholds() } - ReadoutStat { - label: "Sigma (Σ)" - value: s.sigma !== undefined ? s.sigma : "—" - } - ReadoutStat { - label: "3Σ Value" - 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 + ")" - : "—" + + 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 } - } - Behavior on border.color { - ColorAnimation { duration: Theme.durationFast } - } - } - onEditingFinished: pushThresholds() + ReadoutStat { + label: "Max Temp" + value: s.max !== undefined + ? s.max + ((s.maxIndex !== undefined && s.maxIndex >= 0) ? " #" + s.maxIndex : "") + : "—" + valueColor: Theme.sensorHigh } - - Label { - text: "Margin (±°C)" - color: Theme.bodyColor - font.pixelSize: Theme.fontSm + ReadoutStat { + label: "Diff" + value: s.diff !== undefined ? s.diff : "—" + valueColor: Theme.diffAccent } - 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 } - } - Behavior on border.color { - ColorAnimation { duration: Theme.durationFast } - } - } - onEditingFinished: pushThresholds() + ReadoutStat { + label: "Average" + value: s.avg !== undefined ? s.avg : "—" } - - PanelCheckBox { - id: autoCheck - text: "Auto range (mean ± 1σ)" - checked: true - font.pixelSize: Theme.fontSm - onCheckedChanged: pushThresholds() + ReadoutStat { + label: "Sigma (Σ)" + value: s.sigma !== undefined ? s.sigma : "—" + } + ReadoutStat { + label: "3Σ Value" + 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 + ")" + : "—" } } } diff --git a/src/pygui/ISC/Tabs/components/ReplaceSensorDialog.qml b/src/pygui/ISC/Tabs/components/ReplaceSensorDialog.qml index 7e3de1e..33b7641 100644 --- a/src/pygui/ISC/Tabs/components/ReplaceSensorDialog.qml +++ b/src/pygui/ISC/Tabs/components/ReplaceSensorDialog.qml @@ -5,6 +5,7 @@ import ISC Dialog { id: root + parent: Overlay.overlay title: "Sensor Override" modal: true standardButtons: Dialog.NoButton diff --git a/src/pygui/ISC/Tabs/components/WaferMapView.qml b/src/pygui/ISC/Tabs/components/WaferMapView.qml index 267b159..b4cd906 100644 --- a/src/pygui/ISC/Tabs/components/WaferMapView.qml +++ b/src/pygui/ISC/Tabs/components/WaferMapView.qml @@ -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 || ""); } diff --git a/src/pygui/backend/controllers/session_controller.py b/src/pygui/backend/controllers/session_controller.py index 549edb4..ab68cb0 100644 --- a/src/pygui/backend/controllers/session_controller.py +++ b/src/pygui/backend/controllers/session_controller.py @@ -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: diff --git a/src/pygui/backend/models/session_model.py b/src/pygui/backend/models/session_model.py index c0eb272..bb0e54e 100644 --- a/src/pygui/backend/models/session_model.py +++ b/src/pygui/backend/models/session_model.py @@ -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()