feat: add file comparison, data splitting, and sensor modification functionality to SessionController
This commit is contained in:
+746
-122
@@ -2,183 +2,807 @@ import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import ISC
|
||||
import ISC.Wafer
|
||||
|
||||
// ===== Data Tab =====
|
||||
// Displays parsed temperature data in a scrollable table.
|
||||
// Column 0 = Row index, remaining columns = Sensor1, Sensor2, ...
|
||||
// ===== Data Tab (Compare Mode Only) =====
|
||||
// Focuses exclusively on comparing two wafer runs side-by-side using DTW.
|
||||
// Clicking Box 1 or Box 2 activates selection mode, and clicking a file in the
|
||||
// left rail's file browser populates that slot.
|
||||
Item {
|
||||
id: root
|
||||
anchors.fill: parent
|
||||
|
||||
readonly property int rowCount: deviceController.dataRowCount
|
||||
// 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 real maxSensorDeviation: -1
|
||||
property bool comparing: false
|
||||
property var trendDataA: []
|
||||
property var trendDataB: []
|
||||
property string errorMessage: ""
|
||||
|
||||
// Re-layout columns whenever the model is reset
|
||||
// Wafer overlap properties
|
||||
property var compareSensorLayout: []
|
||||
property var compareSensorDiff: []
|
||||
property string compareWaferShape: "round"
|
||||
property real compareWaferSize: 300.0
|
||||
|
||||
function diffBands(diffs) {
|
||||
return diffs.map(d => d > 1.0 ? "high" : d < -1.0 ? "low" : "in_range")
|
||||
}
|
||||
|
||||
function resetComparison() {
|
||||
root.compareFileA = ""
|
||||
root.compareFileB = ""
|
||||
root.warpingDistance = -1
|
||||
root.maxSensorDeviation = -1
|
||||
root.comparing = false
|
||||
root.trendDataA = []
|
||||
root.trendDataB = []
|
||||
root.compareSensorLayout = []
|
||||
root.compareSensorDiff = []
|
||||
root.errorMessage = ""
|
||||
root.activeBox = 1
|
||||
}
|
||||
|
||||
// Connections to handle clicked/selected files on left rail file browser
|
||||
Connections {
|
||||
target: deviceController
|
||||
function onParsedDataReady(result) {
|
||||
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) {
|
||||
dataTable.forceLayout();
|
||||
root.warpingDistance = result.distance
|
||||
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 = ""
|
||||
} else {
|
||||
root.warpingDistance = -1
|
||||
root.maxSensorDeviation = -1
|
||||
root.trendDataA = []
|
||||
root.trendDataB = []
|
||||
root.compareSensorLayout = []
|
||||
root.compareSensorDiff = []
|
||||
root.errorMessage = (result && result.error) ? result.error : "Comparison failed"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Empty State =====
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: 16
|
||||
visible: root.rowCount === 0
|
||||
|
||||
Label {
|
||||
text: "No Data"
|
||||
font.pixelSize: Theme.font2xl
|
||||
font.bold: true
|
||||
color: Theme.headingColor
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
Label {
|
||||
text: "Read and parse wafer data to see temperature readings here."
|
||||
font.pixelSize: Theme.fontMd
|
||||
color: Theme.bodyColor
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Data Table =====
|
||||
// ── Main Layout ───────────────────────────────────────────────
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.panelPadding
|
||||
spacing: Theme.rightPaneGap
|
||||
visible: root.rowCount > 0
|
||||
|
||||
// --- Header row ---
|
||||
// ── Title / Header ─────────────────────────────────────────
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.rightPaneGap
|
||||
spacing: 8
|
||||
|
||||
Label {
|
||||
text: "Temperature Data"
|
||||
text: "Run Comparison (DTW)"
|
||||
font.pixelSize: Theme.fontXl
|
||||
font.bold: true
|
||||
color: Theme.headingColor
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
// Reset button
|
||||
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 ? Qt.rgba(1, 1, 1, 0.08) : "transparent"
|
||||
}
|
||||
|
||||
contentItem: Label {
|
||||
text: "\u21BA"
|
||||
font.pixelSize: Theme.fontLg
|
||||
color: Theme.bodyColor
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
ToolTip.visible: resetBtn.hovered
|
||||
ToolTip.text: "Reset comparison"
|
||||
}
|
||||
|
||||
Label {
|
||||
text: deviceController.dataRowCount + " rows × " + deviceController.dataColCount + " sensors"
|
||||
text: "Dynamic Time Warping Profile Matching"
|
||||
font.pixelSize: Theme.fontSm
|
||||
color: Theme.bodyColor
|
||||
}
|
||||
}
|
||||
|
||||
// --- Toolbar ---
|
||||
// ── Selection Boxes (Box 1 and Box 2) ──────────────────────
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
spacing: 16
|
||||
|
||||
Button {
|
||||
text: "Open in Excel"
|
||||
icon.source: "icons/excel.svg"
|
||||
icon.color: Theme.headingColor
|
||||
onClicked: deviceController.openCsvFile()
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Compare"
|
||||
onClicked: compareDialog.open()
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Split"
|
||||
onClicked: splitDialog.open()
|
||||
}
|
||||
|
||||
Item {
|
||||
// --- Box 1: Reference CSV (Run A) ---
|
||||
Rectangle {
|
||||
id: box1
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
// --- Table container ---
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
color: Theme.panelBackground
|
||||
border.color: Theme.cardBorder
|
||||
border.width: Theme.borderThin
|
||||
radius: Theme.radiusSm
|
||||
clip: true
|
||||
Layout.preferredHeight: 70
|
||||
radius: Theme.radiusMd
|
||||
color: Theme.cardBackground
|
||||
border.width: root.activeBox === 1 ? 2 : 1
|
||||
border.color: root.activeBox === 1 ? Theme.primaryAccent : Theme.cardBorder
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 1
|
||||
spacing: 0
|
||||
Behavior on border.color { ColorAnimation { duration: Theme.durationFast } }
|
||||
|
||||
// Column header strip
|
||||
HorizontalHeaderView {
|
||||
id: headerView
|
||||
Layout.fillWidth: true
|
||||
syncView: dataTable
|
||||
clip: true
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.activeBox = 1
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
implicitHeight: 28
|
||||
color: Theme.cardBackground
|
||||
border.color: Theme.cardBorder
|
||||
border.width: 1
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 14
|
||||
spacing: 10
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
// 'display' is the Qt.DisplayRole value from headerData()
|
||||
text: display !== undefined ? display : ""
|
||||
color: Theme.headingColor
|
||||
// Color indicator dot
|
||||
Rectangle {
|
||||
width: 10; height: 10; radius: 5
|
||||
color: Theme.primaryAccent
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 2
|
||||
Label {
|
||||
text: "REFERENCE (RUN A)"
|
||||
font.pixelSize: Theme.fontXs
|
||||
font.bold: true
|
||||
font.letterSpacing: 1.0
|
||||
color: root.activeBox === 1 ? Theme.primaryAccent : Theme.sideMutedText
|
||||
}
|
||||
Label {
|
||||
text: root.compareFileA !== ""
|
||||
? root.compareFileA.split("/").pop()
|
||||
: (root.activeBox === 1 ? "← Select file from left panel..." : "Click to select")
|
||||
font.pixelSize: Theme.fontSm
|
||||
color: root.compareFileA !== "" ? Theme.headingColor : Theme.bodyColor
|
||||
opacity: root.compareFileA !== "" ? 1.0 : 0.6
|
||||
elide: Text.ElideMiddle
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "✕"
|
||||
visible: root.compareFileA !== ""
|
||||
flat: true
|
||||
implicitWidth: 24; implicitHeight: 24
|
||||
onClicked: {
|
||||
root.compareFileA = ""
|
||||
root.activeBox = 1
|
||||
root.warpingDistance = -1
|
||||
root.trendDataA = []
|
||||
root.trendDataB = []
|
||||
root.compareSensorLayout = []
|
||||
root.compareSensorDiff = []
|
||||
}
|
||||
background: Rectangle {
|
||||
radius: Theme.radiusSm
|
||||
color: parent.hovered ? Qt.rgba(1, 1, 1, 0.08) : "transparent"
|
||||
}
|
||||
contentItem: Label {
|
||||
text: "✕"
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: Theme.fontSm
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Box 2: Session CSV (Run B) ---
|
||||
Rectangle {
|
||||
id: box2
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 70
|
||||
radius: Theme.radiusMd
|
||||
color: Theme.cardBackground
|
||||
border.width: root.activeBox === 2 ? 2 : 1
|
||||
border.color: root.activeBox === 2 ? Theme.themeSkill : Theme.cardBorder
|
||||
|
||||
Behavior on border.color { ColorAnimation { duration: Theme.durationFast } }
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.activeBox = 2
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 14
|
||||
spacing: 10
|
||||
|
||||
// Color indicator dot
|
||||
Rectangle {
|
||||
width: 10; height: 10; radius: 5
|
||||
color: Theme.themeSkill
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 2
|
||||
Label {
|
||||
text: "SESSION (RUN B)"
|
||||
font.pixelSize: Theme.fontXs
|
||||
font.bold: true
|
||||
font.letterSpacing: 1.0
|
||||
color: root.activeBox === 2 ? Theme.themeSkill : Theme.sideMutedText
|
||||
}
|
||||
Label {
|
||||
text: root.compareFileB !== ""
|
||||
? root.compareFileB.split("/").pop()
|
||||
: (root.activeBox === 2 ? "← Select file from left panel..." : "Click to select")
|
||||
font.pixelSize: Theme.fontSm
|
||||
color: root.compareFileB !== "" ? Theme.headingColor : Theme.bodyColor
|
||||
opacity: root.compareFileB !== "" ? 1.0 : 0.6
|
||||
elide: Text.ElideMiddle
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "✕"
|
||||
visible: root.compareFileB !== ""
|
||||
flat: true
|
||||
implicitWidth: 24; implicitHeight: 24
|
||||
onClicked: {
|
||||
root.compareFileB = ""
|
||||
root.activeBox = 2
|
||||
root.warpingDistance = -1
|
||||
root.trendDataA = []
|
||||
root.trendDataB = []
|
||||
root.compareSensorLayout = []
|
||||
root.compareSensorDiff = []
|
||||
}
|
||||
background: Rectangle {
|
||||
radius: Theme.radiusSm
|
||||
color: parent.hovered ? Qt.rgba(1, 1, 1, 0.08) : "transparent"
|
||||
}
|
||||
contentItem: Label {
|
||||
text: "✕"
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: Theme.fontSm
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Compare Button ─────────────────────────────────────────
|
||||
Button {
|
||||
id: compareBtn
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 40
|
||||
enabled: root.compareFileA !== "" && root.compareFileB !== "" && !root.comparing
|
||||
onClicked: {
|
||||
root.comparing = true
|
||||
root.warpingDistance = -1
|
||||
root.maxSensorDeviation = -1
|
||||
root.trendDataA = []
|
||||
root.trendDataB = []
|
||||
root.errorMessage = ""
|
||||
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")
|
||||
: "SELECT BOTH RUNS ABOVE"
|
||||
color: compareBtn.enabled ? Theme.tone100 : Theme.sideMutedText
|
||||
font.pixelSize: Theme.fontSm
|
||||
font.bold: true
|
||||
font.letterSpacing: 0.8
|
||||
}
|
||||
BusyIndicator {
|
||||
running: root.comparing
|
||||
visible: root.comparing
|
||||
width: 16; height: 16
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Error Banner ───────────────────────────────────────────
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: errorLabel.implicitHeight + 16
|
||||
visible: root.errorMessage !== ""
|
||||
color: Qt.rgba(0.93, 0.27, 0.27, 0.15)
|
||||
border.color: "#ef4444"
|
||||
border.width: 1
|
||||
radius: Theme.radiusSm
|
||||
|
||||
Label {
|
||||
id: errorLabel
|
||||
anchors.fill: parent
|
||||
anchors.margins: 8
|
||||
text: "⚠ " + root.errorMessage
|
||||
color: "#fca5a5"
|
||||
font.pixelSize: Theme.fontSm
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
|
||||
// ── DTW Chart + Overlap Wafer Map Row ──────────────────────
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
spacing: 16
|
||||
|
||||
// --- Left Column: Aligned Curves Chart ---
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.minimumWidth: 350
|
||||
color: Theme.cardBackground
|
||||
border.color: Theme.cardBorder
|
||||
border.width: 1
|
||||
radius: Theme.radiusMd
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 16
|
||||
spacing: 10
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Label {
|
||||
text: "Aligned Temperature Curves (DTW)"
|
||||
font.pixelSize: Theme.fontSm
|
||||
font.bold: true
|
||||
color: Theme.headingColor
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
// Legend
|
||||
Row {
|
||||
spacing: 16
|
||||
visible: root.trendDataA.length > 1
|
||||
|
||||
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.fillHeight: true
|
||||
color: Theme.fieldBackground
|
||||
border.color: Theme.cardBorder
|
||||
border.width: 1
|
||||
radius: Theme.radiusSm
|
||||
clip: true
|
||||
|
||||
readonly property bool hasSeries: root.trendDataA.length > 1 && root.trendDataB.length > 1
|
||||
|
||||
Canvas {
|
||||
id: overlayCanvas
|
||||
anchors.fill: parent
|
||||
anchors.margins: 0
|
||||
visible: parent.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 = Qt.rgba(1, 1, 1, 0.08)
|
||||
ctx.lineWidth = 1
|
||||
ctx.fillStyle = Qt.rgba(1, 1, 1, 0.35)
|
||||
ctx.font = "10px sans-serif"
|
||||
ctx.textAlign = "right"
|
||||
|
||||
// Y-axis gridlines + labels
|
||||
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)
|
||||
}
|
||||
|
||||
// X-axis labels
|
||||
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)
|
||||
}
|
||||
|
||||
// Y-axis title
|
||||
ctx.save()
|
||||
ctx.translate(12, padTop + plotH / 2)
|
||||
ctx.rotate(-Math.PI / 2)
|
||||
ctx.textAlign = "center"
|
||||
ctx.fillText("°C", 0, 0)
|
||||
ctx.restore()
|
||||
|
||||
// X-axis title
|
||||
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()
|
||||
}
|
||||
|
||||
onPaint: {
|
||||
var ctx = getContext("2d")
|
||||
ctx.reset()
|
||||
if (!parent.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, Math.max(root.trendDataA.length, root.trendDataB.length))
|
||||
drawSeries(ctx, root.trendDataA, minV, range, Theme.primaryAccent)
|
||||
drawSeries(ctx, root.trendDataB, minV, range, Theme.themeSkill)
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onTrendDataAChanged() { overlayCanvas.requestPaint() }
|
||||
function onTrendDataBChanged() { overlayCanvas.requestPaint() }
|
||||
}
|
||||
}
|
||||
|
||||
// Empty state
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
visible: !parent.hasSeries
|
||||
spacing: 8
|
||||
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: root.comparing ? "" : "📊"
|
||||
font.pixelSize: 32
|
||||
}
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: root.comparing ? "Running DTW comparison…" : "Select two files and run comparison"
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: Theme.fontSm
|
||||
}
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
visible: !root.comparing
|
||||
text: "Click Box 1, then a file. Click Box 2, then a file."
|
||||
color: Theme.sideMutedText
|
||||
font.pixelSize: Theme.fontXs
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Right Column: Wafer Map Overlap & Readouts ---
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: false
|
||||
Layout.preferredWidth: 280
|
||||
Layout.minimumWidth: 280
|
||||
Layout.maximumWidth: 280
|
||||
Layout.fillHeight: true
|
||||
spacing: 12
|
||||
|
||||
// ── DTW Readout Cards ─────────────────────────────────
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: dtwReadoutCol.implicitHeight + 24
|
||||
color: Theme.cardBackground
|
||||
border.color: Theme.cardBorder
|
||||
border.width: 1
|
||||
radius: Theme.radiusMd
|
||||
|
||||
ColumnLayout {
|
||||
id: dtwReadoutCol
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
spacing: 8
|
||||
|
||||
Label {
|
||||
text: "COMPARISON RESULTS"
|
||||
font.pixelSize: Theme.fontXs
|
||||
font.bold: true
|
||||
font.letterSpacing: 1.2
|
||||
color: Theme.sideMutedText
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// DTW Distance
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 52
|
||||
color: Theme.panelBackground
|
||||
border.color: Theme.cardBorder
|
||||
border.width: 1
|
||||
radius: Theme.radiusSm
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 8
|
||||
spacing: 2
|
||||
Label {
|
||||
text: "DTW Alignment Distance"
|
||||
font.pixelSize: Theme.fontXs
|
||||
color: Theme.bodyColor
|
||||
}
|
||||
Label {
|
||||
text: root.warpingDistance >= 0 ? root.warpingDistance.toFixed(2) : "--"
|
||||
font.pixelSize: Theme.fontLg
|
||||
font.bold: true
|
||||
color: root.warpingDistance >= 0
|
||||
? (root.warpingDistance < 50 ? "#6ee7b7" : root.warpingDistance < 100 ? "#fde047" : "#ef4444")
|
||||
: Theme.sideMutedText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Max Sensor Deviation
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 52
|
||||
color: Theme.panelBackground
|
||||
border.color: Theme.cardBorder
|
||||
border.width: 1
|
||||
radius: Theme.radiusSm
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 8
|
||||
spacing: 2
|
||||
Label {
|
||||
text: "Max Sensor Deviation"
|
||||
font.pixelSize: Theme.fontXs
|
||||
color: Theme.bodyColor
|
||||
}
|
||||
Label {
|
||||
text: root.maxSensorDeviation >= 0 ? root.maxSensorDeviation.toFixed(2) + "\u00B0C" : "--"
|
||||
font.pixelSize: Theme.fontLg
|
||||
font.bold: true
|
||||
color: root.maxSensorDeviation >= 0
|
||||
? (root.maxSensorDeviation < 1.0 ? "#6ee7b7" : root.maxSensorDeviation < 3.0 ? "#fde047" : "#ef4444")
|
||||
: Theme.sideMutedText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Match Quality Badge
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 28
|
||||
visible: root.warpingDistance >= 0
|
||||
radius: Theme.radiusSm
|
||||
color: root.warpingDistance < 50 ? Qt.rgba(0.43, 0.91, 0.72, 0.12)
|
||||
: root.warpingDistance < 100 ? Qt.rgba(0.99, 0.88, 0.28, 0.12)
|
||||
: Qt.rgba(0.94, 0.27, 0.27, 0.12)
|
||||
|
||||
Label {
|
||||
anchors.centerIn: parent
|
||||
text: root.warpingDistance < 50 ? "✓ Excellent Match"
|
||||
: root.warpingDistance < 100 ? "⚠ Moderate Deviation"
|
||||
: "✗ Poor Match"
|
||||
font.pixelSize: Theme.fontXs
|
||||
font.bold: true
|
||||
color: root.warpingDistance < 50 ? "#6ee7b7"
|
||||
: root.warpingDistance < 100 ? "#fde047"
|
||||
: "#ef4444"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Data rows
|
||||
TableView {
|
||||
id: dataTable
|
||||
// ── Wafer Overlap View ────────────────────────────────
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
model: deviceController.dataModel
|
||||
clip: true
|
||||
reuseItems: true
|
||||
color: Theme.cardBackground
|
||||
border.color: Theme.cardBorder
|
||||
border.width: 1
|
||||
radius: Theme.radiusMd
|
||||
|
||||
columnWidthProvider: function (col) {
|
||||
if (col === 0)
|
||||
return 52; // row index column
|
||||
const sensorCols = Math.max(1, dataTable.columns - 1);
|
||||
const available = dataTable.width - 52;
|
||||
return Math.max(52, Math.min(90, available / sensorCols));
|
||||
}
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
spacing: 8
|
||||
|
||||
rowHeightProvider: function () {
|
||||
return 24;
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
color: row % 2 === 0 ? Theme.panelBackground : Theme.cardBackground
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: display !== undefined ? display : ""
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: Theme.fontXs
|
||||
elide: Text.ElideRight
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Label {
|
||||
text: "SENSOR DEVIATION MAP"
|
||||
font.pixelSize: Theme.fontXs
|
||||
font.bold: true
|
||||
font.letterSpacing: 1.2
|
||||
color: Theme.sideMutedText
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
CheckBox {
|
||||
id: enableOverlapCheck
|
||||
checked: true
|
||||
implicitWidth: 18; implicitHeight: 18
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: "Toggle overlap heatmap"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ScrollBar.horizontal: ScrollBar {
|
||||
policy: ScrollBar.AsNeeded
|
||||
}
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
policy: ScrollBar.AsNeeded
|
||||
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
|
||||
spacing: 4
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: "🗺"
|
||||
font.pixelSize: 24
|
||||
}
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: "No sensor map available"
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: Theme.fontXs
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Blend slider
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 6
|
||||
Label {
|
||||
text: "Blend"
|
||||
font.pixelSize: Theme.fontXs
|
||||
color: Theme.bodyColor
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// Color legend
|
||||
Row {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
Row {
|
||||
spacing: 3
|
||||
Rectangle { width: 8; height: 8; radius: 4; color: Theme.sensorLow; anchors.verticalCenter: parent.verticalCenter }
|
||||
Label { text: "Cool"; 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: "Hot"; font.pixelSize: 9; color: Theme.sideMutedText }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,9 +188,16 @@ ColumnLayout {
|
||||
return (modelData.baseName + modelData.waferType + modelData.date).toLowerCase().indexOf(q) >= 0;
|
||||
}
|
||||
|
||||
// Active only in review mode; no highlight during live streaming
|
||||
readonly property bool isActive: streamController.mode === "review" && streamController.loadedFile !== "" && modelData.fileName === streamController.loadedFile
|
||||
|
||||
// Active highlight logic: on Data tab, highlight compareFileA/B; otherwise highlight loadedFile (for Map tab).
|
||||
readonly property bool isActive:{
|
||||
try {
|
||||
if (root.selectedTabIndex === 1 && dataTabLoader.item) {
|
||||
return modelData.fileName === dataTabLoader.item.compareFileA || modelData.fileName === dataTabLoader.item.compareFileB;
|
||||
}
|
||||
} catch (e) {}
|
||||
return streamController.mode === "review" && streamController.loadedFile !== "" && modelData.fileName === streamController.loadedFile;
|
||||
}
|
||||
|
||||
visible: matchesFilter
|
||||
height: matchesFilter ? implicitHeight : 0
|
||||
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Dialogs
|
||||
import ISC
|
||||
|
||||
// ===== Split Dialog (Threshold Segmentation) =====
|
||||
// Modal popup for segmenting temperature profile into Ramp/Soak/Cool phases.
|
||||
// Backend: splitter.segment_profile() returns list of DataSegment objects.
|
||||
|
||||
Popup {
|
||||
id: root
|
||||
modal: true
|
||||
dim: true
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
width: Math.min(parent.width - 100, 800)
|
||||
height: Math.min(parent.height - 100, 600)
|
||||
anchors.centerIn: Overlay.overlay
|
||||
|
||||
property real threshold: 40.0
|
||||
property bool segmenting: false
|
||||
property var segments: []
|
||||
property string currentFile: streamController.loadedFile || ""
|
||||
|
||||
background: Rectangle {
|
||||
radius: Theme.radiusMd
|
||||
color: Theme.cardBackground
|
||||
border.color: Theme.cardBorder
|
||||
border.width: 1
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 20
|
||||
spacing: 16
|
||||
|
||||
// ── Header ────────────────────────────────────────────────
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 10
|
||||
|
||||
Label {
|
||||
text: "SPLIT DATA"
|
||||
font.pixelSize: Theme.fontLg
|
||||
font.bold: true
|
||||
color: Theme.headingColor
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "\u2715"
|
||||
flat: true
|
||||
Layout.preferredWidth: 32
|
||||
Layout.preferredHeight: 32
|
||||
onClicked: root.close()
|
||||
|
||||
background: Rectangle {
|
||||
radius: Theme.radiusXs
|
||||
color: parent.hovered ? Theme.buttonNeutralHover : "transparent"
|
||||
}
|
||||
contentItem: Label {
|
||||
text: parent.text
|
||||
color: Theme.bodyColor
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Threshold Input ───────────────────────────────────────
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 16
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 4
|
||||
|
||||
Label {
|
||||
text: "THRESHOLD TEMPERATURE (°C)"
|
||||
color: Theme.sideMutedText
|
||||
font.pixelSize: Theme.fontSm
|
||||
font.bold: true
|
||||
font.letterSpacing: 1.0
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
SpinBox {
|
||||
id: thresholdSpin
|
||||
from: 0
|
||||
to: 200
|
||||
value: root.threshold
|
||||
stepSize: 1
|
||||
editable: true
|
||||
Layout.fillWidth: true
|
||||
background: Rectangle {
|
||||
color: Theme.fieldBackground
|
||||
border.color: Theme.cardBorder
|
||||
border.width: 1
|
||||
radius: Theme.radiusSm
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
text: "°C"
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: Theme.fontMd
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 4
|
||||
|
||||
Label {
|
||||
text: "SESSION CSV"
|
||||
color: Theme.sideMutedText
|
||||
font.pixelSize: Theme.fontSm
|
||||
font.bold: true
|
||||
font.letterSpacing: 1.0
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
text: root.currentFile !== ""
|
||||
? root.currentFile.split("/").pop()
|
||||
: "No file loaded"
|
||||
color: root.currentFile !== "" ? Theme.bodyColor : Theme.sideMutedText
|
||||
font.pixelSize: Theme.fontSm
|
||||
elide: Text.ElideMiddle
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Split Button ──────────────────────────────────────────
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 10
|
||||
|
||||
Button {
|
||||
id: splitBtn
|
||||
text: "SPLIT"
|
||||
Layout.fillWidth: true
|
||||
enabled: !root.segmenting && root.currentFile !== ""
|
||||
onClicked: {
|
||||
root.threshold = thresholdSpin.value;
|
||||
root.segmenting = true;
|
||||
streamController.splitData(root.currentFile, root.threshold);
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
radius: Theme.radiusSm
|
||||
color: splitBtn.enabled ? Theme.primaryAccent : Theme.buttonNeutralBackground
|
||||
border.color: splitBtn.enabled ? Theme.primaryAccent : "transparent"
|
||||
border.width: 1
|
||||
}
|
||||
contentItem: Row {
|
||||
spacing: 8
|
||||
anchors.centerIn: parent
|
||||
|
||||
Label {
|
||||
text: splitBtn.enabled ? (root.segmenting ? "Segmenting..." : "SPLIT") : "LOAD DATA FIRST"
|
||||
color: splitBtn.enabled ? Theme.tone100 : Theme.sideMutedText
|
||||
font.pixelSize: Theme.fontSm
|
||||
font.bold: true
|
||||
font.letterSpacing: 1.0
|
||||
}
|
||||
|
||||
BusyIndicator {
|
||||
running: root.segmenting
|
||||
visible: root.segmenting
|
||||
width: 16
|
||||
height: 16
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Segments Table ────────────────────────────────────────
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
radius: Theme.radiusSm
|
||||
color: Theme.panelBackground
|
||||
border.color: Theme.cardBorder
|
||||
border.width: 1
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 0
|
||||
spacing: 0
|
||||
|
||||
// Table header
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 36
|
||||
color: Theme.subtleSectionBackground
|
||||
border.color: Theme.cardBorder
|
||||
border.width: 1
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
spacing: 8
|
||||
|
||||
Label { text: "PHASE"; Layout.preferredWidth: 100; color: Theme.sideMutedText; font.bold: true; font.pixelSize: Theme.fontSm }
|
||||
Label { text: "START (s)"; Layout.preferredWidth: 120; color: Theme.sideMutedText; font.bold: true; font.pixelSize: Theme.fontSm }
|
||||
Label { text: "END (s)"; Layout.preferredWidth: 120; color: Theme.sideMutedText; font.bold: true; font.pixelSize: Theme.fontSm }
|
||||
Label { text: "AVG TEMP (°C)"; Layout.fillWidth: true; color: Theme.sideMutedText; font.bold: true; font.pixelSize: Theme.fontSm }
|
||||
}
|
||||
}
|
||||
|
||||
// Table body (scrollable)
|
||||
ScrollView {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
clip: true
|
||||
|
||||
ColumnLayout {
|
||||
width: parent.width - 32
|
||||
spacing: 0
|
||||
|
||||
Repeater {
|
||||
model: root.segments.length > 0 ? root.segments : []
|
||||
delegate: Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 40
|
||||
color: index % 2 === 0 ? Theme.cardBackground : "transparent"
|
||||
border.color: Theme.cardBorder
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
spacing: 8
|
||||
|
||||
Label {
|
||||
text: modelData.label || ""
|
||||
Layout.preferredWidth: 100
|
||||
color: modelData.label === "Soak" ? Theme.statusSuccessColor :
|
||||
modelData.label === "Ramp" ? Theme.statusWarningColor : Theme.bodyColor
|
||||
font.bold: true
|
||||
font.pixelSize: Theme.fontSm
|
||||
}
|
||||
|
||||
Label {
|
||||
text: (modelData.start_frame || 0).toString()
|
||||
Layout.preferredWidth: 120
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: Theme.fontSm
|
||||
}
|
||||
|
||||
Label {
|
||||
text: (modelData.end_frame || 0).toString()
|
||||
Layout.preferredWidth: 120
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: Theme.fontSm
|
||||
}
|
||||
|
||||
Label {
|
||||
text: (modelData.avg_temp || 0).toFixed(2) + " °C"
|
||||
Layout.fillWidth: true
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: Theme.fontSm
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Empty state
|
||||
Label {
|
||||
visible: root.segments.length === 0
|
||||
text: root.currentFile !== "" ? "Click SPLIT to segment data" : "Load a CSV file first"
|
||||
color: Theme.sideMutedText
|
||||
font.pixelSize: Theme.fontSm
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 100
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Summary line ──────────────────────────────────────────
|
||||
Label {
|
||||
visible: root.segments.length > 0
|
||||
text: root.segments.length + " segment" + (root.segments.length === 1 ? "" : "s")
|
||||
+ " found above " + root.threshold.toFixed(0) + "°C threshold."
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: Theme.fontSm
|
||||
}
|
||||
}
|
||||
|
||||
// ── Backend Connection ────────────────────────────────────────
|
||||
Connections {
|
||||
target: streamController
|
||||
function onSplitResult(result) {
|
||||
root.segmenting = false;
|
||||
if (result && result.success) {
|
||||
root.segments = result.segments || [];
|
||||
console.log("[SplitDialog] Segmented into", root.segments.length, "phases");
|
||||
} else {
|
||||
root.segments = [];
|
||||
console.log("[SplitDialog] Split failed:", result.error || "unknown");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,3 +6,4 @@ TransportBar 1.0 TransportBar.qml
|
||||
WaferMapView 1.0 WaferMapView.qml
|
||||
ReplaceSensorDialog 1.0 ReplaceSensorDialog.qml
|
||||
RailActionButton 1.0 RailActionButton.qml
|
||||
SplitDialog 1.0 SplitDialog.qml
|
||||
|
||||
Reference in New Issue
Block a user