911 lines
39 KiB
QML
911 lines
39 KiB
QML
import QtQuick
|
|
import QtQuick.Controls
|
|
import QtQuick.Controls.impl
|
|
import QtQuick.Layouts
|
|
import ISC
|
|
import ISC.Wafer
|
|
|
|
// ===== Data Tab (Compare Content Only) =====
|
|
// Per docs/design/navigation-mock.html: the Data tab shows only the Compare
|
|
// Runs content — the raw CSV table ("Inspect Table" mode in earlier drafts)
|
|
// is intentionally not displayed in the application.
|
|
// Layout: left column = aligned-curves chart with alignment scrubber + wafer
|
|
// overlap map; right side panel = run selection dropdowns, overlap settings,
|
|
// and DTW readout cards.
|
|
Item {
|
|
id: root
|
|
anchors.fill: parent
|
|
|
|
// Properties for DTW Comparison
|
|
property int activeBox: 1 // 1 for Reference (Run A), 2 for Session (Run B), 0 for inactive
|
|
property string compareFileA: ""
|
|
property string compareFileB: ""
|
|
property real warpingDistance: -1
|
|
property int frameOffset: 0
|
|
property real maxSensorDeviation: -1
|
|
property bool comparing: false
|
|
property var trendDataA: []
|
|
property var trendDataB: []
|
|
property string errorMessage: ""
|
|
|
|
// Wafer overlap properties
|
|
property var compareSensorLayout: []
|
|
property var compareSensorDiff: []
|
|
property string compareWaferShape: "round"
|
|
property real compareWaferSize: 300.0
|
|
|
|
readonly property bool hasSeries: trendDataA.length > 1 && trendDataB.length > 1
|
|
readonly property int seriesLen: Math.max(trendDataA.length, trendDataB.length)
|
|
|
|
function diffBands(diffs) {
|
|
return diffs.map(d => d > 1.0 ? "high" : d < -1.0 ? "low" : "in_range")
|
|
}
|
|
|
|
function shortName(path) {
|
|
return path ? path.split("/").pop() : ""
|
|
}
|
|
|
|
function clearResults() {
|
|
root.warpingDistance = -1
|
|
root.frameOffset = 0
|
|
root.maxSensorDeviation = -1
|
|
root.trendDataA = []
|
|
root.trendDataB = []
|
|
root.compareSensorLayout = []
|
|
root.compareSensorDiff = []
|
|
root.errorMessage = ""
|
|
}
|
|
|
|
function resetComparison() {
|
|
root.compareFileA = ""
|
|
root.compareFileB = ""
|
|
root.comparing = false
|
|
root.activeBox = 1
|
|
clearResults()
|
|
}
|
|
|
|
Component.onCompleted: file_browser.refreshFiles()
|
|
|
|
// Clicking a file in the left rail's browser populates the armed run slot
|
|
Connections {
|
|
target: streamController
|
|
function onLoadedFileChanged() {
|
|
if (streamController.loadedFile && root.activeBox > 0) {
|
|
if (root.activeBox === 1) {
|
|
root.compareFileA = streamController.loadedFile
|
|
root.activeBox = 2 // Auto-advance to box 2
|
|
} else if (root.activeBox === 2) {
|
|
root.compareFileB = streamController.loadedFile
|
|
root.activeBox = 0 // Finished selecting
|
|
}
|
|
}
|
|
}
|
|
function onComparisonResult(result) {
|
|
root.comparing = false
|
|
if (result && result.success) {
|
|
root.warpingDistance = result.distance
|
|
root.frameOffset = result.frame_offset !== undefined ? result.frame_offset : 0
|
|
root.maxSensorDeviation = result.max_sensor_deviation !== undefined ? result.max_sensor_deviation : -1
|
|
root.trendDataA = result.series_a || []
|
|
root.trendDataB = result.series_b || []
|
|
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)
|
|
} else {
|
|
root.clearResults()
|
|
root.errorMessage = (result && result.error) ? result.error : "Comparison failed"
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Reusable pieces ────────────────────────────────────────────
|
|
component SectionTitle: Label {
|
|
font.pixelSize: Theme.fontXs
|
|
font.bold: true
|
|
font.letterSpacing: 1.2
|
|
color: Theme.sideMutedText
|
|
}
|
|
|
|
component PanelBox: Rectangle {
|
|
color: Theme.cardBackground
|
|
border.color: Theme.cardBorder
|
|
border.width: 1
|
|
radius: Theme.radiusMd
|
|
}
|
|
|
|
// Mini stat box under the alignment scrubber (mockup's subtle-box trio)
|
|
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
|
|
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
|
|
Label {
|
|
text: scrubStat.label
|
|
font.pixelSize: 9
|
|
color: Theme.sideMutedText
|
|
}
|
|
Label {
|
|
text: scrubStat.value
|
|
font.pixelSize: Theme.fontSm
|
|
font.bold: true
|
|
font.family: Theme.codeFontFamily
|
|
color: scrubStat.valueColor
|
|
}
|
|
}
|
|
}
|
|
|
|
// Large labeled value card for the DTW readout panel
|
|
component ReadoutCard: Rectangle {
|
|
id: readoutCard
|
|
property string label
|
|
property string value
|
|
property color valueColor: Theme.headingColor
|
|
Layout.fillWidth: true
|
|
implicitHeight: 52
|
|
radius: Theme.radiusSm
|
|
color: Theme.subtleSectionBackground
|
|
border.color: Theme.cardBorder
|
|
border.width: 1
|
|
|
|
ColumnLayout {
|
|
anchors.fill: parent
|
|
anchors.margins: 8
|
|
spacing: 2
|
|
Label {
|
|
text: readoutCard.label
|
|
font.pixelSize: 9
|
|
font.bold: true
|
|
font.letterSpacing: 0.2
|
|
color: Theme.sideMutedText
|
|
}
|
|
Label {
|
|
text: readoutCard.value
|
|
font.pixelSize: Theme.fontLg
|
|
font.bold: true
|
|
font.family: Theme.codeFontFamily
|
|
color: readoutCard.valueColor
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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: 9
|
|
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.fontXs
|
|
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 {
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: errorRow.implicitHeight + 16
|
|
visible: root.errorMessage !== ""
|
|
color: Theme.errorSurface
|
|
border.color: Theme.statusErrorColor
|
|
border.width: 1
|
|
radius: Theme.radiusSm
|
|
|
|
RowLayout {
|
|
id: errorRow
|
|
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.errorTextSoft
|
|
Layout.alignment: Qt.AlignTop
|
|
}
|
|
|
|
Label {
|
|
id: errorLabel
|
|
text: root.errorMessage
|
|
color: Theme.errorTextSoft
|
|
font.pixelSize: Theme.fontSm
|
|
wrapMode: Text.WordWrap
|
|
Layout.fillWidth: true
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Body: main column | side panel ─────────────────────────
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
spacing: 16
|
|
|
|
// --- Left Column: chart + scrubber, wafer overlap map ---
|
|
ColumnLayout {
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
Layout.minimumWidth: 350
|
|
spacing: 12
|
|
|
|
// Chart card with alignment scrubber
|
|
PanelBox {
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: chartCol.implicitHeight + 24
|
|
|
|
ColumnLayout {
|
|
id: chartCol
|
|
anchors.fill: parent
|
|
anchors.margins: 12
|
|
spacing: 8
|
|
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
SectionTitle {
|
|
text: "ALIGNED TEMP CURVE OVERLAY (DTW)"
|
|
Layout.fillWidth: true
|
|
}
|
|
Row {
|
|
spacing: 16
|
|
visible: root.hasSeries
|
|
Row {
|
|
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
|
|
Rectangle { width: 12; height: 3; radius: 1.5; color: Theme.themeSkill; anchors.verticalCenter: parent.verticalCenter }
|
|
Label { text: "Run B"; font.pixelSize: Theme.fontXs; color: Theme.bodyColor }
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: 200
|
|
color: Theme.fieldBackground
|
|
border.color: Theme.cardBorder
|
|
border.width: 1
|
|
radius: Theme.radiusSm
|
|
clip: true
|
|
|
|
Canvas {
|
|
id: overlayCanvas
|
|
anchors.fill: parent
|
|
visible: root.hasSeries
|
|
|
|
readonly property real padLeft: 50
|
|
readonly property real padRight: 16
|
|
readonly property real padTop: 16
|
|
readonly property real padBottom: 28
|
|
|
|
function drawGrid(ctx, minV, maxV, dataLen) {
|
|
var plotW = width - padLeft - padRight
|
|
var plotH = height - padTop - padBottom
|
|
var range = (maxV - minV) || 1
|
|
|
|
ctx.strokeStyle = Theme.chartGridLine
|
|
ctx.lineWidth = 1
|
|
ctx.fillStyle = Theme.chartAxisText
|
|
ctx.font = "10px sans-serif"
|
|
ctx.textAlign = "right"
|
|
|
|
var ySteps = 5
|
|
for (var yi = 0; yi <= ySteps; yi++) {
|
|
var yFrac = yi / ySteps
|
|
var yPx = padTop + plotH * yFrac
|
|
var yVal = maxV - yFrac * range
|
|
|
|
ctx.beginPath()
|
|
ctx.moveTo(padLeft, yPx)
|
|
ctx.lineTo(padLeft + plotW, yPx)
|
|
ctx.stroke()
|
|
|
|
ctx.fillText(yVal.toFixed(1), padLeft - 6, yPx + 3)
|
|
}
|
|
|
|
ctx.textAlign = "center"
|
|
var xSteps = Math.min(5, dataLen - 1)
|
|
for (var xi = 0; xi <= xSteps; xi++) {
|
|
var xFrac = xi / xSteps
|
|
var xPx = padLeft + plotW * xFrac
|
|
var xIdx = Math.round(xFrac * (dataLen - 1))
|
|
ctx.fillText(xIdx.toString(), xPx, height - 6)
|
|
}
|
|
|
|
ctx.save()
|
|
ctx.translate(12, padTop + plotH / 2)
|
|
ctx.rotate(-Math.PI / 2)
|
|
ctx.textAlign = "center"
|
|
ctx.fillText("°C", 0, 0)
|
|
ctx.restore()
|
|
|
|
ctx.textAlign = "center"
|
|
ctx.fillText("Frame", padLeft + plotW / 2, height - 2)
|
|
}
|
|
|
|
function drawSeries(ctx, series, minV, range, color) {
|
|
var plotW = width - padLeft - padRight
|
|
var plotH = height - padTop - padBottom
|
|
|
|
ctx.strokeStyle = color
|
|
ctx.lineWidth = 2
|
|
ctx.beginPath()
|
|
for (var i = 0; i < series.length; i++) {
|
|
var x = padLeft + (i / (series.length - 1)) * plotW
|
|
var y = padTop + plotH - ((series[i] - minV) / range) * plotH
|
|
if (i === 0) ctx.moveTo(x, y)
|
|
else ctx.lineTo(x, y)
|
|
}
|
|
ctx.stroke()
|
|
}
|
|
|
|
function drawScrubMarker(ctx) {
|
|
if (root.seriesLen < 2) return
|
|
var plotW = width - padLeft - padRight
|
|
var x = padLeft + (scrubber.value / (root.seriesLen - 1)) * plotW
|
|
ctx.strokeStyle = Theme.primaryAccent
|
|
ctx.lineWidth = 1.5
|
|
ctx.setLineDash([3, 3])
|
|
ctx.beginPath()
|
|
ctx.moveTo(x, padTop)
|
|
ctx.lineTo(x, height - padBottom)
|
|
ctx.stroke()
|
|
ctx.setLineDash([])
|
|
}
|
|
|
|
onPaint: {
|
|
var ctx = getContext("2d")
|
|
ctx.reset()
|
|
if (!root.hasSeries) return
|
|
var all = root.trendDataA.concat(root.trendDataB)
|
|
var minV = Math.min.apply(null, all)
|
|
var maxV = Math.max.apply(null, all)
|
|
var padding = (maxV - minV) * 0.05
|
|
minV -= padding
|
|
maxV += padding
|
|
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)
|
|
drawScrubMarker(ctx)
|
|
}
|
|
|
|
Connections {
|
|
target: root
|
|
function onTrendDataAChanged() { overlayCanvas.requestPaint() }
|
|
function onTrendDataBChanged() { overlayCanvas.requestPaint() }
|
|
}
|
|
Connections {
|
|
target: scrubber
|
|
function onValueChanged() { overlayCanvas.requestPaint() }
|
|
}
|
|
}
|
|
|
|
// Empty state
|
|
ColumnLayout {
|
|
anchors.centerIn: parent
|
|
visible: !root.hasSeries
|
|
spacing: 8
|
|
|
|
IconImage {
|
|
Layout.alignment: Qt.AlignHCenter
|
|
visible: !root.comparing
|
|
source: "icons/bar-chart.svg"
|
|
width: 32; height: 32
|
|
sourceSize.width: 32
|
|
sourceSize.height: 32
|
|
color: Theme.sideMutedText
|
|
}
|
|
Label {
|
|
Layout.alignment: Qt.AlignHCenter
|
|
text: root.comparing ? "Running DTW comparison…" : "Select two runs and run comparison"
|
|
color: Theme.bodyColor
|
|
font.pixelSize: Theme.fontSm
|
|
}
|
|
Label {
|
|
Layout.alignment: Qt.AlignHCenter
|
|
visible: !root.comparing
|
|
text: "Click a run box on the right, then select a file from the left panel."
|
|
color: Theme.sideMutedText
|
|
font.pixelSize: Theme.fontXs
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Alignment Timeline Scrubber ────────────
|
|
ColumnLayout {
|
|
Layout.fillWidth: true
|
|
spacing: 4
|
|
visible: root.hasSeries
|
|
|
|
Rectangle {
|
|
Layout.fillWidth: true
|
|
height: 1
|
|
color: Theme.cardBorder
|
|
}
|
|
|
|
RowLayout {
|
|
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
|
|
}
|
|
}
|
|
|
|
Slider {
|
|
id: scrubber
|
|
Layout.fillWidth: true
|
|
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 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
|
|
|
|
ScrubStat {
|
|
label: "Temp (Run A)"
|
|
value: scrubReadout.tempA.toFixed(1) + "°C"
|
|
valueColor: Theme.primaryAccent
|
|
}
|
|
ScrubStat {
|
|
label: "Temp (Run B)"
|
|
value: scrubReadout.tempB.toFixed(1) + "°C"
|
|
valueColor: Theme.themeSkill
|
|
}
|
|
ScrubStat {
|
|
label: "Difference"
|
|
value: Math.abs(scrubReadout.tempA - scrubReadout.tempB).toFixed(1) + "°C"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 MAP OVERLAP VIEW"
|
|
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 {
|
|
Layout.fillWidth: false
|
|
Layout.preferredWidth: 280
|
|
Layout.minimumWidth: 280
|
|
Layout.maximumWidth: 280
|
|
Layout.fillHeight: true
|
|
spacing: 12
|
|
|
|
// Bento 1: Run Selection
|
|
PanelBox {
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: runSelCol.implicitHeight + 24
|
|
|
|
ColumnLayout {
|
|
id: runSelCol
|
|
anchors.fill: parent
|
|
anchors.margins: 12
|
|
spacing: 10
|
|
|
|
SectionTitle { text: "RUN SELECTION" }
|
|
|
|
RunSlot {
|
|
title: "REFERENCE (RUN A)"
|
|
boxIndex: 1
|
|
accent: Theme.primaryAccent
|
|
filePath: root.compareFileA
|
|
onCleared: {
|
|
root.compareFileA = ""
|
|
root.activeBox = 1
|
|
root.clearResults()
|
|
}
|
|
}
|
|
|
|
RunSlot {
|
|
title: "SESSION (RUN B)"
|
|
boxIndex: 2
|
|
accent: Theme.themeSkill
|
|
filePath: root.compareFileB
|
|
onCleared: {
|
|
root.compareFileB = ""
|
|
root.activeBox = 2
|
|
root.clearResults()
|
|
}
|
|
}
|
|
|
|
Button {
|
|
id: compareBtn
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: 32
|
|
enabled: root.compareFileA !== "" && root.compareFileB !== "" && !root.comparing
|
|
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: compareBtn.enabled
|
|
? (root.comparing ? "Comparing…" : "Run DTW Comparison")
|
|
: (root.comparing ? "Comparing…" : "Select both runs")
|
|
color: compareBtn.enabled ? Theme.tone100 : Theme.sideMutedText
|
|
font.pixelSize: Theme.fontXs
|
|
font.bold: true
|
|
}
|
|
BusyIndicator {
|
|
running: root.comparing
|
|
visible: root.comparing
|
|
width: 14; height: 14
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Bento 2: Overlap Map Settings
|
|
PanelBox {
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: overlapCol.implicitHeight + 24
|
|
|
|
ColumnLayout {
|
|
id: overlapCol
|
|
anchors.fill: parent
|
|
anchors.margins: 12
|
|
spacing: 8
|
|
|
|
SectionTitle { text: "OVERLAP MAP SETTINGS" }
|
|
|
|
CheckBox {
|
|
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
|
|
}
|
|
}
|
|
|
|
ColumnLayout {
|
|
Layout.fillWidth: true
|
|
spacing: 6
|
|
visible: enableOverlapCheck.checked
|
|
|
|
Rectangle {
|
|
Layout.fillWidth: true
|
|
height: 1
|
|
color: Theme.cardBorder
|
|
}
|
|
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
spacing: 6
|
|
Label {
|
|
text: "Blend"
|
|
font.pixelSize: Theme.fontXs
|
|
color: Theme.bodyColor
|
|
Layout.preferredWidth: 40
|
|
}
|
|
Slider {
|
|
id: blendSlider
|
|
Layout.fillWidth: true
|
|
from: 0; to: 100; value: 50
|
|
stepSize: 1
|
|
}
|
|
Label {
|
|
text: Math.round(blendSlider.value) + "%"
|
|
font.pixelSize: Theme.fontXs
|
|
color: Theme.sideMutedText
|
|
Layout.preferredWidth: 30
|
|
}
|
|
}
|
|
|
|
// Diff color legend
|
|
Row {
|
|
Layout.fillWidth: true
|
|
spacing: 10
|
|
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 }
|
|
}
|
|
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 }
|
|
}
|
|
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 }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Bento 3: DTW Comparison Readout
|
|
PanelBox {
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: readoutCol.implicitHeight + 24
|
|
|
|
ColumnLayout {
|
|
id: readoutCol
|
|
anchors.fill: parent
|
|
anchors.margins: 12
|
|
spacing: 8
|
|
|
|
SectionTitle { text: "DTW COMPARISON READOUT" }
|
|
|
|
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
|
|
}
|
|
ReadoutCard {
|
|
label: "TEMPORAL FRAME OFFSET"
|
|
value: root.warpingDistance >= 0
|
|
? ((root.frameOffset >= 0 ? "+" : "") + root.frameOffset + " frames")
|
|
: "--"
|
|
valueColor: root.warpingDistance >= 0 ? Theme.headingColor : Theme.sideMutedText
|
|
}
|
|
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
|
|
}
|
|
}
|
|
}
|
|
|
|
Item { Layout.fillHeight: true }
|
|
}
|
|
}
|
|
}
|
|
}
|