9e3bec9031
- Parse F1 debug data via family-independent temp formula (bits 4-14 scaled by 2**(i-8), sign at bit 0) - Save sensor+debug temps side-by-side to debug_info.csv - Wire READ DEBUG button in StatusActionsPanel - Rename ReplayChart → RunChart; move into GraphTab - Extract PanelBox/SectionTitle/PanelSlider in ReadoutPanel to cut ~300 lines of duplicated styling - Add rightRailWidth token to Theme for consistent rail sizing - Wrap ReadoutPanel in ScrollView so Thresholds card stays reachable when E-C delta rows appear
376 lines
15 KiB
QML
376 lines
15 KiB
QML
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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|