From c3f91fbd3b3c064e2ded78cf307d23a8d6deacc7 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 05:24:20 +0700 Subject: [PATCH 01/21] fix(injector): ALWAYS send 4I+4V table snapshot atomically before each SV START --- apps/arstack_studio/qml/WorkflowBar.qml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/arstack_studio/qml/WorkflowBar.qml b/apps/arstack_studio/qml/WorkflowBar.qml index e49593114..64f990e71 100644 --- a/apps/arstack_studio/qml/WorkflowBar.qml +++ b/apps/arstack_studio/qml/WorkflowBar.qml @@ -281,9 +281,12 @@ SurfacePanel { controller.showMessage(ribbon.startReason(), false) return } - const accepted = controller.pendingApply - ? smartSession.requestStartWithScenario(controller.startScenarioSnapshot()) - : smartSession.requestStart() + // Every START uploads the current visible 4I+4V model. The board may + // still hold an older scenario after STOP, reconnect, PTP role change, + // or another session. Never take a bare START shortcut from a GUI + // that displays different phasor angles. + const accepted = smartSession.requestStartWithScenario( + controller.startScenarioSnapshot()) if (!accepted) { controller.pendingApply = true controller.showMessage("Start rejected. Check device status.", true) From 8843d9c8ca364a048b4e70a9c0cddc5779006eca Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 05:25:34 +0700 Subject: [PATCH 02/21] fix(studio): default verified PTP SOURCE, live-setpoint failure handling, stacked full-height voltage/current --- apps/arstack_studio/qml/Main.qml | 54 ++++++++++++++++++++++++++------ 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/apps/arstack_studio/qml/Main.qml b/apps/arstack_studio/qml/Main.qml index 66a0daf89..b5a5b87f1 100644 --- a/apps/arstack_studio/qml/Main.qml +++ b/apps/arstack_studio/qml/Main.qml @@ -119,6 +119,8 @@ ApplicationWindow { // an explicit PTP ON can be restored after a firmware reflash/reconnect // without introducing an uncontrolled power-on publisher. property bool ptpDesiredForSession: false + property bool ptpDefaultStartPending: true + property bool ptpExplicitOff: false property string ptpDesiredRole: "SOURCE" property bool ptpRestoreAfterFirmware: false property int ptpRestoreChecks: 0 @@ -198,6 +200,8 @@ ApplicationWindow { function onDeviceVerifiedChanged() { root.pendingApply = true root.offlinePreviewRunning = false + if (device.deviceVerified && !root.ptpExplicitOff) + root.ptpDefaultStartPending = true } } @@ -215,6 +219,28 @@ ApplicationWindow { } } + // One automatic SOURCE start on verified connection. An explicit OFF + // cancels this for the remainder of the Studio session; never send PTP + // commands without a verified board or while firmware is updating. + Timer { + interval: 400 + repeat: true + running: root.ptpDefaultStartPending && !root.ptpExplicitOff && device.deviceVerified + onTriggered: { + if (device.ptpRunning) { + root.ptpDefaultStartPending = false + return + } + const session = workflowBar.session + if (!session || !session.startReady || session.updatingFirmware || + device.ptpStarting || !device.ptpAvailable) + return + root.ptpDefaultStartPending = false + if (!root.startPtpRole("SOURCE")) + root.showMessage("PTP auto-start was not accepted. Use PTP On to retry.", true) + } + } + Timer { interval: 300 repeat: true @@ -317,6 +343,8 @@ ApplicationWindow { const accepted = profilePanel.startPtpRole(role) if (accepted) { root.ptpDesiredForSession = true + root.ptpExplicitOff = false + root.ptpDefaultStartPending = false root.ptpDesiredRole = role } return accepted @@ -324,6 +352,8 @@ ApplicationWindow { function requestStopPtp() { root.ptpDesiredForSession = false + root.ptpDefaultStartPending = false + root.ptpExplicitOff = true root.ptpRestoreAfterFirmware = false return workflowBar.session && workflowBar.session.requestStopPtp() } @@ -468,8 +498,11 @@ ApplicationWindow { selectSignal(group, row) markScenarioDirty() if (workflowBar.session && workflowBar.session.liveControlReady && !pendingApply) { - if (phaseLink && row < 3) sendLinkedGroup(group) - else sendSignal(group, row) + const sent = phaseLink && row < 3 ? sendLinkedGroup(group) : sendSignal(group, row) + if (!sent) { + pendingApply = true + showMessage("Setpoint not queued. STOP and START to apply all displayed 4I+4V values.", true) + } } return true } @@ -932,8 +965,8 @@ ApplicationWindow { ColumnLayout { anchors.fill: parent - anchors.margins: root.compactLayout ? 11 : 13 - spacing: root.compactLayout ? 6 : 8 + anchors.margins: root.compactLayout ? 8 : 9 + spacing: root.compactLayout ? 4 : 5 RowLayout { Layout.fillWidth: true @@ -1049,11 +1082,15 @@ ApplicationWindow { } } - RowLayout { + // Stack voltage above current. Both tables share the + // available height instead of leaving an empty lower + // half; the inner rows stretch with the desktop. + ColumnLayout { Layout.fillWidth: true - Layout.preferredHeight: root.compactLayout ? 214 : 224 - Layout.maximumHeight: root.compactLayout ? 214 : 224 - spacing: 9 + Layout.fillHeight: true + Layout.minimumHeight: 408 + Layout.preferredHeight: 422 + spacing: 7 SignalMatrix { id: voltageMatrix enabled: !workflowBar.startQueued @@ -1136,7 +1173,6 @@ ApplicationWindow { compact: root.compactLayout } } - Item { Layout.fillHeight: true } } } From a2d938cb18babd2cfe7b9803bcb0455b636d70e3 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 05:26:23 +0700 Subject: [PATCH 03/21] fix(studio UX): improve full-height tables, ribbon brand/button centering and stable checkbox hover --- apps/arstack_studio/qml/SignalMatrix.qml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/arstack_studio/qml/SignalMatrix.qml b/apps/arstack_studio/qml/SignalMatrix.qml index 1c84e2b43..7eb1f375f 100644 --- a/apps/arstack_studio/qml/SignalMatrix.qml +++ b/apps/arstack_studio/qml/SignalMatrix.qml @@ -196,7 +196,9 @@ Rectangle { } Layout.fillWidth: true - Layout.preferredHeight: matrix.compact ? 40 : 42 + Layout.fillHeight: true + Layout.minimumHeight: 36 + Layout.preferredHeight: matrix.compact ? 39 : 40 color: signalRow.selected ? "#101c27" : (magnitudeField.activeFocus || phaseField.activeFocus) ? "#0f1922" : rowHover.hovered ? "#0d161e" : "transparent" @@ -407,6 +409,5 @@ Rectangle { } } - Item { Layout.fillHeight: true } } } From 284e11497b0aa48ab29d3ee0801dbc077e558b0f Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 05:26:28 +0700 Subject: [PATCH 04/21] fix(studio UX): improve full-height tables, ribbon brand/button centering and stable checkbox hover --- apps/arstack_studio/qml/ModernRibbon.qml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/arstack_studio/qml/ModernRibbon.qml b/apps/arstack_studio/qml/ModernRibbon.qml index aa995b10d..8c218bf92 100644 --- a/apps/arstack_studio/qml/ModernRibbon.qml +++ b/apps/arstack_studio/qml/ModernRibbon.qml @@ -47,24 +47,24 @@ Rectangle { spacing: 6 RowLayout { - Layout.preferredWidth: bar.compact ? 105 : 124 + Layout.preferredWidth: bar.compact ? 150 : 176 Layout.alignment: Qt.AlignVCenter spacing: 8 Rectangle { - width: 28 - height: 28 - radius: 7 + width: 40 + height: 40 + radius: 9 color: "#0d151d" border.width: 1 border.color: bar.theme.lineSoft Image { anchors.centerIn: parent - width: 15 - height: 15 + width: 28 + height: 28 source: Qt.resolvedUrl("../assets/material/smv_injector.svg") - sourceSize.width: 30 - sourceSize.height: 30 + sourceSize.width: 56 + sourceSize.height: 56 } } @@ -73,7 +73,7 @@ Rectangle { text: "SMV Injector" color: bar.theme.text font.family: bar.uiFont - font.pixelSize: 11 + font.pixelSize: bar.compact ? 14 : 15 font.weight: Font.DemiBold elide: Text.ElideRight verticalAlignment: Text.AlignVCenter From bc0be48cccdbe8937f76246165dcd1d395aa1fa3 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 05:26:33 +0700 Subject: [PATCH 05/21] fix(studio UX): improve full-height tables, ribbon brand/button centering and stable checkbox hover --- apps/arstack_studio/qml/RibbonAction.qml | 49 +++++++++++++----------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/apps/arstack_studio/qml/RibbonAction.qml b/apps/arstack_studio/qml/RibbonAction.qml index a882bb495..c55a83ea0 100644 --- a/apps/arstack_studio/qml/RibbonAction.qml +++ b/apps/arstack_studio/qml/RibbonAction.qml @@ -38,28 +38,33 @@ Button { : tone === "warning" ? "#f5daaa" : tone === "danger" ? "#ffe3e6" : tone === "accent" || checked ? "#e1f1ff" : theme.textSoft - contentItem: ColumnLayout { - spacing: 3 - Image { - visible: control.iconSource.toString().length > 0 - source: control.hovered && control.enabled ? control.hoverPath : control.iconSource - Layout.alignment: Qt.AlignHCenter - Layout.preferredWidth: 24 - Layout.preferredHeight: 24 - sourceSize.width: 48 - sourceSize.height: 48 - opacity: control.enabled ? 1 : 0.3 - } - Text { - id: caption - visible: !control.iconOnly - text: control.text - color: control.textColor - font: control.font - Layout.alignment: Qt.AlignHCenter - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight + // The content box owns full button geometry; its visual group is + // centered in both axes (Windows / 100–175% DPI). + contentItem: Item { + Column { + anchors.centerIn: parent + spacing: 4 + Image { + visible: control.iconSource.toString().length > 0 + source: control.hovered && control.enabled ? control.hoverPath : control.iconSource + anchors.horizontalCenter: parent.horizontalCenter + width: 24 + height: 24 + sourceSize.width: 48 + sourceSize.height: 48 + opacity: control.enabled ? 1 : 0.3 + } + Text { + id: caption + visible: !control.iconOnly + text: control.text + color: control.textColor + font: control.font + anchors.horizontalCenter: parent.horizontalCenter + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } } } background: Rectangle { From e2fe937e3bd29f1e57393089ca16e3127b07ee6c Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 05:26:38 +0700 Subject: [PATCH 06/21] fix(studio UX): improve full-height tables, ribbon brand/button centering and stable checkbox hover --- apps/arstack_studio/qml/StudioDarkCheckBox.qml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/arstack_studio/qml/StudioDarkCheckBox.qml b/apps/arstack_studio/qml/StudioDarkCheckBox.qml index eedfce586..5d0730d12 100644 --- a/apps/arstack_studio/qml/StudioDarkCheckBox.qml +++ b/apps/arstack_studio/qml/StudioDarkCheckBox.qml @@ -8,7 +8,11 @@ CheckBox { property var theme property string uiFont: "Inter" implicitHeight: 30 + hoverEnabled: true spacing: 9 + padding: 0 + // Disable native Windows hover glyph: one consistent 18px indicator. + background: Item {} font.family: uiFont font.pixelSize: 11 indicator: Rectangle { @@ -20,7 +24,8 @@ CheckBox { color: control.checked ? control.theme.accent : control.theme.surface2 border.width: 1 border.color: control.checked ? control.theme.accent - : control.activeFocus ? control.theme.textSoft : control.theme.line + : control.hovered || control.activeFocus + ? control.theme.textSoft : control.theme.line Text { anchors.centerIn: parent visible: control.checked From f50c376beeb9ed258fe7b7a6d7ea9a0adc787bf0 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 05:27:42 +0700 Subject: [PATCH 07/21] fix(studio): readable light-hover/dark-caption and centered advanced controls --- apps/arstack_studio/qml/CalmButton.qml | 54 +++++++++++++++----------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/apps/arstack_studio/qml/CalmButton.qml b/apps/arstack_studio/qml/CalmButton.qml index ddddca61c..98f74608f 100644 --- a/apps/arstack_studio/qml/CalmButton.qml +++ b/apps/arstack_studio/qml/CalmButton.qml @@ -22,39 +22,47 @@ Button { Behavior on scale { NumberAnimation { duration: 70; easing.type: Easing.OutCubic } } readonly property color activeFill: !enabled ? "#10161d" : - tone === "success" ? (hovered ? "#1e5c43" : "#194d38") : - tone === "danger" ? (hovered ? "#523039" : theme.redSoft) : - tone === "accent" ? (hovered ? "#1d3c5e" : theme.accentSoft) : - (down ? "#1a2530" : hovered ? theme.raisedHover : "#141c25") + hovered ? "#e9f2fd" : + tone === "success" ? "#194d38" : + tone === "danger" ? theme.redSoft : + tone === "accent" ? theme.accentSoft : + down ? "#1a2530" : "#141c25" readonly property color activeEdge: !enabled ? theme.lineSoft : + hovered ? "#a6c9f2" : tone === "success" ? "#347a59" : tone === "danger" ? "#86434b" : tone === "accent" ? "#315f8d" : (activeFocus ? theme.accent : hovered ? "#3a4a5b" : theme.line) readonly property color activeText: !enabled ? theme.muted2 : + hovered ? "#142334" : tone === "success" ? "#d6f4e6" : tone === "danger" ? "#ffdce0" : tone === "accent" ? "#dcebff" : theme.textSoft - contentItem: Row { - spacing: control.iconSource.toString().length > 0 ? 7 : 0 - anchors.centerIn: parent - Image { - visible: control.iconSource.toString().length > 0 - width: control.iconSize - height: control.iconSize - anchors.verticalCenter: parent.verticalCenter - source: control.iconSource - sourceSize.width: control.iconSize * 2 - sourceSize.height: control.iconSize * 2 - opacity: control.enabled ? 0.94 : 0.30 - } - Text { - text: control.text - color: control.activeText - font: control.font - anchors.verticalCenter: parent.verticalCenter - Behavior on color { ColorAnimation { duration: 90 } } + contentItem: Item { + Row { + spacing: control.iconSource.toString().length > 0 ? 7 : 0 + anchors.centerIn: parent + Image { + visible: control.iconSource.toString().length > 0 + width: control.iconSize + height: control.iconSize + anchors.verticalCenter: parent.verticalCenter + source: control.hovered && control.enabled + ? control.iconSource.toString().replace("/assets/material/", "/assets/material_dark/") + : control.iconSource + sourceSize.width: control.iconSize * 2 + sourceSize.height: control.iconSize * 2 + opacity: control.enabled ? 0.94 : 0.30 + } + Text { + text: control.text + color: control.activeText + font: control.font + anchors.verticalCenter: parent.verticalCenter + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } } } From 72b5592a8b0f348ed39e28de3d3b9d443dbab626 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 05:27:47 +0700 Subject: [PATCH 08/21] fix(studio): readable light-hover/dark-caption and centered advanced controls --- apps/arstack_studio/qml/ConfigurationHub.qml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/apps/arstack_studio/qml/ConfigurationHub.qml b/apps/arstack_studio/qml/ConfigurationHub.qml index fecaa51a3..708e23d28 100644 --- a/apps/arstack_studio/qml/ConfigurationHub.qml +++ b/apps/arstack_studio/qml/ConfigurationHub.qml @@ -34,16 +34,18 @@ Item { font.weight: checked ? Font.DemiBold : Font.Medium contentItem: Label { text: parent.text - color: parent.checked ? hub.theme.text : hub.theme.muted + color: parent.hovered && !parent.checked ? "#142334" + : parent.checked ? hub.theme.text : hub.theme.muted font: parent.font horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } background: Rectangle { - color: parent.checked ? hub.theme.raised : "transparent" + color: parent.hovered && !parent.checked ? "#e9f2fd" + : parent.checked ? hub.theme.raised : "transparent" radius: 6 - border.width: parent.checked ? 1 : 0 - border.color: hub.theme.line + border.width: parent.checked || parent.hovered ? 1 : 0 + border.color: parent.hovered && !parent.checked ? "#a6c9f2" : hub.theme.line Rectangle { visible: parent.parent.checked anchors.left: parent.left @@ -59,7 +61,7 @@ Item { ColumnLayout { anchors.fill: parent - spacing: 16 + spacing: 11 RowLayout { Layout.fillWidth: true From 7b96dad9bd1933e2095a2bc05d4320fbb637022c Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 05:27:52 +0700 Subject: [PATCH 09/21] fix(studio): readable light-hover/dark-caption and centered advanced controls --- apps/arstack_studio/qml/StudioDarkComboBox.qml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/arstack_studio/qml/StudioDarkComboBox.qml b/apps/arstack_studio/qml/StudioDarkComboBox.qml index 60f7afb00..6ce33ad21 100644 --- a/apps/arstack_studio/qml/StudioDarkComboBox.qml +++ b/apps/arstack_studio/qml/StudioDarkComboBox.qml @@ -21,7 +21,7 @@ ComboBox { background: Rectangle { radius: 7 - color: combo.enabled ? (combo.hovered ? combo.theme.raisedHover : combo.theme.surface2) + color: combo.enabled ? (combo.hovered ? "#e9f2fd" : combo.theme.surface2) : combo.theme.surface2 border.width: 1 border.color: combo.activeFocus ? combo.theme.accent : combo.theme.line @@ -29,7 +29,7 @@ ComboBox { contentItem: Text { text: combo.displayText - color: combo.enabled ? combo.theme.text : combo.theme.muted + color: combo.enabled ? (combo.hovered ? "#142334" : combo.theme.text) : combo.theme.muted font: combo.font verticalAlignment: Text.AlignVCenter leftPadding: 12 @@ -39,7 +39,7 @@ ComboBox { indicator: Text { text: "\u25be" - color: combo.enabled ? combo.theme.textSoft : combo.theme.muted + color: combo.enabled ? (combo.hovered ? "#142334" : combo.theme.textSoft) : combo.theme.muted font.pixelSize: 14 anchors.right: parent.right anchors.rightMargin: 11 @@ -54,7 +54,7 @@ ComboBox { highlighted: combo.highlightedIndex === index contentItem: Text { text: combo.textAt(choice.index) - color: combo.theme.text + color: choice.highlighted ? "#142334" : combo.theme.text font: combo.font verticalAlignment: Text.AlignVCenter elide: Text.ElideRight @@ -63,7 +63,7 @@ ComboBox { } background: Rectangle { radius: 5 - color: parent.highlighted ? combo.theme.accentSoft : combo.theme.raised + color: parent.highlighted ? "#e9f2fd" : combo.theme.raised } } From 25c7a1a974d7cc5f893178506241b66cbf1596b1 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 05:28:43 +0700 Subject: [PATCH 10/21] fix(studio): unify Advanced and Main PTP user intent, with explicit OFF canceling auto restart --- apps/arstack_studio/qml/PtpExpertPanel.qml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/arstack_studio/qml/PtpExpertPanel.qml b/apps/arstack_studio/qml/PtpExpertPanel.qml index 14fb81763..8060e4176 100644 --- a/apps/arstack_studio/qml/PtpExpertPanel.qml +++ b/apps/arstack_studio/qml/PtpExpertPanel.qml @@ -49,6 +49,13 @@ SurfacePanel { || panel.device.ptpRunning || panel.startPending || panel.session.updatingFirmware) return false const accepted = panel.session.requestStartPtpConfigured(role, panel.ptpProfile()) + if (accepted && panel.controller) { + // A start from Advanced has exactly the same owner as Main. + panel.controller.ptpDesiredForSession = true + panel.controller.ptpExplicitOff = false + panel.controller.ptpDefaultStartPending = false + panel.controller.ptpDesiredRole = role + } if (!accepted && panel.controller) panel.controller.showMessage("PTP start blocked. Check timing diagnostics.", true) return Boolean(accepted) @@ -134,7 +141,7 @@ SurfacePanel { const wantsRunning = checked checked = Qt.binding(function() { return Boolean(panel.device.ptpRunning) }) if (wantsRunning) panel.startSelectedMode() - else if (panel.session && !panel.session.requestStopPtp() && panel.controller) + else if (panel.controller && !panel.controller.requestStopPtp()) panel.controller.showMessage("PTP stop was not accepted. Copy diagnostics.", true) } } @@ -287,7 +294,7 @@ SurfacePanel { text: panel.device.ptpStatus.indexOf("PTP STOP unverified") === 0 ? "Retry Stop PTP" : "Stop PTP" enabled: panel.session && (panel.device.ptpRunning || panel.startPending) && !panel.session.updatingFirmware onClicked: { - panel.session.requestStopPtp() + if (panel.controller) panel.controller.requestStopPtp() } } CalmButton { From c72cf68b53f18884ad6c580c781a291a20360490 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 05:29:50 +0700 Subject: [PATCH 11/21] =?UTF-8?q?test(studio):=20enforce=20exact=20?= =?UTF-8?q?=E2=88=9230/=E2=88=92150/+90=20degree=20injected=20SET=20wire?= =?UTF-8?q?=20angles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/DeterministicSessionHarness.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/apps/arstack_studio/src/DeterministicSessionHarness.cpp b/apps/arstack_studio/src/DeterministicSessionHarness.cpp index bd5fe2233..299e770a9 100644 --- a/apps/arstack_studio/src/DeterministicSessionHarness.cpp +++ b/apps/arstack_studio/src/DeterministicSessionHarness.cpp @@ -644,6 +644,14 @@ class DeterministicSessionHarness final { QStringLiteral("Ib"), 1.0, -120.0, 0U, 1000.0, 100.0, 50.0); const QString ic = DeviceController::formatSignalCommand( QStringLiteral("Ic"), 1.0, 120.0, 0U, 1000.0, 100.0, 50.0); + // The operator's non-zero phasor settings must survive milli-degree + // wire encoding; never substitute the balanced 0/-120/+120 preset. + const QString iaShifted = DeviceController::formatSignalCommand( + QStringLiteral("Ia"), 1.0, -30.0, 0U, 1000.0, 100.0, 50.0); + const QString ibShifted = DeviceController::formatSignalCommand( + QStringLiteral("Ib"), 1.0, -150.0, 0U, 1000.0, 100.0, 50.0); + const QString icShifted = DeviceController::formatSignalCommand( + QStringLiteral("Ic"), 1.0, 90.0, 0U, 1000.0, 100.0, 50.0); const QString badId = DeviceController::formatSignalCommand( QStringLiteral("IX"), 1.0, 0.0, 0U, 1000.0, 100.0, 50.0); const QString overflow = DeviceController::formatSignalCommand( @@ -653,6 +661,9 @@ class DeterministicSessionHarness final { return ia == QStringLiteral("SET IA 1000 0 0") && ib == QStringLiteral("SET IB 1000 -120000 0") && ic == QStringLiteral("SET IC 1000 120000 0") && + iaShifted == QStringLiteral("SET IA 1000 -30000 0") && + ibShifted == QStringLiteral("SET IB 1000 -150000 0") && + icShifted == QStringLiteral("SET IC 1000 90000 0") && badId.isEmpty() && overflow.isEmpty() && dcNegative == QStringLiteral("SET IA -1000 0 0"); } From cd85669afcffd1dc8da34a9703748e0ad83fecf5 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 05:30:46 +0700 Subject: [PATCH 12/21] test(studio): lock snapshot-on-every-START, default PTP, 4I/4V stacked UX and high-contrast hover --- .../tests/check_operator_workspace.py | 46 ++++++++++++++++++- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/apps/arstack_studio/tests/check_operator_workspace.py b/apps/arstack_studio/tests/check_operator_workspace.py index ecca26e80..94154f5d9 100644 --- a/apps/arstack_studio/tests/check_operator_workspace.py +++ b/apps/arstack_studio/tests/check_operator_workspace.py @@ -115,8 +115,12 @@ def main() -> None: and 'enabled: !workflowBar.startQueued; onActivated: root.balanced()' in main_qml and 'enabled: !workflowBar.startQueued; onActivated: root.zeroAll()' in main_qml, "scenario editors and shortcuts must be frozen during a pending START snapshot") + start_path = workflow.split("function requestStart() {", 1)[1].split( + "function requestStop()", 1)[0] require("controller.pendingApply = true" in workflow - and "requestStartWithScenario(controller.startScenarioSnapshot())" in workflow + and "smartSession.requestStartWithScenario(" in start_path + and "controller.startScenarioSnapshot()" in start_path + and "smartSession.requestStart()" not in start_path and "controller.applyAllSignals()" not in workflow and "startWatchdog" in workflow and "onRunningChanged()" in workflow, "reconnect must not auto-transmit, but one-click Start must stage a bounded scenario") @@ -145,7 +149,8 @@ def main() -> None: and 'toolTipText: "Advanced"' not in ribbon and 'ToolTip.text: "smpSynch mode"' not in timing_ui, "captioned operator controls must not duplicate labels as tooltips") - require('requestStartWithScenario(controller.startScenarioSnapshot())' in workflow + require("smartSession.requestStartWithScenario(" in start_path + and "controller.startScenarioSnapshot()" in start_path and "Apply to board" not in main_qml, "Edit-to-Start must not regress to a manual Apply-to-board action") require("assets/material/smv_injector.svg" in cmake_icons @@ -156,6 +161,43 @@ def main() -> None: and "Equal magnitudes A/B/C" in matrix and "Balance angles · 120°" in matrix, "context actions must stay compact, descriptive and grouped") + # Freeze operator control geometry, hover contrast and table layout. + ribbon_action = source("RibbonAction.qml") + calm = source("CalmButton.qml") + check_box = source("StudioDarkCheckBox.qml") + combo = source("StudioDarkComboBox.qml") + require("Layout.minimumHeight: 408" in main_qml + and "ColumnLayout {\n Layout.fillWidth: true\n Layout.fillHeight: true" in main_qml + and "Layout.fillHeight: true" in matrix + and "Item { Layout.fillHeight: true }" not in matrix, + "4I and 4V must stack and expand, not leave unused lower area") + require("width: 40" in ribbon and "height: 40" in ribbon + and 'font.pixelSize: bar.compact ? 14 : 15' in ribbon + and "anchors.centerIn: parent" in ribbon_action, + "Studio identity and ribbon content must be legible and centered") + require('hovered ? "#e9f2fd"' in calm + and 'hovered ? "#142334"' in calm + and 'hovered && !parent.checked ? "#142334"' in config + and 'choice.highlighted ? "#142334"' in combo, + "Advanced hover must use light background with dark text") + require("background: Item {}" in check_box + and "implicitWidth: 18" in check_box + and "implicitHeight: 18" in check_box, + "checkbox must not draw a second native hover glyph") + require("property bool ptpDefaultStartPending: true" in main_qml + and 'root.startPtpRole("SOURCE")' in main_qml + and "root.ptpExplicitOff = true" in main_qml + and "root.ptpDefaultStartPending = false" in main_qml + and "panel.controller.requestStopPtp()" in expert, + "PTP defaults ON after verified connection while explicit OFF cancels restarts") + require("property bool phasorDockVisible: true" in main_qml + and "property bool waveformDockVisible: true" in main_qml + and "tray.session.requestSmpSynch(" in tray, + "phasor and waveform must default visible and keep AUTO sync selection") + require("const sent = phaseLink && row < 3" in main_qml + and "if (!sent) {" in main_qml + and "pendingApply = true" in main_qml, + "live SET rejection must never silently claim displayed setpoints were sent") assert "implicitHeight: 65" in source("RibbonAction.qml") assert 'hovered ? "#142334"' in source("RibbonAction.qml") require('title: ""' in menu and 'property string contextLabel: ""' in menu From 5d7e3dace96d36dc78e8e3f5db5bbc1b9cd7cd27 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 05:32:04 +0700 Subject: [PATCH 13/21] docs: lock good SV/PTP AUTO-LOCAL 120s evidence and operator UI regression baseline --- ...SMV_PTP_OPERATOR_BASELINE_NO_REGRESSION.md | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 docs/SMV_PTP_OPERATOR_BASELINE_NO_REGRESSION.md diff --git a/docs/SMV_PTP_OPERATOR_BASELINE_NO_REGRESSION.md b/docs/SMV_PTP_OPERATOR_BASELINE_NO_REGRESSION.md new file mode 100644 index 000000000..19875fab6 --- /dev/null +++ b/docs/SMV_PTP_OPERATOR_BASELINE_NO_REGRESSION.md @@ -0,0 +1,46 @@ +# ARStack Studio — SMV/PTP non-regression baseline + +Physical checkpoint (2026-10-17, 120s ASIX/Npcap): 480,294 canonical VLAN +priority-tagged SV frames, APPID 0x4000, svID ARSTACK_SV01, +4000 nominal FPS with 0 smpCnt gaps, duplicates or reordering, 120 +counter wraps; 8 quality words/channel GOOD and AUTO smpSynch=1 LOCAL in +all captured SV frames. Diagnostic mirror OFF. Domain-0 two-step PTP +had 120 Announce, 120 Sync, 120 matched Follow_Up; SOURCE local clock +class 248. Host-arrival correlation placed smpCnt=0 near integer PTP +seconds with median +49 microseconds and stable relation for 120 epochs. + +That +49us is *not a measurement of hardware sample phase*; Npcap, +Windows and USB Ethernet add variable latency. A real PTP subscription, +peer-delay exchange, independent PPS and IED acceptance have not been +certified. Never replace measured AUTO=1 with an unconditional forced +flag; never automatically advertise globally traceable smpSynch=2. + +Mandatory anti-regression gates for every Studio release: + +1. Preserve tested M3C.6 SOURCE local AUTO qualification, phase tracking, + timestamp evidence, failure revocation and canonical 4000-FPS stream. + GUI-only fixes must not change the firmware protocol engine or PTP. +2. Every operator START must atomically enqueue the *current displayed* + FREQ, eight channel SET/ENABLE commands, CT shape and then START. + Do not use a bare START on the basis of a cached pendingApply flag. + Fail on queue exhaustion; never report a rejected live edit as applied. +3. End-to-end angle encoding at 1 A, scale 1000: + Ia -30 degrees => SET IA 1000 -30000 0 + Ib -150 degrees => SET IB 1000 -150000 0 + Ic +90 degrees => SET IC 1000 90000 0. + Do not silently replace with default 0/-120/+120 degree pattern. +4. Default PTP SOURCE ON only once a verified ESP32-P4 is ready; + smpSynch AUTO; user explicit OFF cancels any automatic restart. + Phasor and Wave docks visible by default. +5. Voltage above Current, with both tables occupying the editor height. + Shared buttons have centered content, light hover with dark captions, + and a single unchanged-size checkbox indicator when hovered. +6. Windows Native Preview must package firmware from the **same commit** + with phase servo ON, canonical hardware MAC TX timestamp evidence ON, + and diagnostic mirror OFF. Reject outdated firmware bundles. +7. CI: PTP Core GCC/Clang/MSVC, ESP32-P4 SV+PTP SOURCE/RECEIVER, + Studio Qt/operator workspace contracts, Windows Lifecycle and Native + Preview must all pass before any public merge/release candidate. + +This baseline documents the best known functioning stream, not third-party +subscriber certification or independent microsecond hardware accuracy. From 7f4bd26c4d9d19f509146c15f5dbaf33e6aca8b5 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 05:36:26 +0700 Subject: [PATCH 14/21] fix(studio): never hide unsent phasor/frequency/quality changes while injecting --- apps/arstack_studio/qml/Main.qml | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/apps/arstack_studio/qml/Main.qml b/apps/arstack_studio/qml/Main.qml index b5a5b87f1..f29c7143c 100644 --- a/apps/arstack_studio/qml/Main.qml +++ b/apps/arstack_studio/qml/Main.qml @@ -54,7 +54,8 @@ ApplicationWindow { saveScenarioTimer.restart() // While STOPPED, a new edit belongs to the next supervised Start batch. // Only a genuinely RUNNING board receives incremental live SETs. - if (!workflowBar.injectionRunning) + if (!workflowBar.injectionRunning || + !workflowBar.session || !workflowBar.session.liveControlReady) pendingApply = true } function restoreLocalScenario() { @@ -536,8 +537,9 @@ ApplicationWindow { frequencyField.invalidInput = false refreshPreview() markScenarioDirty() - if (workflowBar.session && workflowBar.session.liveControlReady && !pendingApply) - workflowBar.session.requestSetFrequency(value) + if (workflowBar.session && workflowBar.session.liveControlReady && !pendingApply && + !workflowBar.session.requestSetFrequency(value)) + pendingApply = true return true } @@ -643,8 +645,9 @@ ApplicationWindow { selectSignal(activeGroup, activeRow) refreshPreview() markScenarioDirty() - if (workflowBar.session && workflowBar.session.liveControlReady && !pendingApply) - workflowBar.session.requestZero() + if (workflowBar.session && workflowBar.session.liveControlReady && !pendingApply && + !workflowBar.session.requestZero()) + pendingApply = true } function setActiveQuality(value) { @@ -652,8 +655,9 @@ ApplicationWindow { groupModel(activeGroup).setProperty(activeRow, "quality", unsignedValue) activeQuality = unsignedValue markScenarioDirty() - if (workflowBar.session && workflowBar.session.liveControlReady && !pendingApply) - workflowBar.session.requestSetQuality(activeSignal, unsignedValue) + if (workflowBar.session && workflowBar.session.liveControlReady && !pendingApply && + !workflowBar.session.requestSetQuality(activeSignal, unsignedValue)) + pendingApply = true } Shortcut { sequence: "Ctrl+B"; enabled: !workflowBar.startQueued; onActivated: root.balanced() } @@ -731,11 +735,12 @@ ApplicationWindow { } Rectangle { width: 1; height: 12; color: studioTheme.lineSoft } Label { - text: root.pendingApply && !workflowBar.injectionRunning - ? "4I + 4V · Changes sent on Start" + text: root.pendingApply + ? (workflowBar.injectionRunning + ? "4I + 4V · Changes pending · restart to apply" + : "4I + 4V · Changes sent on Start") : "4I + 4V · 4000 samples/s" - color: root.pendingApply && !workflowBar.injectionRunning - ? studioTheme.amber : studioTheme.muted + color: root.pendingApply ? studioTheme.amber : studioTheme.muted font.family: root.uiFont font.pixelSize: 9 } From 620ff150840e2e362a7f57c791d08ca97557430a Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 05:36:44 +0700 Subject: [PATCH 15/21] test(studio): catch silent live-setpoint failures and show pending state during injection --- apps/arstack_studio/tests/check_operator_workspace.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/arstack_studio/tests/check_operator_workspace.py b/apps/arstack_studio/tests/check_operator_workspace.py index 94154f5d9..80163d780 100644 --- a/apps/arstack_studio/tests/check_operator_workspace.py +++ b/apps/arstack_studio/tests/check_operator_workspace.py @@ -34,8 +34,9 @@ def main() -> None: and "Signals, firmware, PTP and device settings" in config, "Advanced must not imply this is only a simulated lab tool") require("Changes sent on Start" in main_qml - and "root.pendingApply && !workflowBar.injectionRunning" in main_qml, - "STOPPED waveform edits must distinguish draft from sent setpoints") + and "Changes pending · restart to apply" in main_qml + and "root.pendingApply ? studioTheme.amber" in main_qml, + "show draft edits distinctly from sent setpoints, even while injecting") controller = (STUDIO / "src/DeviceController.cpp").read_text(encoding="utf-8") require("logUpdateTimer_.setInterval(100)" in controller and "if (!logUpdateTimer_.isActive()) logUpdateTimer_.start();" in controller, @@ -194,6 +195,11 @@ def main() -> None: and "property bool waveformDockVisible: true" in main_qml and "tray.session.requestSmpSynch(" in tray, "phasor and waveform must default visible and keep AUTO sync selection") + require("!workflowBar.session.liveControlReady" in main_qml + and "!workflowBar.session.requestSetFrequency(value)" in main_qml + and "!workflowBar.session.requestZero()" in main_qml + and "!workflowBar.session.requestSetQuality(activeSignal, unsignedValue)" in main_qml, + "unavailable or rejected live updates must be marked pending") require("const sent = phaseLink && row < 3" in main_qml and "if (!sent) {" in main_qml and "pendingApply = true" in main_qml, From 5c2b72e80d2adb908b73fe702fb0a93032711d64 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 05:44:50 +0700 Subject: [PATCH 16/21] =?UTF-8?q?test(studio):=20enforce=20vertical=20Volt?= =?UTF-8?q?age=20over=20Current=20and=20no=20overlap=20at=20Windows=20100?= =?UTF-8?q?=E2=80=93175%=20DPI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/arstack_studio/src/main.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/arstack_studio/src/main.cpp b/apps/arstack_studio/src/main.cpp index 13518af84..595849292 100644 --- a/apps/arstack_studio/src/main.cpp +++ b/apps/arstack_studio/src/main.cpp @@ -82,8 +82,13 @@ struct UiBounds { return false; } } - if (!(voltage.area.right() <= current.area.left() + tolerance - && tray.area.top() >= voltage.area.bottom() - tolerance + // Voltage and Current intentionally stack vertically. The native + // geometry test must detect clipping/overlap in the NEW layout, rather + // than assert the obsolete side-by-side relationship. + if (!(voltage.area.bottom() <= current.area.top() + tolerance + && std::abs(voltage.area.left() - current.area.left()) <= tolerance + && std::abs(voltage.area.right() - current.area.right()) <= tolerance + && tray.area.top() >= current.area.bottom() - tolerance && quality.area.top() >= tray.area.bottom() - tolerance && quality.area.bottom() <= surface.area.bottom() + tolerance)) { qCritical().noquote() << "Studio UI panel order/overlap violation" << size From 651076a6c059e32234131871bad6674f35175d2b Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 05:45:07 +0700 Subject: [PATCH 17/21] build(studio): explicitly include cmath for DPI-aligned geometric tolerance --- apps/arstack_studio/src/main.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/arstack_studio/src/main.cpp b/apps/arstack_studio/src/main.cpp index 595849292..02257db7d 100644 --- a/apps/arstack_studio/src/main.cpp +++ b/apps/arstack_studio/src/main.cpp @@ -30,6 +30,7 @@ #include #include +#include #include #include From b4337c64b58cb5d9de137c549bc999d74c57aeea Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 05:47:08 +0700 Subject: [PATCH 18/21] fix(studio): scroll vertically at minimum window size without clipping stacked 4I/4V controls --- apps/arstack_studio/qml/Main.qml | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/apps/arstack_studio/qml/Main.qml b/apps/arstack_studio/qml/Main.qml index f29c7143c..098ad49d6 100644 --- a/apps/arstack_studio/qml/Main.qml +++ b/apps/arstack_studio/qml/Main.qml @@ -968,10 +968,31 @@ ApplicationWindow { SplitView.preferredWidth: root.compactLayout ? 700 : 860 theme: studioTheme - ColumnLayout { + // A narrow Windows viewport cannot display two full + // engineering tables plus timing/quality at once. Scroll + // the injection editor as ONE surface at small heights; + // normal desktop sizes fully expand the two matrices. + ScrollView { + id: injectionScroll + objectName: "studioInjectionScroll" anchors.fill: parent - anchors.margins: root.compactLayout ? 8 : 9 - spacing: root.compactLayout ? 4 : 5 + clip: true + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AsNeeded + contentWidth: availableWidth + contentHeight: injectionEditor.y + injectionEditor.height + + (root.compactLayout ? 8 : 9) + + ColumnLayout { + id: injectionEditor + x: root.compactLayout ? 8 : 9 + y: root.compactLayout ? 8 : 9 + width: Math.max(1, injectionScroll.availableWidth - + 2 * (root.compactLayout ? 8 : 9)) + height: Math.max(implicitHeight, + injectionScroll.availableHeight - + 2 * (root.compactLayout ? 8 : 9)) + spacing: root.compactLayout ? 4 : 5 RowLayout { Layout.fillWidth: true @@ -1178,6 +1199,7 @@ ApplicationWindow { compact: root.compactLayout } } + } } } From 5dbc4995f77728370043b3e7245079fcc6da8d27 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 05:47:33 +0700 Subject: [PATCH 19/21] test(studio): validate scrollable minimum viewport and full desktop vertical matrix geometry --- apps/arstack_studio/src/main.cpp | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/apps/arstack_studio/src/main.cpp b/apps/arstack_studio/src/main.cpp index 02257db7d..09203b4b7 100644 --- a/apps/arstack_studio/src/main.cpp +++ b/apps/arstack_studio/src/main.cpp @@ -73,11 +73,21 @@ struct UiBounds { {"voltage matrix", voltage}, {"current matrix", current}, {"PTP timing tray", tray}, {"quality editor", quality} }}; + // At the supported 960x620 minimum viewport the editor uses a + // vertical ScrollView; offscreen lower controls must remain laid out + // correctly and reachable by scrolling, not be smashed to <30px rows. + // Full desktop views MUST still show all controls without clipping. + const bool compactScroll = size.height() < 700; for (const auto& [label, item] : areas) { + const bool mustBeInViewport = !compactScroll || + std::string_view{label} == "toolbar" || + std::string_view{label} == "injection workspace" || + std::string_view{label} == "voltage matrix"; if (!item.visible || item.area.width() < 25 || item.area.height() < 15 || - !viewport.adjusted(-tolerance, -tolerance, tolerance, tolerance) - .contains(item.area.adjusted(tolerance, tolerance, - -tolerance, -tolerance))) { + (mustBeInViewport && + !viewport.adjusted(-tolerance, -tolerance, tolerance, tolerance) + .contains(item.area.adjusted(tolerance, tolerance, + -tolerance, -tolerance)))) { qCritical().noquote() << "Studio UI viewport violation" << size << label << "bounds=" << item.area << "viewport=" << viewport; return false; @@ -86,6 +96,14 @@ struct UiBounds { // Voltage and Current intentionally stack vertically. The native // geometry test must detect clipping/overlap in the NEW layout, rather // than assert the obsolete side-by-side relationship. + const auto scroll = findUiBounds(window, "studioInjectionScroll"); + if (!scroll.visible || + !surface.area.adjusted(-tolerance, -tolerance, tolerance, tolerance) + .contains(scroll.area.adjusted(tolerance, tolerance, -tolerance, -tolerance))) { + qCritical().noquote() << "Studio operator scrolling surface missing" << size + << scroll.area << surface.area; + return false; + } if (!(voltage.area.bottom() <= current.area.top() + tolerance && std::abs(voltage.area.left() - current.area.left()) <= tolerance && std::abs(voltage.area.right() - current.area.right()) <= tolerance From db7558d77fe29c639e6a2f3a68d837a3c7e6fee7 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 05:48:15 +0700 Subject: [PATCH 20/21] test(studio): enforce vertical matrix scroll at supported 960x620 window --- apps/arstack_studio/tests/check_operator_workspace.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/arstack_studio/tests/check_operator_workspace.py b/apps/arstack_studio/tests/check_operator_workspace.py index 80163d780..49e9d235c 100644 --- a/apps/arstack_studio/tests/check_operator_workspace.py +++ b/apps/arstack_studio/tests/check_operator_workspace.py @@ -168,10 +168,12 @@ def main() -> None: check_box = source("StudioDarkCheckBox.qml") combo = source("StudioDarkComboBox.qml") require("Layout.minimumHeight: 408" in main_qml - and "ColumnLayout {\n Layout.fillWidth: true\n Layout.fillHeight: true" in main_qml + and 'objectName: "studioInjectionScroll"' in main_qml + and "id: injectionEditor" in main_qml + and "contentHeight: injectionEditor.y + injectionEditor.height" in main_qml and "Layout.fillHeight: true" in matrix and "Item { Layout.fillHeight: true }" not in matrix, - "4I and 4V must stack and expand, not leave unused lower area") + "4I and 4V must stack, expand, and scroll rather than clip at minimum size") require("width: 40" in ribbon and "height: 40" in ribbon and 'font.pixelSize: bar.compact ? 14 : 15' in ribbon and "anchors.centerIn: parent" in ribbon_action, From d02bb037790942dbe2215ae7ca10eb0c9f8ee6aa Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 05:48:39 +0700 Subject: [PATCH 21/21] test(studio): allow quality editor below fold only when minimum-height viewport scrolls --- apps/arstack_studio/src/main.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/arstack_studio/src/main.cpp b/apps/arstack_studio/src/main.cpp index 09203b4b7..a605c19cd 100644 --- a/apps/arstack_studio/src/main.cpp +++ b/apps/arstack_studio/src/main.cpp @@ -109,7 +109,8 @@ struct UiBounds { && std::abs(voltage.area.right() - current.area.right()) <= tolerance && tray.area.top() >= current.area.bottom() - tolerance && quality.area.top() >= tray.area.bottom() - tolerance - && quality.area.bottom() <= surface.area.bottom() + tolerance)) { + && (compactScroll || + quality.area.bottom() <= surface.area.bottom() + tolerance))) { qCritical().noquote() << "Studio UI panel order/overlap violation" << size << voltage.area << current.area << tray.area << quality.area; return false;