fix: comparison results never reached QML; restore click-box run selection

This commit is contained in:
jack
2026-07-06 13:12:58 -07:00
parent f604308466
commit 3b919f576f
3 changed files with 137 additions and 149 deletions
+89 -143
View File
@@ -16,6 +16,7 @@ Item {
anchors.fill: parent anchors.fill: parent
// Properties for DTW Comparison // 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 compareFileA: ""
property string compareFileB: "" property string compareFileB: ""
property real warpingDistance: -1 property real warpingDistance: -1
@@ -43,17 +44,6 @@ Item {
return path ? path.split("/").pop() : "" return path ? path.split("/").pop() : ""
} }
// Badge colors per wafer type, matching the mockup's file-row chips
function badgeFg(t) {
return t === "Z" ? "#a78bfa" : t === "B" ? "#93c5fd" : t === "A" ? "#6ee7b7" : "#fde047"
}
function badgeBg(t) {
return t === "Z" ? Qt.rgba(139 / 255, 92 / 255, 246 / 255, 0.15)
: t === "B" ? Qt.rgba(59 / 255, 130 / 255, 246 / 255, 0.15)
: t === "A" ? Qt.rgba(16 / 255, 185 / 255, 129 / 255, 0.15)
: Qt.rgba(245 / 255, 158 / 255, 11 / 255, 0.15)
}
function clearResults() { function clearResults() {
root.warpingDistance = -1 root.warpingDistance = -1
root.frameOffset = 0 root.frameOffset = 0
@@ -69,15 +59,26 @@ Item {
root.compareFileA = "" root.compareFileA = ""
root.compareFileB = "" root.compareFileB = ""
root.comparing = false root.comparing = false
runACombo.currentIndex = -1 root.activeBox = 1
runBCombo.currentIndex = -1
clearResults() clearResults()
} }
Component.onCompleted: file_browser.refreshFiles() Component.onCompleted: file_browser.refreshFiles()
// Clicking a file in the left rail's browser populates the armed run slot
Connections { Connections {
target: streamController 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) { function onComparisonResult(result) {
root.comparing = false root.comparing = false
if (result && result.success) { if (result && result.success) {
@@ -183,124 +184,79 @@ Item {
} }
} }
// Run selector dropdown fed by the left-rail file browser model // Run slot card: click to arm, then pick a file in the left rail's browser
component RunSelector: ComboBox { component RunSlot: Rectangle {
id: selector id: slotBox
property string placeholder: "Select CSV…" property int boxIndex: 1
property string title
property string filePath: ""
property color accent: Theme.primaryAccent
signal cleared()
Layout.fillWidth: true Layout.fillWidth: true
implicitHeight: 36 implicitHeight: 58
model: file_browser.files radius: Theme.radiusSm
currentIndex: -1 color: Theme.fieldBackground
border.width: root.activeBox === boxIndex ? 2 : 1
border.color: root.activeBox === boxIndex ? accent : Theme.cardBorder
readonly property var currentFile: (currentIndex >= 0 && currentIndex < file_browser.files.length) Behavior on border.color { ColorAnimation { duration: Theme.durationFast } }
? file_browser.files[currentIndex] : null
background: Rectangle { MouseArea {
radius: Theme.radiusXs anchors.fill: parent
color: Theme.fieldBackground cursorShape: Qt.PointingHandCursor
border.color: selector.popup.visible ? Theme.fieldBorderFocus : Theme.fieldBorder onClicked: root.activeBox = slotBox.boxIndex
border.width: 1
} }
contentItem: RowLayout { RowLayout {
anchors.fill: parent
anchors.margins: 10
spacing: 8 spacing: 8
Rectangle { Rectangle {
Layout.leftMargin: 8 width: 8; height: 8; radius: 4
width: 16; height: 16; radius: 4 color: slotBox.accent
visible: selector.currentFile !== null Layout.alignment: Qt.AlignVCenter
color: root.badgeBg(selector.currentFile ? selector.currentFile.waferType : "") }
ColumnLayout {
Layout.fillWidth: true
spacing: 2
Label { Label {
anchors.centerIn: parent text: slotBox.title
text: selector.currentFile ? selector.currentFile.waferType : ""
font.pixelSize: 9 font.pixelSize: 9
font.bold: true font.bold: true
color: root.badgeFg(selector.currentFile ? selector.currentFile.waferType : "") font.letterSpacing: 1.0
color: root.activeBox === slotBox.boxIndex ? slotBox.accent : Theme.sideMutedText
} }
} Label {
Label { text: slotBox.filePath !== ""
Layout.fillWidth: true ? root.shortName(slotBox.filePath)
Layout.leftMargin: selector.currentFile === null ? 8 : 0 : (root.activeBox === slotBox.boxIndex ? "← Select file from left panel…" : "Click to select")
text: selector.currentFile ? selector.currentFile.baseName : selector.placeholder font.pixelSize: Theme.fontXs
font.pixelSize: Theme.fontSm color: slotBox.filePath !== "" ? Theme.headingColor : Theme.bodyColor
font.weight: selector.currentFile ? Font.Medium : Font.Normal opacity: slotBox.filePath !== "" ? 1.0 : 0.6
color: selector.currentFile ? Theme.fieldText : Theme.fieldPlaceholder elide: Text.ElideMiddle
elide: Text.ElideMiddle
}
}
indicator: Label {
x: selector.width - width - 8
y: (selector.height - height) / 2
text: "▾"
font.pixelSize: Theme.fontSm
color: Theme.sideMutedText
}
delegate: ItemDelegate {
id: fileDelegate
width: selector.width
highlighted: selector.highlightedIndex === index
background: Rectangle {
radius: Theme.radiusXs
color: fileDelegate.highlighted ? Theme.buttonNeutralHover : "transparent"
}
contentItem: RowLayout {
spacing: 8
Rectangle {
width: 20; height: 20; radius: 4
color: root.badgeBg(modelData.waferType)
Label {
anchors.centerIn: parent
text: modelData.waferType
font.pixelSize: Theme.fontXs
font.bold: true
color: root.badgeFg(modelData.waferType)
}
}
ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
spacing: 0
Label {
Layout.fillWidth: true
text: modelData.baseName
font.pixelSize: Theme.fontSm
font.weight: Font.Medium
color: Theme.headingColor
elide: Text.ElideMiddle
}
Label {
Layout.fillWidth: true
text: modelData.date || modelData.chamber || ""
visible: text !== ""
font.pixelSize: 9
color: Theme.sideMutedText
elide: Text.ElideRight
}
} }
} }
}
popup: Popup { Button {
y: selector.height + 4 visible: slotBox.filePath !== ""
width: selector.width flat: true
implicitHeight: Math.min(contentItem.implicitHeight + 8, 220) implicitWidth: 24; implicitHeight: 24
padding: 4 onClicked: slotBox.cleared()
background: Rectangle {
background: Rectangle { radius: Theme.radiusSm
radius: Theme.radiusXs color: parent.hovered ? Qt.rgba(1, 1, 1, 0.08) : "transparent"
color: Theme.cardBackground }
border.color: Theme.cardBorder contentItem: Label {
border.width: 1 text: "✕"
} color: Theme.bodyColor
contentItem: ListView { font.pixelSize: Theme.fontSm
clip: true horizontalAlignment: Text.AlignHCenter
implicitHeight: contentHeight verticalAlignment: Text.AlignVCenter
model: selector.popup.visible ? selector.delegateModel : null }
currentIndex: selector.highlightedIndex
ScrollIndicator.vertical: ScrollIndicator {}
} }
} }
} }
@@ -565,7 +521,7 @@ Item {
Label { Label {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
visible: !root.comparing visible: !root.comparing
text: "Pick Run A and Run B in the panel on the right." text: "Click a run box on the right, then select a file from the left panel."
color: Theme.sideMutedText color: Theme.sideMutedText
font.pixelSize: Theme.fontXs font.pixelSize: Theme.fontXs
} }
@@ -739,37 +695,27 @@ Item {
SectionTitle { text: "RUN SELECTION" } SectionTitle { text: "RUN SELECTION" }
ColumnLayout { RunSlot {
Layout.fillWidth: true title: "REFERENCE (RUN A)"
spacing: 4 boxIndex: 1
Label { accent: Theme.primaryAccent
text: "Reference CSV (Run A)" filePath: root.compareFileA
font.pixelSize: 9 onCleared: {
color: Theme.sideMutedText root.compareFileA = ""
} root.activeBox = 1
RunSelector { root.clearResults()
id: runACombo
onActivated: function (index) {
root.compareFileA = file_browser.files[index].fileName
root.clearResults()
}
} }
} }
ColumnLayout { RunSlot {
Layout.fillWidth: true title: "SESSION (RUN B)"
spacing: 4 boxIndex: 2
Label { accent: Theme.themeSkill
text: "Session CSV (Run B)" filePath: root.compareFileB
font.pixelSize: 9 onCleared: {
color: Theme.sideMutedText root.compareFileB = ""
} root.activeBox = 2
RunSelector { root.clearResults()
id: runBCombo
onActivated: function (index) {
root.compareFileB = file_browser.files[index].fileName
root.clearResults()
}
} }
} }
@@ -40,8 +40,10 @@ class SessionController(QObject):
liveStatsChanged = Signal() # emitted when receivedCount or errorCount change liveStatsChanged = Signal() # emitted when receivedCount or errorCount change
# trend: per-frame avg for live graph # trend: per-frame avg for live graph
trendData = Signal(str) # JSON array of floats trendData = Signal(str) # JSON array of floats
comparisonResult = Signal(object) # dict with success, distance, warping_path, max_sensor_deviation, series_a, series_b # NOTE: dict (QVariantMap), not object — Signal(object) delivers Python
splitResult = Signal(object) # dict with success, segments # dicts to QML as an opaque empty wrapper with no accessible fields.
comparisonResult = Signal(dict) # dict with success, distance, warping_path, max_sensor_deviation, series_a, series_b
splitResult = Signal(dict) # dict with success, segments
# private: marshal a worker-thread frame onto the main thread # private: marshal a worker-thread frame onto the main thread
_liveFrame = Signal(object) # Frame _liveFrame = Signal(object) # Frame
_liveError = Signal() # worker-thread error ping _liveError = Signal() # worker-thread error ping
@@ -357,10 +359,11 @@ class SessionController(QObject):
result = compare_runs(series_a, series_b) result = compare_runs(series_a, series_b)
# Mean temporal shif along the DTW path: positive means run B # Mean temporal shift along the DTW path: positive means run B
# reaches the same profile features lather than run A. # reaches the same profile features later than run A.
path = result["warping_path"] path = result["warping_path"]
frame_offset = round(sum(j - i for i, j in path) / len(path)) if path else 0 frame_offset = round(sum(j - i for i, j in path) / len(path)) if path else 0
# Max sensor deviation: walk the DTW-aligned frame pairs and take # Max sensor deviation: walk the DTW-aligned frame pairs and take
# the largest per-sensor abs difference across all sensors, not # the largest per-sensor abs difference across all sensors, not
# just the sensor[0] series used for alignment. # just the sensor[0] series used for alignment.
@@ -422,7 +425,8 @@ class SessionController(QObject):
self.comparisonResult.emit({ self.comparisonResult.emit({
"success": True, "success": True,
"distance": result["distance"], "distance": result["distance"],
"warping_path": result["warping_path"][:50], # limit path size # Lists, not tuples — tuples don't survive QVariant conversion
"warping_path": [list(p) for p in result["warping_path"][:50]],
"frame_offset": frame_offset, "frame_offset": frame_offset,
"max_sensor_deviation": max_sensor_deviation, "max_sensor_deviation": max_sensor_deviation,
"series_a": series_a, "series_a": series_a,
@@ -709,6 +713,9 @@ class SessionController(QObject):
self._trend_timestamps.pop(0) self._trend_timestamps.pop(0)
self._trend_buffer.pop(0) self._trend_buffer.pop(0)
def _flush_repaint(self) -> None: def _flush_repaint(self) -> None:
if not self._dirty: if not self._dirty:
return return
+36 -1
View File
@@ -64,7 +64,42 @@ def test_compare_files_integration(controller):
assert "series_a" in args assert "series_a" in args
assert "series_b" in args assert "series_b" in args
assert args["frame_offset"] == 0 assert args["frame_offset"] == 0
def test_comparison_result_reaches_qml(qapp, controller):
"""The result dict must arrive in QML as a real JS object with fields.
Signal(object) delivers Python dicts to QML as an opaque empty wrapper;
the signal must be declared with a dict/QVariantMap signature.
"""
from PySide6.QtQml import QQmlApplicationEngine
engine = QQmlApplicationEngine()
engine.rootContext().setContextProperty("streamController", controller)
engine.loadData(b"""
import QtQuick
Item {
id: probe
property bool gotSuccess: false
property real gotDistance: -1
Connections {
target: streamController
function onComparisonResult(result) {
probe.gotSuccess = result.success === true
probe.gotDistance = result.distance !== undefined ? result.distance : -1
}
}
}
""")
assert engine.rootObjects(), "probe QML failed to load"
root = engine.rootObjects()[0]
csv_path = "tests/fixtures/sample_stream.csv"
controller.compareFiles(csv_path, csv_path)
qapp.processEvents()
assert root.property("gotSuccess") is True
assert root.property("gotDistance") >= 0
def test_recording_toggle(controller, tmp_path): def test_recording_toggle(controller, tmp_path):
csv_path = str(tmp_path / "rec" / "live_test.csv") csv_path = str(tmp_path / "rec" / "live_test.csv")
controller.startRecording(csv_path, "SN123") controller.startRecording(csv_path, "SN123")