style: increase ReadoutPanel font sizes and add CSV test fixture for data logging

This commit is contained in:
jack
2026-07-07 12:10:47 -07:00
parent 7df7fd4c6f
commit 1e03227788
12 changed files with 587 additions and 315 deletions
+221 -64
View File
@@ -22,11 +22,17 @@ Item {
property string compareFileB: ""
property real warpingDistance: -1
property int frameOffset: 0
property real frameOffsetSeconds: 0
property real maxSensorDeviation: -1
property bool comparing: false
property var trendDataA: []
property var trendDataB: []
property var timeDataA: []
property var timeDataB: []
property int frameCountA: 0
property int frameCountB: 0
property string errorMessage: ""
property string mismatchMessage: ""
// Wafer overlap properties
property var compareSensorLayout: []
@@ -45,15 +51,28 @@ Item {
return path ? path.split("/").pop() : ""
}
function scrubTimeLabel(idx) {
var t = idx < root.timeDataA.length ? root.timeDataA[idx]
: (idx < root.timeDataB.length ? root.timeDataB[idx] : null)
return t !== null ? " (" + t.toFixed(1) + "s)" : ""
}
function clearResults() {
root.warpingDistance = -1
root.frameOffset = 0
root.frameOffsetSeconds = 0
root.maxSensorDeviation = -1
root.trendDataA = []
root.trendDataB = []
root.timeDataA = []
root.timeDataB = []
root.frameCountA = 0
root.frameCountB = 0
root.compareSensorLayout = []
root.compareSensorDiff = []
root.errorMessage = ""
root.mismatchMessage = ""
mismatchHideTimer.stop()
}
function resetComparison() {
@@ -66,6 +85,12 @@ Item {
Component.onCompleted: file_browser.refreshFiles()
Timer {
id: mismatchHideTimer
interval: 6000
onTriggered: root.mismatchMessage = ""
}
// Clicking a file in the left rail's browser populates the armed run slot
Connections {
target: streamController
@@ -85,15 +110,31 @@ Item {
if (result && result.success) {
root.warpingDistance = result.distance
root.frameOffset = result.frame_offset !== undefined ? result.frame_offset : 0
root.frameOffsetSeconds = result.frame_offset_seconds !== undefined ? result.frame_offset_seconds : 0
root.maxSensorDeviation = result.max_sensor_deviation !== undefined ? result.max_sensor_deviation : -1
root.trendDataA = result.series_a || []
root.trendDataB = result.series_b || []
root.timeDataA = result.time_a || []
root.timeDataB = result.time_b || []
root.frameCountA = result.frame_count_a || 0
root.frameCountB = result.frame_count_b || 0
root.compareSensorLayout = result.sensor_layout || []
root.compareSensorDiff = result.sensor_diff || []
root.compareWaferShape = result.wafer_shape || "round"
root.compareWaferSize = result.wafer_size || 300.0
root.errorMessage = ""
scrubber.value = Math.floor(root.seriesLen / 2)
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
root.mismatchMessage = "Run A: " + root.frameCountA + " frames (" + timeA.toFixed(1) + "s) · "
+ "Run B: " + root.frameCountB + " frames (" + timeB.toFixed(1) + "s). "
+ "Deviation beyond the shorter run shows as \"none\"."
mismatchHideTimer.restart()
} else {
root.mismatchMessage = ""
mismatchHideTimer.stop()
}
} else {
root.clearResults()
root.errorMessage = (result && result.error) ? result.error : "Comparison failed"
@@ -103,17 +144,73 @@ Item {
// ── Reusable pieces ────────────────────────────────────────────
component SectionTitle: Label {
font.pixelSize: Theme.fontXs
font.family: Theme.uiFontFamily
font.pixelSize: Theme.fontSm
font.letterSpacing: 1.5
font.bold: true
font.letterSpacing: 1.2
color: Theme.sideMutedText
}
component PanelBox: Rectangle {
color: Theme.cardBackground
border.color: Theme.cardBorder
color: Theme.sidePanelBackground
border.color: Theme.sideBorder
border.width: 1
radius: Theme.radiusMd
radius: Theme.sidePanelRadius
}
// Checkbox styled to match MapTab's right-rail ReadoutPanel (square accent
// indicator + check icon) instead of the platform-default CheckBox look.
component PanelCheckBox: CheckBox {
id: toggle
indicator: Rectangle {
implicitWidth: 18
implicitHeight: 18
x: toggle.leftPadding
y: parent.height / 2 - height / 2
radius: Theme.radiusXs
color: toggle.checked ? Theme.primaryAccent : "transparent"
border.width: Theme.borderThin
border.color: toggle.checked ? Theme.primaryAccent : Theme.fieldBorder
Behavior on color { ColorAnimation { duration: Theme.durationFast } }
Behavior on border.color { ColorAnimation { duration: Theme.durationFast } }
IconImage {
anchors.centerIn: parent
source: "icons/check.svg"
width: 12; height: 12
sourceSize.width: 12
sourceSize.height: 12
color: Theme.panelBackground
opacity: toggle.checked ? 1.0 : 0.0
Behavior on opacity { NumberAnimation { duration: Theme.durationFast } }
}
}
contentItem: Text {
text: toggle.text
color: Theme.headingColor
font.family: Theme.uiFontFamily
verticalAlignment: Text.AlignVCenter
leftPadding: toggle.indicator.width + toggle.spacing
font.pixelSize: toggle.font.pixelSize || Theme.fontXs
}
}
// Slider styled to match MapTab's right-rail heatmap slider (circular
// accent handle that grows slightly on press).
component PanelSlider: Slider {
id: panelSlider
handle: Rectangle {
x: panelSlider.leftPadding + panelSlider.visualPosition * (panelSlider.availableWidth - width)
y: panelSlider.topPadding + panelSlider.availableHeight / 2 - height / 2
implicitWidth: 18
implicitHeight: 18
radius: 9
color: Theme.primaryAccent
scale: panelSlider.pressed ? 1.15 : 1.0
Behavior on scale { NumberAnimation { duration: Theme.durationFast; easing.type: Theme.easeStandard } }
}
}
// Mini stat box under the alignment scrubber (mockup's subtle-box trio)
@@ -170,7 +267,7 @@ Item {
spacing: 2
Label {
text: readoutCard.label
font.pixelSize: 9
font.pixelSize: Theme.fontXs
font.bold: true
font.letterSpacing: 0.2
color: Theme.sideMutedText
@@ -225,7 +322,7 @@ Item {
spacing: 2
Label {
text: slotBox.title
font.pixelSize: 9
font.pixelSize: Theme.fontXs
font.bold: true
font.letterSpacing: 1.0
color: root.activeBox === slotBox.boxIndex ? slotBox.accent : Theme.sideMutedText
@@ -234,7 +331,7 @@ Item {
text: slotBox.filePath !== ""
? root.shortName(slotBox.filePath)
: (root.activeBox === slotBox.boxIndex ? "<- Select file from left panel..." : "Click to select")
font.pixelSize: Theme.fontXs
font.pixelSize: Theme.fontSm
color: slotBox.filePath !== "" ? Theme.headingColor : Theme.bodyColor
opacity: slotBox.filePath !== "" ? 1.0 : 0.6
elide: Text.ElideMiddle
@@ -371,7 +468,7 @@ Item {
RowLayout {
Layout.fillWidth: true
SectionTitle {
text: "ALIGNED TEMP CURVE OVERLAY (DTW)"
text: "CURVE OVERLAY"
Layout.fillWidth: true
}
Row {
@@ -406,8 +503,8 @@ Item {
readonly property real padLeft: 50
readonly property real padRight: 16
readonly property real padTop: 16
readonly property real padBottom: 28
readonly property real padTop: 24
readonly property real padBottom: 36
function drawGrid(ctx, minV, maxV, dataLen) {
var plotW = width - padLeft - padRight
@@ -417,7 +514,7 @@ Item {
ctx.strokeStyle = Theme.chartGridLine
ctx.lineWidth = 1
ctx.fillStyle = Theme.chartAxisText
ctx.font = "10px sans-serif"
ctx.font = "10px " + Theme.uiFontFamily
ctx.textAlign = "right"
var ySteps = 5
@@ -440,21 +537,27 @@ Item {
var xFrac = xi / xSteps
var xPx = padLeft + plotW * xFrac
var xIdx = Math.round(xFrac * (dataLen - 1))
ctx.fillText(xIdx.toString(), xPx, height - 6)
ctx.fillText(xIdx.toString(), xPx, height - 20)
}
ctx.save()
ctx.translate(12, padTop + plotH / 2)
ctx.translate(14, padTop + plotH / 2)
ctx.rotate(-Math.PI / 2)
ctx.textAlign = "center"
ctx.fillText("°C", 0, 0)
ctx.font = "12px " + Theme.uiFontFamily
ctx.fillText("Celcius (°C)", 0, 0)
ctx.restore()
ctx.textAlign = "center"
ctx.fillText("Frame", padLeft + plotW / 2, height - 2)
ctx.font = "12px " + Theme.uiFontFamily
ctx.fillText("Frame", padLeft + plotW / 2, height - 4)
}
function drawSeries(ctx, series, minV, range, color) {
function drawSeries(ctx, series, minV, range, color, dataLen) {
// x is mapped against the shared frame domain (dataLen), not the
// series' own length — otherwise a shorter run's line stretches to
// fill the whole plot instead of stopping where its data ends.
if (series.length < 2) return
var plotW = width - padLeft - padRight
var plotH = height - padTop - padBottom
@@ -462,7 +565,7 @@ Item {
ctx.lineWidth = 2
ctx.beginPath()
for (var i = 0; i < series.length; i++) {
var x = padLeft + (i / (series.length - 1)) * plotW
var x = padLeft + (i / (dataLen - 1)) * plotW
var y = padTop + plotH - ((series[i] - minV) / range) * plotH
if (i === 0) ctx.moveTo(x, y)
else ctx.lineTo(x, y)
@@ -497,8 +600,8 @@ Item {
var range = (maxV - minV) || 1
drawGrid(ctx, minV, maxV, root.seriesLen)
drawSeries(ctx, root.trendDataA, minV, range, Theme.primaryAccent)
drawSeries(ctx, root.trendDataB, minV, range, Theme.themeSkill)
drawSeries(ctx, root.trendDataA, minV, range, Theme.primaryAccent, root.seriesLen)
drawSeries(ctx, root.trendDataB, minV, range, Theme.themeSkill, root.seriesLen)
drawScrubMarker(ctx)
}
@@ -556,19 +659,10 @@ Item {
color: Theme.cardBorder
}
RowLayout {
SectionTitle {
text: "TIMELINE SCRUBBER"
Layout.fillWidth: true
SectionTitle {
text: "ALIGNMENT TIMELINE SCRUBBER"
Layout.fillWidth: true
}
Label {
text: "Frame " + Math.round(scrubber.value) + " / " + Math.max(0, root.seriesLen - 1)
font.pixelSize: Theme.fontXs
font.bold: true
font.family: Theme.codeFontFamily
color: Theme.headingColor
}
Layout.topMargin: 8
}
Slider {
@@ -586,24 +680,30 @@ Item {
spacing: 8
readonly property int scrubIdx: Math.round(scrubber.value)
readonly property real tempA: root.trendDataA.length > 0
? root.trendDataA[Math.min(scrubIdx, root.trendDataA.length - 1)] : 0
readonly property real tempB: root.trendDataB.length > 0
? root.trendDataB[Math.min(scrubIdx, root.trendDataB.length - 1)] : 0
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.tempA.toFixed(1) + "°C"
value: scrubReadout.hasA ? scrubReadout.tempA.toFixed(1) + "°C" : "none"
valueColor: Theme.primaryAccent
}
ScrubStat {
label: "Temp (Run B)"
value: scrubReadout.tempB.toFixed(1) + "°C"
value: scrubReadout.hasB ? scrubReadout.tempB.toFixed(1) + "°C" : "none"
valueColor: Theme.themeSkill
}
ScrubStat {
label: "Difference"
value: Math.abs(scrubReadout.tempA - scrubReadout.tempB).toFixed(1) + "°C"
value: (scrubReadout.hasA && scrubReadout.hasB)
? Math.abs(scrubReadout.tempA - scrubReadout.tempB).toFixed(1) + "°C" : "none"
}
}
}
@@ -624,7 +724,7 @@ Item {
RowLayout {
Layout.fillWidth: true
SectionTitle {
text: "WAFER MAP OVERLAP VIEW"
text: "WAFER OVERLAP"
Layout.fillWidth: true
}
Label {
@@ -742,7 +842,10 @@ Item {
id: compareBtn
Layout.fillWidth: true
Layout.preferredHeight: 32
enabled: root.compareFileA !== "" && root.compareFileB !== "" && !root.comparing
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()
@@ -761,11 +864,13 @@ Item {
anchors.centerIn: parent
Label {
anchors.verticalCenter: parent.verticalCenter
text: compareBtn.enabled
? (root.comparing ? "Comparing…" : "Run DTW Comparison")
: (root.comparing ? "Comparing…" : "Select both runs")
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.fontXs
font.pixelSize: Theme.fontSm
font.bold: true
}
BusyIndicator {
@@ -790,22 +895,14 @@ Item {
anchors.margins: 12
spacing: 8
SectionTitle { text: "OVERLAP MAP SETTINGS" }
SectionTitle { text: "OVERLAP SETTINGS" }
CheckBox {
PanelCheckBox {
id: enableOverlapCheck
Layout.fillWidth: true
checked: true
text: "Enable Overlap Map"
font.pixelSize: Theme.fontXs
contentItem: Label {
leftPadding: enableOverlapCheck.indicator.width + 6
text: enableOverlapCheck.text
font.pixelSize: Theme.fontXs
color: Theme.headingColor
verticalAlignment: Text.AlignVCenter
}
font.pixelSize: Theme.fontSm
}
ColumnLayout {
@@ -824,11 +921,11 @@ Item {
spacing: 6
Label {
text: "Blend"
font.pixelSize: Theme.fontXs
font.pixelSize: Theme.fontSm
color: Theme.bodyColor
Layout.preferredWidth: 40
}
Slider {
PanelSlider {
id: blendSlider
Layout.fillWidth: true
from: 0; to: 100; value: 50
@@ -836,7 +933,7 @@ Item {
}
Label {
text: Math.round(blendSlider.value) + "%"
font.pixelSize: Theme.fontXs
font.pixelSize: Theme.fontSm
color: Theme.sideMutedText
Layout.preferredWidth: 30
}
@@ -849,17 +946,17 @@ Item {
Row {
spacing: 3
Rectangle { width: 8; height: 8; radius: 4; color: Theme.sensorLow; anchors.verticalCenter: parent.verticalCenter }
Label { text: "B cooler"; font.pixelSize: 9; color: Theme.sideMutedText }
Label { text: "B cooler"; font.pixelSize: Theme.fontXs; 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: 9; color: Theme.sideMutedText }
Label { text: "Match"; font.pixelSize: Theme.fontXs; 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: 9; color: Theme.sideMutedText }
Label { text: "B hotter"; font.pixelSize: Theme.fontXs; color: Theme.sideMutedText }
}
}
}
@@ -877,7 +974,7 @@ Item {
anchors.margins: 12
spacing: 8
SectionTitle { text: "DTW COMPARISON READOUT" }
SectionTitle { text: "DTW READOUT" }
ReadoutCard {
label: "DTW ALIGNMENT DISTANCE"
@@ -889,7 +986,8 @@ Item {
ReadoutCard {
label: "TEMPORAL FRAME OFFSET"
value: root.warpingDistance >= 0
? ((root.frameOffset >= 0 ? "+" : "") + root.frameOffset + " frames")
? ((root.frameOffset >= 0 ? "+" : "") + root.frameOffset + " frames / "
+ (root.frameOffsetSeconds >= 0 ? "+" : "") + root.frameOffsetSeconds.toFixed(1) + "s")
: "--"
valueColor: root.warpingDistance >= 0 ? Theme.headingColor : Theme.sideMutedText
}
@@ -907,4 +1005,63 @@ Item {
}
}
}
// ── Frame Mismatch Banner (amber, floats over content, auto-hides) ──
Rectangle {
id: mismatchBanner
visible: root.mismatchMessage !== ""
z: 100
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: 12
width: Math.min(parent.width - 48, 640)
height: mismatchRow.implicitHeight + 16
color: Theme.cardBackground
border.color: Theme.statusWarningColor
border.width: 1
radius: Theme.radiusSm
RowLayout {
id: mismatchRow
anchors.fill: parent
anchors.margins: 8
spacing: 8
IconImage {
source: "icons/triangle-alert.svg"
width: 16; height: 16
sourceSize.width: 16
sourceSize.height: 16
color: Theme.statusWarningColor
Layout.alignment: Qt.AlignTop
}
Label {
text: root.mismatchMessage
color: Theme.statusWarningColor
font.pixelSize: Theme.fontSm
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
Button {
flat: true
implicitWidth: 24
implicitHeight: 24
Layout.alignment: Qt.AlignTop
onClicked: {
root.mismatchMessage = ""
mismatchHideTimer.stop()
}
background: Rectangle { color: "transparent" }
contentItem: Label {
text: "✕"
color: Theme.statusWarningColor
font.pixelSize: Theme.fontXs
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
}
}
}