feat: implement master file configuration, UI indicators, and a Run Comparison tool for the Data tab

This commit is contained in:
jack
2026-07-07 15:05:22 -07:00
parent 1e03227788
commit 92f130b3bd
8 changed files with 798 additions and 428 deletions
+287 -7
View File
@@ -181,8 +181,6 @@ Rectangle {
Layout.preferredWidth: Theme.sideRailWidth
Layout.fillHeight: true
color: Theme.sideRailBackground
border.color: Theme.sideBorder
border.width: 1
ColumnLayout {
anchors.fill: parent
@@ -290,6 +288,286 @@ Rectangle {
}
}
// ── BOX 1.5: Run Comparison (Data tab only) ────────────────
Rectangle {
id: runComparisonBox
Layout.fillWidth: true
Layout.preferredHeight: runCompCol.implicitHeight + 28
visible: root.selectedTabIndex === 1
radius: Theme.sidePanelRadius
border.color: Theme.sideBorder
border.width: 1
color: Theme.sidePanelBackground
readonly property var dataTab: dataTabLoader.item
component RunSlot: Rectangle {
id: slotBox
property string title
property string filePath: ""
property color accent: Theme.primaryAccent
property bool active: false
property bool locked: false
property string placeholderText: "Click to select"
signal cleared()
signal armed()
Layout.fillWidth: true
implicitHeight: 58
radius: Theme.radiusSm
color: Theme.fieldBackground
opacity: slotBox.locked ? 0.65 : 1.0
border.width: slotBox.active ? 2 : 1
border.color: slotBox.active ? accent : Theme.sideBorder
Behavior on border.color { ColorAnimation { duration: Theme.durationFast } }
MouseArea {
anchors.fill: parent
enabled: !slotBox.locked
cursorShape: slotBox.locked ? Qt.ArrowCursor : Qt.PointingHandCursor
onClicked: slotBox.armed()
}
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: slotBox.active ? slotBox.accent : Theme.sideMutedText
}
Label {
text: slotBox.filePath !== ""
? slotBox.filePath.split("/").pop()
: (slotBox.locked ? slotBox.placeholderText : (slotBox.active ? "<- Select file below..." : "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 !== "" && !slotBox.locked
flat: true
implicitWidth: 24; implicitHeight: 24
onClicked: slotBox.cleared()
background: Rectangle {
radius: Theme.radiusSm
color: parent.hovered ? Theme.flatButtonHover : "transparent"
}
contentItem: IconImage {
source: "Tabs/icons/x.svg"
width: 14; height: 14
sourceSize.width: 14
sourceSize.height: 14
color: Theme.bodyColor
anchors.centerIn: parent
}
}
}
}
component PanelCheckBox: CheckBox {
id: toggle
indicator: Rectangle {
implicitWidth: 18
implicitHeight: 18
x: toggle.leftPadding
y: parent.height / 2 - height / 2
radius: Theme.radiusXs
color: toggle.checked ? Theme.primaryAccent : "transparent"
border.width: Theme.borderThin
border.color: toggle.checked ? Theme.primaryAccent : Theme.fieldBorder
Behavior on color { ColorAnimation { duration: Theme.durationFast } }
Behavior on border.color { ColorAnimation { duration: Theme.durationFast } }
IconImage {
anchors.centerIn: parent
source: "Tabs/icons/check.svg"
width: 12; height: 12
sourceSize.width: 12
sourceSize.height: 12
color: Theme.panelBackground
opacity: toggle.checked ? 1.0 : 0.0
Behavior on opacity { NumberAnimation { duration: Theme.durationFast } }
}
}
contentItem: Text {
text: toggle.text
color: Theme.headingColor
font.family: Theme.uiFontFamily
verticalAlignment: Text.AlignVCenter
leftPadding: toggle.indicator.width + toggle.spacing
font.pixelSize: toggle.font.pixelSize || Theme.fontXs
}
}
ColumnLayout {
id: runCompCol
anchors.fill: parent
anchors.margins: 14
spacing: 10
RowLayout {
Layout.fillWidth: true
spacing: 8
Label {
text: "RUN COMPARISON (DTW)"
color: Theme.sideMutedText
font.family: Theme.uiFontFamily
font.pixelSize: Theme.fontSm
font.letterSpacing: 1.5
font.bold: true
Layout.fillWidth: true
elide: Text.ElideRight
}
Button {
id: resetRunBtn
flat: true
implicitWidth: 32
implicitHeight: 32
visible: true
onClicked: if (runComparisonBox.dataTab) runComparisonBox.dataTab.resetComparison()
background: Rectangle {
radius: Theme.radiusSm
color: resetRunBtn.hovered ? Theme.flatButtonHover : "transparent"
}
contentItem: IconImage {
source: "Tabs/icons/rotate-ccw.svg"
width: 18; height: 18
sourceSize.width: 18
sourceSize.height: 18
color: Theme.bodyColor
anchors.centerIn: parent
}
ToolTip.visible: resetRunBtn.hovered
ToolTip.text: "Reset comparison"
}
}
PanelCheckBox {
id: useMasterCheck
Layout.fillWidth: true
text: "Compare vs Master File"
checked: runComparisonBox.dataTab ? runComparisonBox.dataTab.useMasterFile : false
onToggled: {
if (!runComparisonBox.dataTab) return
runComparisonBox.dataTab.useMasterFile = checked
if (checked) runComparisonBox.dataTab.activeBox = 2
}
}
RunSlot {
readonly property bool useMaster: runComparisonBox.dataTab ? runComparisonBox.dataTab.useMasterFile : false
title: useMaster ? "MASTER FILE" : "REFERENCE (RUN A)"
accent: Theme.primaryAccent
locked: useMaster
placeholderText: useMaster
? (runComparisonBox.dataTab.runBWaferType !== ""
? "No master file for type " + runComparisonBox.dataTab.runBWaferType
: "Select Run B to resolve master")
: "Click to select"
active: runComparisonBox.dataTab ? (!useMaster && runComparisonBox.dataTab.activeBox === 1) : false
filePath: runComparisonBox.dataTab
? (useMaster ? runComparisonBox.dataTab.masterFileForRunB : runComparisonBox.dataTab.compareFileA)
: ""
onArmed: if (runComparisonBox.dataTab && !useMaster) runComparisonBox.dataTab.activeBox = 1
onCleared: {
if (!runComparisonBox.dataTab) return
runComparisonBox.dataTab.compareFileA = ""
runComparisonBox.dataTab.activeBox = 1
runComparisonBox.dataTab.clearResults()
}
}
RunSlot {
title: "SESSION (RUN B)"
accent: Theme.themeSkill
active: runComparisonBox.dataTab ? runComparisonBox.dataTab.activeBox === 2 : false
filePath: runComparisonBox.dataTab ? runComparisonBox.dataTab.compareFileB : ""
onArmed: if (runComparisonBox.dataTab) runComparisonBox.dataTab.activeBox = 2
onCleared: {
if (!runComparisonBox.dataTab) return
runComparisonBox.dataTab.compareFileB = ""
runComparisonBox.dataTab.activeBox = 2
runComparisonBox.dataTab.clearResults()
}
}
Button {
id: compareRunBtn
Layout.fillWidth: true
Layout.preferredHeight: 32
readonly property var dt: runComparisonBox.dataTab
readonly property string fileA: !!dt ? (dt.useMasterFile ? dt.masterFileForRunB : dt.compareFileA) : ""
readonly property bool masterMissing: !!dt && dt.useMasterFile && dt.masterFileForRunB === ""
readonly property bool sameFile: !!dt && fileA !== "" && fileA === dt.compareFileB
enabled: !!dt && fileA !== "" && dt.compareFileB !== "" && !sameFile && !dt.comparing && !masterMissing
ToolTip.visible: (sameFile || masterMissing) && hovered
ToolTip.text: masterMissing
? "No master file registered for this wafer type (Settings → Master Files)"
: "Choose two different runs to compare"
onClicked: {
dt.comparing = true
dt.clearResults()
streamController.compareFiles(fileA, dt.compareFileB)
}
background: Rectangle {
radius: Theme.radiusSm
color: compareRunBtn.enabled
? (compareRunBtn.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: !compareRunBtn.dt ? "Select both runs"
: compareRunBtn.dt.comparing
? "Comparing…"
: compareRunBtn.enabled
? "Run DTW Comparison"
: (compareRunBtn.masterMissing ? "No master for this type"
: compareRunBtn.sameFile ? "Choose different runs" : "Select both runs")
color: compareRunBtn.enabled ? Theme.tone100 : Theme.sideMutedText
font.pixelSize: Theme.fontSm
font.bold: true
}
BusyIndicator {
running: !!compareRunBtn.dt && compareRunBtn.dt.comparing
visible: !!compareRunBtn.dt && compareRunBtn.dt.comparing
width: 14; height: 14
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
}
// ── BOX 2: Context-Sensitive Rail Panel ────────────────────
Rectangle {
Layout.fillWidth: true
@@ -363,20 +641,23 @@ Rectangle {
}
// ── BOX 3: Hardware Status Footer ──────────────────────────
// Not relevant on the Data tab — Run Comparison takes its place above.
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: Theme.sideFooterConnection
visible: root.selectedTabIndex !== 1
radius: Theme.sidePanelRadius
border.color: Theme.sideBorder
border.width: 1
color: Theme.sidePanelBackground
clip: true
ColumnLayout {
anchors.fill: parent
anchors.margins: 14
spacing: 6
// Row 1: Title + status badge (unchanged)
// Row 1: Title + status badge
RowLayout {
spacing: 8
Layout.fillWidth: true
@@ -388,12 +669,15 @@ Rectangle {
font.pixelSize: Theme.fontSm
font.letterSpacing: 1.5
font.bold: true
Layout.fillWidth: true
elide: Text.ElideRight
}
Rectangle {
height: 18
width: badgeLabel.implicitWidth + 14
radius: 9
Layout.alignment: Qt.AlignVCenter
color: {
var s = deviceController.connectionStatus
if (s === "Connected") return Theme.statusSuccessColor
@@ -410,8 +694,6 @@ Rectangle {
font.bold: true
}
}
Item { Layout.fillWidth: true }
}
// Row 2: Status dot + descriptive text (replaces dotted trail)
@@ -595,8 +877,6 @@ Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: Theme.workspaceBackground
border.color: Theme.workspaceBorder
border.width: 1
StackLayout {
anchors.fill: parent
+330 -335
View File
@@ -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,125 +771,15 @@ 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
}
}
}
}
}
@@ -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
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
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
SectionTitle { text: "DTW 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 / "
+ (root.frameOffsetSeconds >= 0 ? "+" : "") + root.frameOffsetSeconds.toFixed(1) + "s")
: "--"
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
}
}
}
@@ -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"
}
}
}
+43 -49
View File
@@ -74,8 +74,8 @@ Item {
}
TabButton {
text: "Review"
implicitWidth: 64
implicitHeight: 28
implicitWidth: 84
implicitHeight: 34
background: Rectangle {
color: parent.checked ? Theme.tabActiveBackground : "transparent"
radius: Theme.radiusSm - 1
@@ -83,29 +83,61 @@ Item {
contentItem: Text {
text: parent.text
color: parent.checked ? Theme.headingColor : Theme.bodyColor
font.pixelSize: Theme.fontSm
font.pixelSize: Theme.fontMd
font.weight: parent.checked ? Font.Medium : Font.Normal
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
TabButton {
id: liveTab
text: "Live"
enabled: deviceController.connectionStatus === "Connected"
opacity: enabled ? 1.0 : 0.4
implicitWidth: 58
implicitHeight: 28
implicitWidth: 84
implicitHeight: 34
// Disabled controls swallow no events at all, so hover for the
// "why is this greyed out" tooltip needs its own MouseArea.
ToolTip.visible: disabledHover.containsMouse && !enabled
ToolTip.text: "Connect a wafer to enable Live mode"
ToolTip.delay: 300
MouseArea {
id: disabledHover
anchors.fill: parent
enabled: !liveTab.enabled
hoverEnabled: true
acceptedButtons: Qt.NoButton
}
background: Rectangle {
color: parent.checked ? Theme.tabActiveBackground : "transparent"
radius: Theme.radiusSm - 1
}
contentItem: Text {
text: parent.text
color: parent.checked ? Theme.headingColor : Theme.bodyColor
font.pixelSize: Theme.fontSm
font.weight: parent.checked ? Font.Medium : Font.Normal
horizontalAlignment: Text.AlignHCenter
contentItem: Row {
spacing: 5
leftPadding: 8
Rectangle {
id: liveTabDot
width: 7; height: 7; radius: 3.5
anchors.verticalCenter: parent.verticalCenter
color: liveTab.enabled ? Theme.metricGood : Theme.sideMutedText
// Flash while actually streaming live
SequentialAnimation on opacity {
running: streamController.mode === "live" && streamController.state !== "idle"
loops: Animation.Infinite
NumberAnimation { to: 0.2; duration: 600; easing.type: Easing.InOutQuad }
NumberAnimation { to: 1.0; duration: 600; easing.type: Easing.InOutQuad }
onRunningChanged: if (!running) liveTabDot.opacity = 1.0
}
}
Text {
text: liveTab.text
color: liveTab.checked ? Theme.headingColor : Theme.bodyColor
font.pixelSize: Theme.fontMd
font.weight: liveTab.checked ? Font.Medium : Font.Normal
verticalAlignment: Text.AlignVCenter
height: parent.height
}
}
}
onCurrentIndexChanged: if (currentIndex === 1) {
@@ -127,44 +159,6 @@ Item {
}
}
// Live source info: WiFi icon + port · serial
RowLayout {
visible: streamController.mode === "live"
spacing: 5
// Live status dot (matches the connection-flow indicator dot elsewhere)
Rectangle {
id: liveIndicator
width: 8; height: 8
radius: 4
Layout.alignment: Qt.AlignVCenter
color: (deviceController.connectionStatus === "Connected" && streamController.state !== "idle") ? Theme.liveColor : Theme.bodyColor
SequentialAnimation on opacity {
running: deviceController.connectionStatus === "Connected" && streamController.state !== "idle"
loops: Animation.Infinite
NumberAnimation {
to: 0.2
duration: 600
easing.type: Easing.InOutQuad
}
NumberAnimation {
to: 1.0
duration: 600
easing.type: Easing.InOutQuad
}
onRunningChanged: {
if (!running)
liveIndicator.opacity = 1.0;
}
}
}
Label {
text: "Live stream"
color: Theme.bodyColor
font.pixelSize: Theme.fontXs
}
}
Item {
Layout.fillWidth: true
}
+58 -6
View File
@@ -190,14 +190,35 @@ ColumnLayout {
return (modelData.baseName + modelData.waferType + modelData.date).toLowerCase().indexOf(q) >= 0;
}
// Active highlight logic: on Data tab, highlight compareFileA/B; otherwise highlight loadedFile (for Map tab).
readonly property bool isActive:{
readonly property var dataTab: {
try {
if (root.selectedTabIndex === 1 && dataTabLoader.item) {
return modelData.fileName === dataTabLoader.item.compareFileA || modelData.fileName === dataTabLoader.item.compareFileB;
return (root.selectedTabIndex === 1 && dataTabLoader.item) ? dataTabLoader.item : null;
} catch (e) { return null; }
}
} catch (e) {}
return streamController.mode === "review" && streamController.loadedFile !== "" && modelData.fileName === streamController.loadedFile;
// Run A's effective file: the master file when "Compare vs Master
// File" is toggled on, otherwise the manually selected file.
readonly property string effectiveRunAFile: fileItem.dataTab
? (fileItem.dataTab.useMasterFile ? fileItem.dataTab.masterFileForRunB : fileItem.dataTab.compareFileA)
: ""
readonly property bool isRunA: fileItem.dataTab !== null
&& fileItem.effectiveRunAFile !== "" && modelData.fileName === fileItem.effectiveRunAFile
readonly property bool isRunB: fileItem.dataTab !== null
&& modelData.fileName === fileItem.dataTab.compareFileB
// Active highlight logic: on Data tab, highlight the Run A/Run B files (or the
// resolved master file standing in for Run A); otherwise highlight loadedFile (Map tab).
readonly property bool isActive: fileItem.dataTab !== null
? (fileItem.isRunA || fileItem.isRunB)
: (streamController.mode === "review" && streamController.loadedFile !== "" && modelData.fileName === streamController.loadedFile)
// A file registered as any family's master in Settings → Master Files.
readonly property bool isMasterFile: {
var m = settingsModel.masters
for (var fam in m) {
if (m[fam] && m[fam] === modelData.fileName) return true
}
return false
}
readonly property bool isLiveFile: (modelData.baseName || "").toLowerCase().indexOf("live") >= 0
@@ -233,6 +254,8 @@ ColumnLayout {
bottomMargin: 5
}
color: {
if (fileItem.isRunA) return Theme.primaryAccent;
if (fileItem.isRunB) return Theme.themeSkill;
var t = modelData.waferType;
if (t === "A" || t === "E" || t === "P")
return Theme.familyBlueAccent;
@@ -290,6 +313,16 @@ ColumnLayout {
anchors.topMargin: -2
anchors.rightMargin: -2
}
// Accent ring — this file is registered as a master (Settings → Master Files).
Rectangle {
visible: fileItem.isMasterFile
anchors.fill: parent
radius: parent.radius
color: "transparent"
border.color: Theme.primaryAccent
border.width: 2
}
}
// Typography hierarchy
@@ -329,6 +362,25 @@ ColumnLayout {
font.letterSpacing: 0.6
}
}
Rectangle {
visible: fileItem.isMasterFile
radius: 4
color: Qt.alpha(Theme.primaryAccent, 0.18)
implicitWidth: masterLabel.implicitWidth + 10
implicitHeight: masterLabel.implicitHeight + 4
Layout.alignment: Qt.AlignVCenter
Label {
id: masterLabel
anchors.centerIn: parent
text: "MASTER"
color: Theme.primaryAccent
font.pixelSize: Theme.font2xs
font.bold: true
font.letterSpacing: 0.6
}
}
}
// Secondary: date · time
+1 -1
View File
@@ -205,7 +205,7 @@ QtObject {
readonly property int rightPaneGap: 6
readonly property int panelPadding: 12
// Side rail layout
readonly property int sideRailWidth: 280
readonly property int sideRailWidth: 300
readonly property int sideRailMargin: 16
readonly property int sideRailSpacing: 16
readonly property int sideButtonHeight: 44
@@ -35,6 +35,7 @@ class SessionController(QObject):
recordingChanged = Signal()
sensorsChanged = Signal()
loadedFileChanged = Signal()
loadFileError = Signal(str)
clusterAveragingEnabledChanged = Signal()
settingsChanged = Signal() # emitted when session state changes that should be persisted
liveStatsChanged = Signal() # emitted when receivedCount or errorCount change
@@ -90,6 +91,11 @@ class SessionController(QObject):
self._received_count: int = 0
self._error_count: int = 0
# Comparison cache for dynamic overlap mapping
self._compare_recs_a: list = []
self._compare_recs_b: list = []
self._compare_alignment_map: dict[int, int] = {}
# Restore persisted state if available
self._load_settings(settings or {})
@@ -269,24 +275,25 @@ class SessionController(QObject):
frames = []
try:
if is_official_csv(file_path):
sensors, records = read_official_csv(file_path)
frames = frames_from_wafer_data(None, records)
else:
try:
data, _ = ZWaferParser().parse(file_path)
except (ValueError, KeyError) as exc:
log.warning("Could not parse %s: %s", file_path, exc)
return
if data is None or not data.sensors:
log.warning("Could not parse %s", file_path)
self.loadFileError.emit("Invalid layout or missing sensors in custom CSV.")
return
records = read_data_records(file_path)
sensors = data.sensors
frames = frames_from_wafer_data(data, records)
if not sensors or not frames:
log.warning("No sensors or data in %s", file_path)
self.loadFileError.emit("No sensors or frames found in data file.")
return
except Exception as exc:
log.warning("Could not parse file %s: %s", file_path, exc)
self.loadFileError.emit(f"Load error: {exc}")
return
wafer_id = ""
@@ -360,6 +367,9 @@ class SessionController(QObject):
})
return
self._compare_recs_a = recs_a
self._compare_recs_b = recs_b
len_a, len_b = len(recs_a), len(recs_b)
# DTW can only align frames both runs actually have; the longer
# run's extra tail is still returned for display, just unaligned.
@@ -370,6 +380,7 @@ class SessionController(QObject):
time_b = [round(recs_b[i].time, 3) for i in range(len_b)]
result = compare_runs(series_a[:overlap_frames], series_b[:overlap_frames])
self._compare_alignment_map = {i: j for i, j in result["warping_path"]}
# Mean temporal shift along the DTW path: positive means run B
# reaches the same profile features later than run A.
@@ -459,6 +470,25 @@ class SessionController(QObject):
"error": str(exc)
})
@Slot(int, result="QVariantList")
def getSensorDiffAt(self, scrub_idx: int) -> list[float]:
"""Return the per-sensor temp diff (Run B - Run A) at the given scrub frame index of Run A, using DTW alignment."""
if not self._compare_recs_a or not self._compare_recs_b:
return []
idx_a = min(max(0, scrub_idx), len(self._compare_recs_a) - 1)
if idx_a not in self._compare_alignment_map:
# DTW path only covers indices up to the shorter run's length —
# past that, Run B has no data at this frame, not a clampable one.
return []
idx_b = self._compare_alignment_map[idx_a]
values_a = self._compare_recs_a[idx_a].values
values_b = self._compare_recs_b[idx_b].values
n = min(len(values_a), len(values_b))
return [round(values_b[i] - values_a[i], 3) for i in range(n)]
# ---- splitting: threshold-based segmentation ----
@Slot(str, float)
@slot_error_boundary
@@ -12,6 +12,7 @@ from __future__ import annotations
import json
import logging
import math
from typing import Optional
from PySide6.QtCore import Property, QPoint, QRectF, Qt, Signal, Slot
@@ -190,9 +191,14 @@ class TrendChartItem(QQuickPaintedItem):
lo = min(self._data)
hi = max(self._data)
if hi - lo < 1e-9:
return lo - 5.0, hi + 5.0
buf = (hi - lo) * 0.1
return lo - buf, hi + buf
lo, hi = lo - 5.0, hi + 5.0
# Snap to a "nice" tick step (1/2/5 × 10^k) so the axis holds still
# while streaming and only moves when data crosses a step boundary —
# raw min/max rescaled every frame, which read as an unstable chart.
raw_step = (hi - lo) / 4
mag = 10 ** math.floor(math.log10(raw_step))
step = next(s * mag for s in (1, 2, 5, 10) if s * mag >= raw_step)
return math.floor(lo / step) * step, math.ceil(hi / step) * step
def _x_to_px(self, i: int, n: int, plot_rect: QRectF) -> float:
if n <= 1:
@@ -234,7 +240,7 @@ class TrendChartItem(QQuickPaintedItem):
t = i / rows
y_val = y_max - t * (y_max - y_min)
y_px = plot_rect.top() + t * plot_rect.height()
label = f"{y_val:.1f}"
label = f"{y_val:g}"
painter.drawText(QRectF(0, y_px - 7, label_w, 14),
Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter,
label)
@@ -250,12 +256,17 @@ class TrendChartItem(QQuickPaintedItem):
for i in range(cols + 1):
idx = round((i / cols) * (n - 1))
x_px = self._x_to_px(idx, n, plot_rect)
align = (Qt.AlignmentFlag.AlignLeft if i == 0
else Qt.AlignmentFlag.AlignRight if i == cols
else Qt.AlignmentFlag.AlignHCenter)
painter.drawText(QRectF(x_px - 20, y_px, 40, 14),
align | Qt.AlignmentFlag.AlignTop,
str(idx))
if i == 0:
rect = QRectF(x_px, y_px, 40, 14)
align = Qt.AlignmentFlag.AlignLeft
elif i == cols:
# Right-anchor inside the plot so the label never clips off-widget
rect = QRectF(x_px - 40, y_px, 40, 14)
align = Qt.AlignmentFlag.AlignRight
else:
rect = QRectF(x_px - 20, y_px, 40, 14)
align = Qt.AlignmentFlag.AlignHCenter
painter.drawText(rect, align | Qt.AlignmentFlag.AlignTop, str(idx))
def _draw_line(
self,
@@ -347,6 +347,10 @@ class WaferMapItem(QQuickPaintedItem):
self._thickness_heatmap = None
return
field = refine_field(field)
# ponytail: RBF overshoots around outlier sensors, inventing values no
# sensor produced (e.g. negative diffs when all diffs are positive).
# Clamp to the real data range so color only reflects measured values.
field = np.clip(field, vs.min(), vs.max())
if self._shape == "round":
alpha = ellipse_alpha(*field.shape)
else:
@@ -467,6 +471,10 @@ class WaferMapItem(QQuickPaintedItem):
self._heatmap = None
return
field = refine_field(field)
# ponytail: RBF overshoots around outlier sensors, inventing values no
# sensor produced (e.g. negative diffs when all diffs are positive).
# Clamp to the real data range so color only reflects measured values.
field = np.clip(field, vs.min(), vs.max())
if self._shape == "round":
alpha = ellipse_alpha(*field.shape)
else:
@@ -609,11 +617,11 @@ class WaferMapItem(QQuickPaintedItem):
font_scale = 0.85
id_font = QFont()
id_font.setPointSize(max(4, int(r * font_scale)))
id_font.setPointSize(max(9, int(r * font_scale)))
id_font.setBold(True)
temp_font = QFont()
temp_font.setPointSize(max(3, int((r - 1) * font_scale)))
temp_font.setPointSize(max(9, int(r * font_scale)))
band_color = {
"in_range": self._in_range_color,