feat: implement master file configuration, UI indicators, and a Run Comparison tool for the Data tab
This commit is contained in:
+330
-335
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user