feat: implement master file configuration, UI indicators, and a Run Comparison tool for the Data tab

This commit is contained in:
jack
2026-07-07 15:05:22 -07:00
parent 1e03227788
commit 92f130b3bd
8 changed files with 798 additions and 428 deletions
+330 -335
View File
@@ -20,6 +20,23 @@ Item {
property int activeBox: 1 // 1 for Reference (Run A), 2 for Session (Run B), 0 for inactive
property string compareFileA: ""
property string compareFileB: ""
property bool useMasterFile: false // Run A comes from settingsModel.masters[runBWaferType] instead of manual selection
function waferTypeForFile(path) {
if (!path) return ""
var rows = file_browser.files
for (var i = 0; i < rows.length; i++) {
if (rows[i].fileName === path) return (rows[i].waferType || "").toUpperCase()
}
var base = path.split("/").pop()
return base ? base.charAt(0).toUpperCase() : ""
}
// Master file must match Run B's wafer family — looking it up by that
// family is itself the comparability gate (a mismatched-family master
// simply won't exist under this key).
readonly property string runBWaferType: root.waferTypeForFile(root.compareFileB)
readonly property string masterFileForRunB: root.runBWaferType !== "" ? (settingsModel.masters[root.runBWaferType] || "") : ""
property real warpingDistance: -1
property int frameOffset: 0
property real frameOffsetSeconds: 0
@@ -57,6 +74,27 @@ Item {
return t !== null ? " (" + t.toFixed(1) + "s)" : ""
}
function formatMinutes(secondsValue) {
var mins = Math.floor(secondsValue / 60)
var secs = secondsValue % 60
return mins + ":" + (secs < 10 ? "0" : "") + secs.toFixed(1)
}
function frameForTime(t) {
var times = root.timeDataA.length > 0 ? root.timeDataA : root.timeDataB
if (times.length === 0) return 0
var closestIdx = 0
var minDiff = Math.abs(times[0] - t)
for (var i = 1; i < times.length; i++) {
var diff = Math.abs(times[i] - t)
if (diff < minDiff) {
minDiff = diff
closestIdx = i
}
}
return closestIdx
}
function clearResults() {
root.warpingDistance = -1
root.frameOffset = 0
@@ -78,6 +116,7 @@ Item {
function resetComparison() {
root.compareFileA = ""
root.compareFileB = ""
root.useMasterFile = false
root.comparing = false
root.activeBox = 1
clearResults()
@@ -124,6 +163,7 @@ Item {
root.compareWaferSize = result.wafer_size || 300.0
root.errorMessage = ""
scrubber.value = Math.floor(root.seriesLen / 2)
scrubber.forceActiveFocus()
if (root.frameCountA !== root.frameCountB) {
var timeA = root.timeDataA.length > 0 ? root.timeDataA[root.timeDataA.length - 1] : 0
var timeB = root.timeDataB.length > 0 ? root.timeDataB[root.timeDataB.length - 1] : 0
@@ -213,34 +253,33 @@ Item {
}
}
// Mini stat box under the alignment scrubber (mockup's subtle-box trio)
// Mini stat box under the alignment scrubber (styled to match DTW ReadoutCard)
component ScrubStat: Rectangle {
id: scrubStat
property string label
property string value
property color valueColor: Theme.headingColor
Layout.fillWidth: true
implicitHeight: 38
radius: 4
color: Theme.cardWash
implicitHeight: 52
radius: Theme.radiusSm
color: Theme.subtleSectionBackground
border.color: Theme.cardBorder
border.width: 1
ColumnLayout {
anchors.fill: parent
anchors.leftMargin: 6
anchors.rightMargin: 6
anchors.topMargin: 4
anchors.bottomMargin: 4
spacing: 0
anchors.margins: 8
spacing: 2
Label {
text: scrubStat.label
font.pixelSize: 9
text: scrubStat.label.toUpperCase()
font.pixelSize: Theme.fontXs
font.bold: true
font.letterSpacing: 0.2
color: Theme.sideMutedText
}
Label {
text: scrubStat.value
font.pixelSize: Theme.fontSm
font.pixelSize: Theme.fontLg
font.bold: true
font.family: Theme.codeFontFamily
color: scrubStat.valueColor
@@ -248,6 +287,111 @@ Item {
}
}
// Editable stat box for scrubber readout (styled to match DTW ReadoutCard)
component EditableScrubStat: Rectangle {
id: edStat
property string label
property real value
property string unit
property string extraText: ""
property bool editable: true
signal valueEdited(real newValue)
Layout.fillWidth: true
implicitHeight: edStatRow.implicitHeight + 16
radius: Theme.radiusSm
color: Theme.subtleSectionBackground
border.color: (edStat.editable && inputField.activeFocus) ? Theme.fieldBorderFocus : Theme.cardBorder
border.width: (edStat.editable && inputField.activeFocus) ? Theme.borderStrong : Theme.borderThin
RowLayout {
id: edStatRow
anchors.fill: parent
anchors.margins: 8
spacing: 6
ColumnLayout {
Layout.fillWidth: true
spacing: 2
Label {
text: edStat.label.toUpperCase()
font.pixelSize: Theme.fontXs
font.bold: true
font.letterSpacing: 0.2
color: Theme.sideMutedText
}
RowLayout {
spacing: 4
TextField {
id: inputField
readOnly: !edStat.editable
hoverEnabled: edStat.editable
font.pixelSize: Theme.fontLg
font.bold: true
font.family: Theme.codeFontFamily
color: edStat.editable ? Theme.headingColor : Theme.sideMutedText
verticalAlignment: TextInput.AlignVCenter
horizontalAlignment: TextInput.AlignHCenter
background: Rectangle {
visible: edStat.editable
color: Theme.fieldBackground
radius: Theme.radiusXs
border.color: inputField.activeFocus ? Theme.fieldBorderFocus : (inputField.hovered ? Theme.fieldBorderFocus : Theme.fieldBorder)
border.width: 1
}
padding: 0
leftPadding: edStat.editable ? 6 : 0
rightPadding: edStat.editable ? 6 : 0
topPadding: edStat.editable ? 5 : 0
bottomPadding: edStat.editable ? 5 : 0
selectByMouse: edStat.editable
inputMethodHints: edStat.label === "Time" ? Qt.ImhFormattedNumbersOnly : Qt.ImhDigitsOnly
Layout.preferredWidth: edStat.editable ? Math.max(36, contentWidth + 12) : contentWidth
Layout.preferredHeight: edStat.editable ? 32 : implicitHeight
text: edStat.value.toFixed(edStat.label === "Time" ? 1 : 0)
onEditingFinished: {
var val = parseFloat(text)
if (!isNaN(val)) {
edStat.valueEdited(val)
}
text = edStat.value.toFixed(edStat.label === "Time" ? 1 : 0)
scrubber.forceActiveFocus()
}
Connections {
target: edStat
function onValueChanged() {
if (!inputField.activeFocus) {
inputField.text = edStat.value.toFixed(edStat.label === "Time" ? 1 : 0)
}
}
}
}
Label {
text: edStat.unit
font.pixelSize: Theme.fontLg
font.bold: true
font.family: Theme.codeFontFamily
color: edStat.editable ? Theme.headingColor : Theme.sideMutedText
Layout.alignment: Qt.AlignVCenter
}
}
}
Label {
visible: edStat.extraText !== ""
text: edStat.extraText
font.pixelSize: Theme.fontLg
font.bold: true
font.family: Theme.codeFontFamily
color: Theme.headingColor
Layout.alignment: Qt.AlignVCenter
}
}
}
// Large labeled value card for the DTW readout panel
component ReadoutCard: Rectangle {
id: readoutCard
@@ -282,128 +426,13 @@ Item {
}
}
// Run slot card: click to arm, then pick a file in the left rail's browser
component RunSlot: Rectangle {
id: slotBox
property int boxIndex: 1
property string title
property string filePath: ""
property color accent: Theme.primaryAccent
signal cleared()
Layout.fillWidth: true
implicitHeight: 58
radius: Theme.radiusSm
color: Theme.fieldBackground
border.width: root.activeBox === boxIndex ? 2 : 1
border.color: root.activeBox === boxIndex ? accent : Theme.cardBorder
Behavior on border.color { ColorAnimation { duration: Theme.durationFast } }
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: root.activeBox = slotBox.boxIndex
}
RowLayout {
anchors.fill: parent
anchors.margins: 10
spacing: 8
Rectangle {
width: 8; height: 8; radius: 4
color: slotBox.accent
Layout.alignment: Qt.AlignVCenter
}
ColumnLayout {
Layout.fillWidth: true
spacing: 2
Label {
text: slotBox.title
font.pixelSize: Theme.fontXs
font.bold: true
font.letterSpacing: 1.0
color: root.activeBox === slotBox.boxIndex ? slotBox.accent : Theme.sideMutedText
}
Label {
text: slotBox.filePath !== ""
? root.shortName(slotBox.filePath)
: (root.activeBox === slotBox.boxIndex ? "<- Select file from left panel..." : "Click to select")
font.pixelSize: Theme.fontSm
color: slotBox.filePath !== "" ? Theme.headingColor : Theme.bodyColor
opacity: slotBox.filePath !== "" ? 1.0 : 0.6
elide: Text.ElideMiddle
Layout.fillWidth: true
}
}
Button {
visible: slotBox.filePath !== ""
flat: true
implicitWidth: 24; implicitHeight: 24
onClicked: slotBox.cleared()
background: Rectangle {
radius: Theme.radiusSm
color: parent.hovered ? Theme.flatButtonHover : "transparent"
}
contentItem: IconImage {
source: "icons/x.svg"
width: 14; height: 14
sourceSize.width: 14
sourceSize.height: 14
color: Theme.bodyColor
anchors.centerIn: parent
}
}
}
}
// ── Main Layout ───────────────────────────────────────────────
ColumnLayout {
anchors.fill: parent
anchors.margins: Theme.panelPadding
spacing: Theme.rightPaneGap
// ── Title / Header ─────────────────────────────────────────
RowLayout {
Layout.fillWidth: true
spacing: 8
Label {
text: "Run Comparison (DTW)"
font.pixelSize: Theme.fontXl
font.bold: true
color: Theme.headingColor
Layout.fillWidth: true
elide: Text.ElideRight
}
Button {
id: resetBtn
flat: true
implicitWidth: 32
implicitHeight: 32
visible: root.compareFileA !== "" || root.compareFileB !== ""
onClicked: root.resetComparison()
background: Rectangle {
radius: Theme.radiusSm
color: resetBtn.hovered ? Theme.flatButtonHover : "transparent"
}
contentItem: IconImage {
source: "icons/rotate-ccw.svg"
width: 18; height: 18
sourceSize.width: 18
sourceSize.height: 18
color: Theme.bodyColor
anchors.centerIn: parent
}
ToolTip.visible: resetBtn.hovered
ToolTip.text: "Reset comparison"
}
}
// ── Error Banner ───────────────────────────────────────────
Rectangle {
@@ -454,6 +483,79 @@ Item {
Layout.minimumWidth: 350
spacing: 12
// Wafer Map Overlap View card
PanelBox {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.minimumHeight: 220
ColumnLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 8
RowLayout {
Layout.fillWidth: true
SectionTitle {
text: "WAFER OVERLAP"
Layout.fillWidth: true
}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: Theme.fieldBackground
border.color: Theme.cardBorder
border.width: 1
radius: Theme.radiusSm
clip: true
WaferMapItem {
id: overlapMapItem
anchors.fill: parent
visible: root.compareSensorLayout.length > 0 && enableOverlapCheck.checked
sensors: root.compareSensorLayout
values: streamController.getSensorDiffAt(scrubReadout.scrubIdx)
bands: root.diffBands(overlapMapItem.values)
shape: root.compareWaferShape
size: root.compareWaferSize
target: 0
margin: 1.0
blend: blendSlider.value / 100
showLabels: true
ringColor: Theme.waferRingColor
axisColor: Theme.waferAxisColor
lowColor: Theme.sensorLow
inRangeColor: Theme.sensorInRange
highColor: Theme.sensorHigh
textColor: Theme.headingColor
}
ColumnLayout {
anchors.centerIn: parent
visible: root.compareSensorLayout.length === 0 || !enableOverlapCheck.checked
spacing: 4
IconImage {
Layout.alignment: Qt.AlignHCenter
source: "icons/map.svg"
width: 24; height: 24
sourceSize.width: 24
sourceSize.height: 24
color: Theme.sideMutedText
}
Label {
Layout.alignment: Qt.AlignHCenter
text: !enableOverlapCheck.checked ? "Overlap map disabled"
: "No sensor map available"
color: Theme.bodyColor
font.pixelSize: Theme.fontXs
}
}
}
}
}
// Chart card with alignment scrubber
PanelBox {
Layout.fillWidth: true
@@ -478,6 +580,7 @@ Item {
spacing: 4
Rectangle { width: 12; height: 3; radius: 1.5; color: Theme.primaryAccent; anchors.verticalCenter: parent.verticalCenter }
Label { text: "Run A"; font.pixelSize: Theme.fontXs; color: Theme.bodyColor }
}
Row {
spacing: 4
@@ -668,129 +771,19 @@ Item {
Slider {
id: scrubber
Layout.fillWidth: true
leftPadding: overlayCanvas.padLeft
rightPadding: overlayCanvas.padRight
from: 0
to: Math.max(1, root.seriesLen - 1)
stepSize: 1
value: 0
}
RowLayout {
id: scrubReadout
Layout.fillWidth: true
spacing: 8
readonly property int scrubIdx: Math.round(scrubber.value)
readonly property bool hasA: scrubIdx < root.trendDataA.length
readonly property bool hasB: scrubIdx < root.trendDataB.length
readonly property real tempA: hasA ? root.trendDataA[scrubIdx] : 0
readonly property real tempB: hasB ? root.trendDataB[scrubIdx] : 0
ScrubStat {
label: "Position"
value: scrubReadout.scrubIdx + " / " + Math.max(0, root.seriesLen - 1)
+ root.scrubTimeLabel(scrubReadout.scrubIdx)
}
ScrubStat {
label: "Temp (Run A)"
value: scrubReadout.hasA ? scrubReadout.tempA.toFixed(1) + "°C" : "none"
valueColor: Theme.primaryAccent
}
ScrubStat {
label: "Temp (Run B)"
value: scrubReadout.hasB ? scrubReadout.tempB.toFixed(1) + "°C" : "none"
valueColor: Theme.themeSkill
}
ScrubStat {
label: "Difference"
value: (scrubReadout.hasA && scrubReadout.hasB)
? Math.abs(scrubReadout.tempA - scrubReadout.tempB).toFixed(1) + "°C" : "none"
}
}
}
}
}
// Wafer Map Overlap View card
PanelBox {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.minimumHeight: 220
ColumnLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 8
RowLayout {
Layout.fillWidth: true
SectionTitle {
text: "WAFER OVERLAP"
Layout.fillWidth: true
}
Label {
visible: root.compareFileA !== "" && root.compareFileB !== ""
text: (root.shortName(root.compareFileA).replace(".csv", "") + " vs "
+ root.shortName(root.compareFileB).replace(".csv", "")).toUpperCase()
font.pixelSize: Theme.fontXs
color: Theme.sideMutedText
elide: Text.ElideMiddle
}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: Theme.fieldBackground
border.color: Theme.cardBorder
border.width: 1
radius: Theme.radiusSm
clip: true
WaferMapItem {
id: overlapMapItem
anchors.fill: parent
visible: root.compareSensorLayout.length > 0 && enableOverlapCheck.checked
sensors: root.compareSensorLayout
values: root.compareSensorDiff
bands: root.diffBands(root.compareSensorDiff)
shape: root.compareWaferShape
size: root.compareWaferSize
target: 0
margin: 1.0
blend: blendSlider.value / 100
showLabels: true
ringColor: Theme.waferRingColor
axisColor: Theme.waferAxisColor
lowColor: Theme.sensorLow
inRangeColor: Theme.sensorInRange
highColor: Theme.sensorHigh
textColor: Theme.headingColor
}
ColumnLayout {
anchors.centerIn: parent
visible: root.compareSensorLayout.length === 0 || !enableOverlapCheck.checked
spacing: 4
IconImage {
Layout.alignment: Qt.AlignHCenter
source: "icons/map.svg"
width: 24; height: 24
sourceSize.width: 24
sourceSize.height: 24
color: Theme.sideMutedText
}
Label {
Layout.alignment: Qt.AlignHCenter
text: !enableOverlapCheck.checked ? "Overlap map disabled"
: "No sensor map available"
color: Theme.bodyColor
font.pixelSize: Theme.fontXs
}
}
}
}
}
}
}
// --- Right Column: run selection, overlap settings, readout ---
ColumnLayout {
@@ -801,85 +794,40 @@ Item {
Layout.fillHeight: true
spacing: 12
// Bento 1: Run Selection
// Bento 3: DTW Comparison Readout
PanelBox {
Layout.fillWidth: true
Layout.preferredHeight: runSelCol.implicitHeight + 24
Layout.preferredHeight: readoutCol.implicitHeight + 24
ColumnLayout {
id: runSelCol
id: readoutCol
anchors.fill: parent
anchors.margins: 12
spacing: 10
spacing: 8
SectionTitle { text: "RUN SELECTION" }
SectionTitle { text: "DTW READOUT" }
RunSlot {
title: "REFERENCE (RUN A)"
boxIndex: 1
accent: Theme.primaryAccent
filePath: root.compareFileA
onCleared: {
root.compareFileA = ""
root.activeBox = 1
root.clearResults()
}
ReadoutCard {
label: "DTW ALIGNMENT DISTANCE"
value: root.warpingDistance >= 0 ? root.warpingDistance.toFixed(2) : "--"
valueColor: root.warpingDistance >= 0
? (root.warpingDistance < 50 ? Theme.metricGood : root.warpingDistance < 100 ? Theme.metricWarn : Theme.metricBad)
: Theme.sideMutedText
}
RunSlot {
title: "SESSION (RUN B)"
boxIndex: 2
accent: Theme.themeSkill
filePath: root.compareFileB
onCleared: {
root.compareFileB = ""
root.activeBox = 2
root.clearResults()
}
ReadoutCard {
label: "TEMPORAL FRAME OFFSET"
value: root.warpingDistance >= 0
? ((root.frameOffset >= 0 ? "+" : "") + root.frameOffset + " frames / "
+ (root.frameOffsetSeconds >= 0 ? "+" : "") + root.frameOffsetSeconds.toFixed(1) + "s")
: "--"
valueColor: root.warpingDistance >= 0 ? Theme.headingColor : Theme.sideMutedText
}
Button {
id: compareBtn
Layout.fillWidth: true
Layout.preferredHeight: 32
readonly property bool sameFile: root.compareFileA !== "" && root.compareFileA === root.compareFileB
enabled: root.compareFileA !== "" && root.compareFileB !== "" && !sameFile && !root.comparing
ToolTip.visible: sameFile && hovered
ToolTip.text: "Choose two different runs to compare"
onClicked: {
root.comparing = true
root.clearResults()
streamController.compareFiles(root.compareFileA, root.compareFileB)
}
background: Rectangle {
radius: Theme.radiusSm
color: compareBtn.enabled
? (compareBtn.hovered ? Qt.lighter(Theme.primaryAccent, 1.1) : Theme.primaryAccent)
: Theme.buttonNeutralBackground
Behavior on color { ColorAnimation { duration: Theme.durationFast } }
}
contentItem: Row {
spacing: 8
anchors.centerIn: parent
Label {
anchors.verticalCenter: parent.verticalCenter
text: root.comparing
? "Comparing…"
: compareBtn.enabled
? "Run DTW Comparison"
: (compareBtn.sameFile ? "Choose different runs" : "Select both runs")
color: compareBtn.enabled ? Theme.tone100 : Theme.sideMutedText
font.pixelSize: Theme.fontSm
font.bold: true
}
BusyIndicator {
running: root.comparing
visible: root.comparing
width: 14; height: 14
anchors.verticalCenter: parent.verticalCenter
}
}
ReadoutCard {
label: "MAX SENSOR DEVIATION"
value: root.maxSensorDeviation >= 0 ? root.maxSensorDeviation.toFixed(2) + "°C" : "--"
valueColor: root.maxSensorDeviation >= 0
? (root.maxSensorDeviation < 1.0 ? Theme.metricGood : root.maxSensorDeviation < 3.0 ? Theme.metricWarn : Theme.metricBad)
: Theme.sideMutedText
}
}
}
@@ -946,57 +894,104 @@ Item {
Row {
spacing: 3
Rectangle { width: 8; height: 8; radius: 4; color: Theme.sensorLow; anchors.verticalCenter: parent.verticalCenter }
Label { text: "B cooler"; font.pixelSize: Theme.fontXs; color: Theme.sideMutedText }
Label { text: "B cooler"; font.pixelSize: Theme.fontSm; color: Theme.sideMutedText }
}
Row {
spacing: 3
Rectangle { width: 8; height: 8; radius: 4; color: Theme.sensorInRange; anchors.verticalCenter: parent.verticalCenter }
Label { text: "Match"; font.pixelSize: Theme.fontXs; color: Theme.sideMutedText }
Label { text: "Match"; font.pixelSize: Theme.fontSm; color: Theme.sideMutedText }
}
Row {
spacing: 3
Rectangle { width: 8; height: 8; radius: 4; color: Theme.sensorHigh; anchors.verticalCenter: parent.verticalCenter }
Label { text: "B hotter"; font.pixelSize: Theme.fontXs; color: Theme.sideMutedText }
Label { text: "B hotter"; font.pixelSize: Theme.fontSm; color: Theme.sideMutedText }
}
}
}
}
}
// Bento 3: DTW Comparison Readout
// Bento: Frame / Time Controls
PanelBox {
Layout.fillWidth: true
Layout.preferredHeight: readoutCol.implicitHeight + 24
Layout.preferredHeight: frameTimeCol.implicitHeight + 32
ColumnLayout {
id: readoutCol
id: frameTimeCol
anchors.fill: parent
anchors.margins: 16
spacing: 20
SectionTitle { text: "FRAME / TIME" }
EditableScrubStat {
label: "Frame"
value: scrubReadout.scrubIdx
unit: "/ " + Math.max(0, root.seriesLen - 1) + (Math.max(0, root.seriesLen - 1) <= 1 ? " frame" : " frames")
editable: root.hasSeries
onValueEdited: (newValue) => {
var maxVal = Math.max(0, root.seriesLen - 1)
var val = Math.max(0, Math.min(newValue, maxVal))
scrubber.value = val
}
}
EditableScrubStat {
label: "Time"
value: scrubReadout.scrubTime
unit: "/ " + scrubReadout.maxTime.toFixed(1) + "s"
editable: root.hasSeries
onValueEdited: (newValue) => {
var closestIdx = root.frameForTime(newValue)
scrubber.value = closestIdx
}
}
}
}
// Bento: Scrub Readout
PanelBox {
Layout.fillWidth: true
Layout.preferredHeight: scrubReadout.implicitHeight + 24
ColumnLayout {
id: scrubReadout
anchors.fill: parent
anchors.margins: 12
spacing: 8
SectionTitle { text: "DTW READOUT" }
readonly property int scrubIdx: Math.round(scrubber.value)
readonly property bool hasA: scrubIdx < root.trendDataA.length
readonly property bool hasB: scrubIdx < root.trendDataB.length
readonly property real tempA: hasA ? root.trendDataA[scrubIdx] : 0
readonly property real tempB: hasB ? root.trendDataB[scrubIdx] : 0
readonly property real scrubTime: {
var t = scrubIdx < root.timeDataA.length ? root.timeDataA[scrubIdx]
: (scrubIdx < root.timeDataB.length ? root.timeDataB[scrubIdx] : 0)
return t
}
readonly property real maxTime: {
var ta = root.timeDataA.length > 0 ? root.timeDataA[root.timeDataA.length - 1] : 0
var tb = root.timeDataB.length > 0 ? root.timeDataB[root.timeDataB.length - 1] : 0
return Math.max(ta, tb)
}
ReadoutCard {
label: "DTW ALIGNMENT DISTANCE"
value: root.warpingDistance >= 0 ? root.warpingDistance.toFixed(2) : "--"
valueColor: root.warpingDistance >= 0
? (root.warpingDistance < 50 ? Theme.metricGood : root.warpingDistance < 100 ? Theme.metricWarn : Theme.metricBad)
: Theme.sideMutedText
SectionTitle { text: "SCRUBBER READOUT" }
ScrubStat {
label: "Temp (Run A)"
value: scrubReadout.hasA ? scrubReadout.tempA.toFixed(1) + "°C" : "none"
valueColor: Theme.primaryAccent
}
ReadoutCard {
label: "TEMPORAL FRAME OFFSET"
value: root.warpingDistance >= 0
? ((root.frameOffset >= 0 ? "+" : "") + root.frameOffset + " frames / "
+ (root.frameOffsetSeconds >= 0 ? "+" : "") + root.frameOffsetSeconds.toFixed(1) + "s")
: "--"
valueColor: root.warpingDistance >= 0 ? Theme.headingColor : Theme.sideMutedText
ScrubStat {
label: "Temp (Run B)"
value: scrubReadout.hasB ? scrubReadout.tempB.toFixed(1) + "°C" : "none"
valueColor: Theme.themeSkill
}
ReadoutCard {
label: "MAX SENSOR DEVIATION"
value: root.maxSensorDeviation >= 0 ? root.maxSensorDeviation.toFixed(2) + "°C" : "--"
valueColor: root.maxSensorDeviation >= 0
? (root.maxSensorDeviation < 1.0 ? Theme.metricGood : root.maxSensorDeviation < 3.0 ? Theme.metricWarn : Theme.metricBad)
: Theme.sideMutedText
ScrubStat {
label: "Difference"
value: (scrubReadout.hasA && scrubReadout.hasB)
? Math.abs(scrubReadout.tempA - scrubReadout.tempB).toFixed(1) + "°C" : "none"
}
}
}
+44 -50
View File
@@ -74,8 +74,8 @@ Item {
}
TabButton {
text: "Review"
implicitWidth: 64
implicitHeight: 28
implicitWidth: 84
implicitHeight: 34
background: Rectangle {
color: parent.checked ? Theme.tabActiveBackground : "transparent"
radius: Theme.radiusSm - 1
@@ -83,29 +83,61 @@ Item {
contentItem: Text {
text: parent.text
color: parent.checked ? Theme.headingColor : Theme.bodyColor
font.pixelSize: Theme.fontSm
font.pixelSize: Theme.fontMd
font.weight: parent.checked ? Font.Medium : Font.Normal
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
TabButton {
id: liveTab
text: "Live"
enabled: deviceController.connectionStatus === "Connected"
opacity: enabled ? 1.0 : 0.4
implicitWidth: 58
implicitHeight: 28
implicitWidth: 84
implicitHeight: 34
// Disabled controls swallow no events at all, so hover for the
// "why is this greyed out" tooltip needs its own MouseArea.
ToolTip.visible: disabledHover.containsMouse && !enabled
ToolTip.text: "Connect a wafer to enable Live mode"
ToolTip.delay: 300
MouseArea {
id: disabledHover
anchors.fill: parent
enabled: !liveTab.enabled
hoverEnabled: true
acceptedButtons: Qt.NoButton
}
background: Rectangle {
color: parent.checked ? Theme.tabActiveBackground : "transparent"
radius: Theme.radiusSm - 1
}
contentItem: Text {
text: parent.text
color: parent.checked ? Theme.headingColor : Theme.bodyColor
font.pixelSize: Theme.fontSm
font.weight: parent.checked ? Font.Medium : Font.Normal
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
contentItem: Row {
spacing: 5
leftPadding: 8
Rectangle {
id: liveTabDot
width: 7; height: 7; radius: 3.5
anchors.verticalCenter: parent.verticalCenter
color: liveTab.enabled ? Theme.metricGood : Theme.sideMutedText
// Flash while actually streaming live
SequentialAnimation on opacity {
running: streamController.mode === "live" && streamController.state !== "idle"
loops: Animation.Infinite
NumberAnimation { to: 0.2; duration: 600; easing.type: Easing.InOutQuad }
NumberAnimation { to: 1.0; duration: 600; easing.type: Easing.InOutQuad }
onRunningChanged: if (!running) liveTabDot.opacity = 1.0
}
}
Text {
text: liveTab.text
color: liveTab.checked ? Theme.headingColor : Theme.bodyColor
font.pixelSize: Theme.fontMd
font.weight: liveTab.checked ? Font.Medium : Font.Normal
verticalAlignment: Text.AlignVCenter
height: parent.height
}
}
}
onCurrentIndexChanged: if (currentIndex === 1) {
@@ -127,44 +159,6 @@ Item {
}
}
// Live source info: WiFi icon + port · serial
RowLayout {
visible: streamController.mode === "live"
spacing: 5
// Live status dot (matches the connection-flow indicator dot elsewhere)
Rectangle {
id: liveIndicator
width: 8; height: 8
radius: 4
Layout.alignment: Qt.AlignVCenter
color: (deviceController.connectionStatus === "Connected" && streamController.state !== "idle") ? Theme.liveColor : Theme.bodyColor
SequentialAnimation on opacity {
running: deviceController.connectionStatus === "Connected" && streamController.state !== "idle"
loops: Animation.Infinite
NumberAnimation {
to: 0.2
duration: 600
easing.type: Easing.InOutQuad
}
NumberAnimation {
to: 1.0
duration: 600
easing.type: Easing.InOutQuad
}
onRunningChanged: {
if (!running)
liveIndicator.opacity = 1.0;
}
}
}
Label {
text: "Live stream"
color: Theme.bodyColor
font.pixelSize: Theme.fontXs
}
}
Item {
Layout.fillWidth: true
}
+59 -7
View File
@@ -190,14 +190,35 @@ ColumnLayout {
return (modelData.baseName + modelData.waferType + modelData.date).toLowerCase().indexOf(q) >= 0;
}
// Active highlight logic: on Data tab, highlight compareFileA/B; otherwise highlight loadedFile (for Map tab).
readonly property bool isActive:{
readonly property var dataTab: {
try {
if (root.selectedTabIndex === 1 && dataTabLoader.item) {
return modelData.fileName === dataTabLoader.item.compareFileA || modelData.fileName === dataTabLoader.item.compareFileB;
}
} catch (e) {}
return streamController.mode === "review" && streamController.loadedFile !== "" && modelData.fileName === streamController.loadedFile;
return (root.selectedTabIndex === 1 && dataTabLoader.item) ? dataTabLoader.item : null;
} catch (e) { return null; }
}
// Run A's effective file: the master file when "Compare vs Master
// File" is toggled on, otherwise the manually selected file.
readonly property string effectiveRunAFile: fileItem.dataTab
? (fileItem.dataTab.useMasterFile ? fileItem.dataTab.masterFileForRunB : fileItem.dataTab.compareFileA)
: ""
readonly property bool isRunA: fileItem.dataTab !== null
&& fileItem.effectiveRunAFile !== "" && modelData.fileName === fileItem.effectiveRunAFile
readonly property bool isRunB: fileItem.dataTab !== null
&& modelData.fileName === fileItem.dataTab.compareFileB
// Active highlight logic: on Data tab, highlight the Run A/Run B files (or the
// resolved master file standing in for Run A); otherwise highlight loadedFile (Map tab).
readonly property bool isActive: fileItem.dataTab !== null
? (fileItem.isRunA || fileItem.isRunB)
: (streamController.mode === "review" && streamController.loadedFile !== "" && modelData.fileName === streamController.loadedFile)
// A file registered as any family's master in Settings → Master Files.
readonly property bool isMasterFile: {
var m = settingsModel.masters
for (var fam in m) {
if (m[fam] && m[fam] === modelData.fileName) return true
}
return false
}
readonly property bool isLiveFile: (modelData.baseName || "").toLowerCase().indexOf("live") >= 0
@@ -233,6 +254,8 @@ ColumnLayout {
bottomMargin: 5
}
color: {
if (fileItem.isRunA) return Theme.primaryAccent;
if (fileItem.isRunB) return Theme.themeSkill;
var t = modelData.waferType;
if (t === "A" || t === "E" || t === "P")
return Theme.familyBlueAccent;
@@ -290,6 +313,16 @@ ColumnLayout {
anchors.topMargin: -2
anchors.rightMargin: -2
}
// Accent ring — this file is registered as a master (Settings → Master Files).
Rectangle {
visible: fileItem.isMasterFile
anchors.fill: parent
radius: parent.radius
color: "transparent"
border.color: Theme.primaryAccent
border.width: 2
}
}
// Typography hierarchy
@@ -329,6 +362,25 @@ ColumnLayout {
font.letterSpacing: 0.6
}
}
Rectangle {
visible: fileItem.isMasterFile
radius: 4
color: Qt.alpha(Theme.primaryAccent, 0.18)
implicitWidth: masterLabel.implicitWidth + 10
implicitHeight: masterLabel.implicitHeight + 4
Layout.alignment: Qt.AlignVCenter
Label {
id: masterLabel
anchors.centerIn: parent
text: "MASTER"
color: Theme.primaryAccent
font.pixelSize: Theme.font2xs
font.bold: true
font.letterSpacing: 0.6
}
}
}
// Secondary: date · time