feat: Add Wafer Map tab and associated components

- Introduced a new Wafer Map tab in the HomePage.qml, which loads WaferMapTab.qml when selected.
- Created WaferMapTab.qml to display wafer map with live data and controls.
- Added ReadoutPanel.qml for displaying sensor statistics and thresholds.
- Implemented SourcePanel.qml for file selection and filtering.
- Developed TransportBar.qml for playback controls.
- Added WaferMapView.qml to visualize wafer data with interactive features.
- Created ReplaceSensorDialog.qml for sensor value overrides.
- Updated Theme.qml for new color schemes and UI adjustments.
- Modified qmldir files to include new components in the ISC.Tabs and ISC.Tabs.components modules.
This commit is contained in:
jack
2026-06-11 11:57:24 -07:00
parent b52b983bb2
commit 97ca58bfc2
10 changed files with 1080 additions and 13 deletions
@@ -0,0 +1,240 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import ISC
ColumnLayout {
spacing: 0
property var s: streamController.stats
property alias showLabels: labelsToggle.checked
property alias heatmapBlend: heatmapSlider.value
// ── READOUT ───────────────────────────────────────────────────────────
Label {
text: "READOUT"
color: Theme.bodyColor
font.pixelSize: 10
font.letterSpacing: 1.8
font.weight: Font.Medium
bottomPadding: 4
}
GridLayout {
columns: 2
rowSpacing: 4
columnSpacing: 4
Layout.fillWidth: true
// MIN TEMP
Rectangle {
Layout.fillWidth: true; height: 52
color: Theme.subtleSectionBackground; radius: Theme.radiusSm
border.color: Theme.cardBorder; border.width: 1
ColumnLayout {
anchors { fill: parent; margins: 7 }
spacing: 1
Label { text: "MIN TEMP"; color: Theme.bodyColor; font.pixelSize: 9; font.letterSpacing: 1 }
Label {
text: s.min !== undefined ? s.min + " #" + s.minIndex : "—"
color: Theme.sensorLow
font.pixelSize: 15; font.weight: Font.Bold
}
}
}
// MAX TEMP
Rectangle {
Layout.fillWidth: true; height: 52
color: Theme.subtleSectionBackground; radius: Theme.radiusSm
border.color: Theme.cardBorder; border.width: 1
ColumnLayout {
anchors { fill: parent; margins: 7 }
spacing: 1
Label { text: "MAX TEMP"; color: Theme.bodyColor; font.pixelSize: 9; font.letterSpacing: 1 }
Label {
text: s.max !== undefined ? s.max + " #" + s.maxIndex : "—"
color: Theme.sensorHigh
font.pixelSize: 15; font.weight: Font.Bold
}
}
}
// DIFFERENTIAL
Rectangle {
Layout.fillWidth: true; height: 52
color: Theme.subtleSectionBackground; radius: Theme.radiusSm
border.color: Theme.cardBorder; border.width: 1
ColumnLayout {
anchors { fill: parent; margins: 7 }
spacing: 1
Label { text: "DIFFERENTIAL"; color: Theme.bodyColor; font.pixelSize: 9; font.letterSpacing: 1 }
Label {
text: s.diff !== undefined ? s.diff : "—"
color: Theme.headingColor
font.pixelSize: 15; font.weight: Font.Bold
}
}
}
// AVERAGE
Rectangle {
Layout.fillWidth: true; height: 52
color: Theme.subtleSectionBackground; radius: Theme.radiusSm
border.color: Theme.cardBorder; border.width: 1
ColumnLayout {
anchors { fill: parent; margins: 7 }
spacing: 1
Label { text: "AVERAGE"; color: Theme.bodyColor; font.pixelSize: 9; font.letterSpacing: 1 }
Label {
text: s.avg !== undefined ? s.avg : "—"
color: Theme.headingColor
font.pixelSize: 15; font.weight: Font.Bold
}
}
}
}
// SIGMA — full-width
Rectangle {
Layout.fillWidth: true; height: 44
color: Theme.subtleSectionBackground; radius: Theme.radiusSm
border.color: Theme.cardBorder; border.width: 1
RowLayout {
anchors { fill: parent; leftMargin: 7; rightMargin: 7; topMargin: 4; bottomMargin: 4 }
ColumnLayout {
spacing: 1
Label { text: "SIGMA (σ)"; color: Theme.bodyColor; font.pixelSize: 9; font.letterSpacing: 1 }
Label { text: "3σ"; color: Theme.bodyColor; font.pixelSize: 9; font.letterSpacing: 1 }
}
Item { Layout.fillWidth: true }
ColumnLayout {
spacing: 1
Label {
text: s.sigma !== undefined ? s.sigma : "—"
color: Theme.headingColor
font.pixelSize: 15; font.weight: Font.Bold
Layout.alignment: Qt.AlignRight
}
Label {
text: s.threeSigma !== undefined ? s.threeSigma : "—"
color: Theme.bodyColor
font.pixelSize: 12; font.weight: Font.Medium
Layout.alignment: Qt.AlignRight
}
}
}
}
Item { height: 8 }
Rectangle { Layout.fillWidth: true; height: 1; color: Theme.cardBorder }
Item { height: 8 }
// ── DISPLAY ───────────────────────────────────────────────────────────
Label {
text: "DISPLAY"
color: Theme.bodyColor
font.pixelSize: 10
font.letterSpacing: 1.8
font.weight: Font.Medium
bottomPadding: 4
}
CheckBox {
id: labelsToggle
text: "Labels"
checked: true
font.pixelSize: 11
}
RowLayout {
Layout.fillWidth: true
spacing: 6
Label {
text: "Heatmap"
color: Theme.bodyColor
font.pixelSize: 11
Layout.preferredWidth: 52
}
Slider {
id: heatmapSlider
from: 0; to: 1; value: 0
Layout.fillWidth: true
ToolTip.visible: hovered
ToolTip.text: Math.round(value * 100) + "%"
}
}
Item { height: 8 }
Rectangle { Layout.fillWidth: true; height: 1; color: Theme.cardBorder }
Item { height: 8 }
// ── THRESHOLDS ────────────────────────────────────────────────────────
Label {
text: "THRESHOLDS"
color: Theme.bodyColor
font.pixelSize: 10
font.letterSpacing: 1.8
font.weight: Font.Medium
bottomPadding: 4
}
Label { text: "Set Point (°C)"; color: Theme.bodyColor; font.pixelSize: 11 }
TextField {
id: spField
Layout.fillWidth: true
text: "149.0"
font.pixelSize: 12
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
}
onEditingFinished: pushThresholds()
}
Item { height: 4 }
Label { text: "Margin (±°C)"; color: Theme.bodyColor; font.pixelSize: 11 }
TextField {
id: mgField
Layout.fillWidth: true
text: "1.0"
font.pixelSize: 12
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
}
onEditingFinished: pushThresholds()
}
Item { height: 4 }
CheckBox {
id: autoCheck
text: "Auto range (mean ± 1σ)"
checked: true
font.pixelSize: 11
onCheckedChanged: pushThresholds()
}
Item { Layout.fillHeight: true }
function pushThresholds() {
var sp = parseFloat(spField.text) || 149.0
var mg = parseFloat(mgField.text) || 1.0
streamController.setThresholds(sp, mg, autoCheck.checked)
}
}
@@ -0,0 +1,206 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import ISC
Dialog {
id: root
title: "Sensor Override"
modal: true
standardButtons: Dialog.NoButton
width: 300
// called by WaferMapView's TapHandler
property int sensorIndex: -1
property string sensorLabel: ""
property real currentValue: 0.0
property bool hasOverride: streamController.overriddenSensors.indexOf(sensorIndex) >= 0
function openFor(index) {
sensorIndex = index
var dot = streamController.sensorDots[index]
if (!dot) return
sensorLabel = dot.label !== undefined ? String(dot.label) : String(index + 1)
currentValue = dot.value
valueField.text = ""
offsetField.text = ""
open()
}
background: Rectangle {
color: Theme.cardBackground
radius: 8
border.color: Theme.cardBorder
}
ColumnLayout {
anchors.fill: parent
spacing: Theme.rightPaneGap
// ── header ───────────────────────────────────────────────
RowLayout {
Layout.fillWidth: true
Label {
text: "Sensor #" + (root.sensorIndex + 1) +
(root.sensorLabel ? " (" + root.sensorLabel + ")" : "")
color: Theme.headingColor
font.bold: true
font.pixelSize: 13
Layout.fillWidth: true
}
Rectangle {
visible: root.hasOverride
width: overrideTag.implicitWidth + 12
height: overrideTag.implicitHeight + 6
radius: 4
color: Theme.statusWarningColor
opacity: 0.85
Label {
id: overrideTag
anchors.centerIn: parent
text: "OVERRIDE"
color: "white"
font.pixelSize: 10
font.bold: true
font.letterSpacing: 1
}
}
}
Label {
text: "Live: " + root.currentValue.toFixed(2)
color: Theme.bodyColor
font.pixelSize: 12
}
Rectangle { Layout.fillWidth: true; height: 1; color: Theme.cardBorder }
// ── replace field ─────────────────────────────────────────
Label { text: "Replace with value"; color: Theme.bodyColor; font.pixelSize: 11 }
TextField {
id: valueField
Layout.fillWidth: true
placeholderText: "100"
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: valueField.activeFocus ? Theme.borderStrong : Theme.borderThin
border.color: valueField.activeFocus ? Theme.fieldBorderFocus : Theme.fieldBorder
}
}
// ── offset field ──────────────────────────────────────────
Label { text: "Or add offset (± delta)"; color: Theme.bodyColor; font.pixelSize: 11 }
TextField {
id: offsetField
Layout.fillWidth: true
placeholderText: "e.g. +0.5 or -0.5 (blank = skip)"
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: offsetField.activeFocus ? Theme.borderStrong : Theme.borderThin
border.color: offsetField.activeFocus ? Theme.fieldBorderFocus : Theme.fieldBorder
}
}
Rectangle { Layout.fillWidth: true; height: 1; color: Theme.cardBorder }
// ── buttons ───────────────────────────────────────────────
RowLayout {
Layout.fillWidth: true
spacing: 8
Button {
id: applyBtn
text: "Apply"
Layout.fillWidth: true
hoverEnabled: true
enabled: valueField.text !== "" || offsetField.text !== ""
opacity: enabled ? 1.0 : 0.4
background: Rectangle {
radius: Theme.radiusSm
color: applyBtn.down ? Theme.buttonNeutralPressed : applyBtn.hovered ? Theme.buttonNeutralHover : Theme.buttonNeutralBackground
border.width: Theme.borderThin
border.color: Theme.fieldBorder
}
contentItem: Text {
text: applyBtn.text
color: Theme.buttonNeutralText
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.bold: true
font.pixelSize: 13
}
onClicked: {
if (valueField.text !== "") {
var v = parseFloat(valueField.text)
if (!isNaN(v))
streamController.replaceSensor(root.sensorIndex, v)
}
if (offsetField.text !== "") {
var d = parseFloat(offsetField.text)
if (!isNaN(d))
streamController.offsetSensor(root.sensorIndex, d)
}
root.close()
}
}
Button {
id: clearBtn
text: "Clear"
Layout.fillWidth: true
hoverEnabled: true
enabled: root.hasOverride
opacity: enabled ? 1.0 : 0.4
background: Rectangle {
radius: Theme.radiusSm
color: clearBtn.down ? Theme.buttonNeutralPressed : clearBtn.hovered ? Theme.buttonNeutralHover : Theme.buttonNeutralBackground
border.width: Theme.borderThin
border.color: Theme.fieldBorder
}
contentItem: Text {
text: clearBtn.text
color: Theme.buttonNeutralText
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.bold: true
font.pixelSize: 13
}
onClicked: {
streamController.clearSensorEdit(root.sensorIndex)
root.close()
}
}
Button {
id: cancelBtn
text: "Cancel"
hoverEnabled: true
background: Rectangle {
radius: Theme.radiusSm
color: cancelBtn.down ? Theme.buttonNeutralPressed : cancelBtn.hovered ? Theme.buttonNeutralHover : Theme.buttonNeutralBackground
border.width: Theme.borderThin
border.color: Theme.fieldBorder
}
contentItem: Text {
text: cancelBtn.text
color: Theme.buttonNeutralText
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.bold: true
font.pixelSize: 13
}
onClicked: root.close()
}
}
}
}
@@ -0,0 +1,229 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import ISC
ColumnLayout {
spacing: 6
// ── Section label ─────────────────────────────────────────────────────
Label {
text: "SOURCE"
color: Theme.bodyColor
font.pixelSize: 10
font.letterSpacing: 1.8
font.weight: Font.Medium
}
// ── Directory button ──────────────────────────────────────────────────
Button {
id: dirBtn
Layout.fillWidth: true
hoverEnabled: true
implicitHeight: 32
leftPadding: 8
rightPadding: 8
background: Rectangle {
radius: Theme.radiusSm
color: dirBtn.pressed ? Theme.buttonNeutralPressed
: dirBtn.hovered ? Theme.buttonNeutralHover
: Theme.buttonNeutralBackground
border.width: Theme.borderThin
border.color: Theme.fieldBorder
}
contentItem: RowLayout {
spacing: 6
Label {
text: "▤"
color: Theme.bodyColor
font.pixelSize: 12
}
Label {
text: file_browser.currentDirectory.split("/").pop() || file_browser.currentDirectory
color: Theme.headingColor
font.pixelSize: 11
elide: Text.ElideMiddle
Layout.fillWidth: true
}
}
onClicked: file_browser.chooseDirectory()
}
// ── Filter ────────────────────────────────────────────────────────────
TextField {
id: filter
Layout.fillWidth: true
placeholderText: "Filter…"
font.pixelSize: 11
color: Theme.fieldText
placeholderTextColor: Theme.fieldPlaceholder
selectedTextColor: Theme.fieldBackground
selectionColor: Theme.fieldText
background: Rectangle {
radius: Theme.radiusXs
color: Theme.fieldBackground
border.width: filter.activeFocus ? Theme.borderStrong : Theme.borderThin
border.color: filter.activeFocus ? Theme.fieldBorderFocus : Theme.fieldBorder
}
}
// ── File list ─────────────────────────────────────────────────────────
ListView {
id: fileList
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
spacing: 2
highlight: null
highlightFollowsCurrentItem: false
model: file_browser.files
delegate: ItemDelegate {
id: fileItem
width: ListView.view.width
padding: 0
highlighted: false
focusPolicy: Qt.NoFocus
readonly property bool matchesFilter: {
if (filter.text === "") return true
var q = filter.text.toLowerCase()
return (modelData.baseName + modelData.waferType + modelData.date)
.toLowerCase().indexOf(q) >= 0
}
// Active only in review mode; no highlight during live streaming
readonly property bool isActive:
streamController.mode === "review" &&
streamController.loadedFile !== "" &&
modelData.fileName === streamController.loadedFile
visible: matchesFilter
height: matchesFilter ? implicitHeight : 0
onClicked: streamController.loadFile(modelData.fileName)
background: Rectangle {
color: fileItem.isActive
? Qt.rgba(1, 1, 1, 0.06)
: fileItem.hovered ? Qt.rgba(1, 1, 1, 0.04) : "transparent"
radius: Theme.radiusSm
// Left accent bar — shown only when active
Rectangle {
visible: fileItem.isActive
width: 3
radius: 1.5
anchors {
top: parent.top; bottom: parent.bottom; left: parent.left
topMargin: 5; bottomMargin: 5
}
color: {
var t = modelData.waferType
if (t === "A" || t === "E" || t === "P") return "#3B82F6"
if (t === "B" || t === "C" || t === "D") return "#10B981"
if (t === "Z") return "#8B5CF6"
return Theme.headingColor
}
}
}
contentItem: RowLayout {
spacing: 8
anchors.margins: 8
// Wafer-type avatar (circle)
Rectangle {
width: 28; height: 28
radius: 14
color: {
var t = modelData.waferType
if (t === "A" || t === "E" || t === "P") return "#1D4ED8"
if (t === "B" || t === "C" || t === "D") return "#065F46"
if (t === "Z") return "#7C3AED"
return "#374151"
}
Label {
anchors.centerIn: parent
text: modelData.waferType || "?"
font.pixelSize: 12
font.weight: Font.Bold
color: "#FFFFFF"
}
}
// Typography hierarchy
ColumnLayout {
spacing: 1
Layout.fillWidth: true
// Primary: serial number
Label {
text: modelData.serialNumber || modelData.baseName
color: Theme.headingColor
font.pixelSize: 13
font.weight: Font.Bold
elide: Text.ElideRight
Layout.fillWidth: true
}
// Secondary: date · time
RowLayout {
spacing: 3
Layout.fillWidth: true
Label {
text: {
var d = modelData.date || ""
return d.length >= 10 ? d.substring(0, 10) : (d || "—")
}
color: Theme.bodyColor
font.pixelSize: 10
}
Label {
visible: (modelData.timeStr || "") !== ""
text: "·"
color: Theme.bodyColor
font.pixelSize: 10
opacity: 0.45
}
Label {
visible: (modelData.timeStr || "") !== ""
text: modelData.timeStr || ""
color: Theme.bodyColor
font.pixelSize: 10
opacity: 0.65
}
}
// Tertiary: edited metadata (chamber · notes) if any
Label {
visible: {
var c = modelData.chamber || ""
var n = modelData.notes || ""
return c !== "" || n !== ""
}
text: {
var parts = []
if (modelData.chamber) parts.push(modelData.chamber)
if (modelData.notes) parts.push(modelData.notes)
return parts.join(" · ")
}
color: Theme.bodyColor
font.pixelSize: 9
opacity: 0.7
elide: Text.ElideRight
Layout.fillWidth: true
}
}
}
}
}
// ── Footer ────────────────────────────────────────────────────────────
Label {
text: file_browser.files.length + " files · CSV only"
color: Theme.bodyColor
font.pixelSize: 10
}
}
@@ -0,0 +1,91 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import ISC
RowLayout {
spacing: 2
component TBtn: Button {
implicitWidth: 44
implicitHeight: 32
hoverEnabled: true
font.pixelSize: 13
background: Rectangle {
color: parent.pressed ? Theme.transportButtonHover
: parent.hovered ? Theme.transportButtonBg
: Theme.transportBackground
radius: Theme.radiusSm
}
contentItem: Text {
text: parent.text
color: Theme.headingColor
font: parent.font
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
// Left fill — centers the button cluster
Item { Layout.fillWidth: true }
TBtn { text: "⏮"; onClicked: streamController.step(-1) }
TBtn { text: "⏹"; onClicked: streamController.stop() }
TBtn { text: "⏸"; onClicked: streamController.pause() }
// Play: slightly wider; leftPadding nudge compensates for ▶ optical offset
TBtn {
text: "▶"
implicitWidth: 52
contentItem: Text {
text: parent.text
color: Theme.headingColor
font: parent.font
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
leftPadding: 2
}
onClicked: streamController.play()
}
TBtn { text: "⏭"; onClicked: streamController.step(1) }
// Right fill — balances centering
Item { Layout.fillWidth: true }
// Speed cycle
Button {
id: speedBtn
property var speeds: [1, 2, 5]
property int idx: 0
implicitWidth: 44
implicitHeight: 32
hoverEnabled: true
text: speeds[idx] + "×"
font.pixelSize: 11
font.weight: Font.Medium
background: Rectangle {
color: parent.pressed ? Theme.transportButtonHover
: parent.hovered ? Theme.transportButtonBg
: Theme.transportBackground
radius: Theme.radiusSm
}
contentItem: Text {
text: parent.text
color: Theme.bodyColor
font: parent.font
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
onClicked: {
idx = (idx + 1) % speeds.length
streamController.setSpeed(speeds[idx])
}
}
Item { width: 8 }
Label {
text: "frame " + (streamController.frameIndex + 1) + " / " + streamController.frameTotal
color: Theme.bodyColor
font.pixelSize: 11
}
}
@@ -0,0 +1,43 @@
import QtQuick
import QtQuick.Controls
import ISC
import ISC.Wafer
Item {
id: root
property real blend: 0.0
property bool showLabels: true
WaferMapItem {
id: map
anchors.fill: parent
sensors: streamController.sensorLayout // [{label,x,y}]
values: streamController.sensorValues // [float]
bands: streamController.sensorBands // ["in_range"|"high"|"low"]
target: streamController.target
margin: streamController.margin
blend: root.blend
showLabels: root.showLabels
// Bind to Theme so colors update on dark/light mode switch
ringColor: Theme.waferRingColor
axisColor: Theme.waferAxisColor
lowColor: Theme.sensorLow
inRangeColor: Theme.sensorInRange
highColor: Theme.sensorHigh
textColor: Theme.headingColor
TapHandler {
onTapped: (ev) => {
var idx = map.which_marker(ev.position.x, ev.position.y)
if (idx >= 0) replaceDialog.openFor(idx)
}
}
}
ReplaceSensorDialog { id: replaceDialog}
}
+7
View File
@@ -0,0 +1,7 @@
# ===== ISC Tabs Components Module =====
module ISC.Tabs.components
SourcePanel 1.0 SourcePanel.qml
ReadoutPanel 1.0 ReadoutPanel.qml
TransportBar 1.0 TransportBar.qml
WaferMapView 1.0 WaferMapView.qml
ReplaceSensorDialog 1.0 ReplaceSensorDialog.qml