import QtQuick import QtQuick.Controls import QtQuick.Controls.impl import QtQuick.Layouts import ISC import ISC.Tabs.components 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 bool useMasterFile: false // Run A comes from settingsModel.masters[runBWaferType] instead of manual selection function waferTypeForFile(path) { if (!path) return "" // A file registered as a master carries that slot's family — this is // the only way a live recording gets a family (its filename/metadata // don't encode one reliably). for (var fam in settingsModel.masters) if (settingsModel.masters[fam] === path) return fam.toUpperCase() var rows = file_browser.files for (var i = 0; i < rows.length; i++) { if (rows[i].fileName === path) { if (rows[i].isRecording) return (rows[i].masterType || "").toUpperCase() return (rows[i].waferType || "").toUpperCase() } } var base = path.split("/").pop() if (base.toLowerCase().indexOf("live_") === 0) return "" 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] || "") : "" readonly property string runAWaferType: root.useMasterFile ? root.runBWaferType : root.waferTypeForFile(root.compareFileA) // Family gate: both runs must resolve to the same wafer family. An empty // family means an unassigned live recording — comparable only after it is // registered as a master file (Settings → Master Files). readonly property string familyGateError: { var fileA = root.useMasterFile ? root.masterFileForRunB : root.compareFileA if (fileA === "" || root.compareFileB === "") return "" if (root.runAWaferType === "" || root.runBWaferType === "") return "Recording has no wafer family — register it as a master file (Settings → Master Files)" if (root.runAWaferType !== root.runBWaferType) return "Wafer Family Mismatch: " + root.runAWaferType + " vs " + root.runBWaferType return "" } 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: [] 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 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 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 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() { root.compareFileA = "" root.compareFileB = "" root.useMasterFile = false root.comparing = false root.activeBox = 1 clearResults() } 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 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.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 = "" chartCard.frameIndex = Math.floor(root.seriesLen / 2) chartCard.focusScrubber() 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" } } } // ── Main Layout ─────────────────────────────────────────────── ColumnLayout { anchors.fill: parent anchors.margins: Theme.panelPadding spacing: Theme.rightPaneGap // ── 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 // Wafer Map Overlap View card OverlapMapCard { Layout.fillWidth: true Layout.fillHeight: true Layout.minimumHeight: 220 sensorLayout: root.compareSensorLayout diffValues: streamController.getSensorDiffAt(Math.round(chartCard.frameIndex)) diffBands: root.diffBands waferShape: root.compareWaferShape waferSize: root.compareWaferSize overlapEnabled: sidePanel.overlapEnabled blendAmount: sidePanel.blendAmount } // Chart card with alignment scrubber ComparisonChartCard { id: chartCard Layout.fillWidth: true trendDataA: root.trendDataA trendDataB: root.trendDataB seriesLen: root.seriesLen hasSeries: root.hasSeries comparing: root.comparing } } // --- Right Column: run selection, overlap settings, readout --- ComparisonSidePanel { id: sidePanel Layout.fillWidth: false Layout.preferredWidth: Theme.rightRailWidth Layout.minimumWidth: Theme.rightRailWidth Layout.maximumWidth: Theme.rightRailWidth Layout.fillHeight: true warpingDistance: root.warpingDistance frameOffset: root.frameOffset frameOffsetSeconds: root.frameOffsetSeconds maxSensorDeviation: root.maxSensorDeviation seriesLen: root.seriesLen hasSeries: root.hasSeries trendDataA: root.trendDataA trendDataB: root.trendDataB timeDataA: root.timeDataA timeDataB: root.timeDataB frameForTime: root.frameForTime frameIndex: chartCard.frameIndex onFrameIndexRequested: (idx) => chartCard.frameIndex = idx onScrubberFocusRequested: chartCard.focusScrubber() } } } // ── 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 } } } } }