From 6020785ad83a2e51e22b5c13ca4cfc8a0fe3dffa Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 07:27:14 +0700 Subject: [PATCH 01/17] fix(studio): rederive linked phasors from visible table and stack voltage/current vertically --- apps/arstack_studio/qml/Main.qml | 36 +++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/apps/arstack_studio/qml/Main.qml b/apps/arstack_studio/qml/Main.qml index 66a0daf89..51b509add 100644 --- a/apps/arstack_studio/qml/Main.qml +++ b/apps/arstack_studio/qml/Main.qml @@ -444,6 +444,30 @@ ApplicationWindow { for (var row = 0; row < 4; ++row) sendSignal(group, row) } + function applyPhaseLink() { + if (!phaseLink || signalFrequency === 0) return + // Preserve each group's selected phase-A setpoint, and derive B/C + // from it consistently in BOTH the model and the next wire batch. + for (var group = 0; group < 2; ++group) { + var model = groupModel(group) + var base = model.get(0).phase + model.setProperty(1, "phase", normalizedAngle(base - 120)) + model.setProperty(2, "phase", normalizedAngle(base + 120)) + } + refreshPreview() + markScenarioDirty() + if (workflowBar.injectionRunning && workflowBar.session && + workflowBar.session.liveControlReady && !pendingApply) { + // Explicit link changes during RUNNING are real setpoint edits. + // Don't silently leave the publisher at its previous angles. + var ok = sendLinkedGroup(1) && sendLinkedGroup(0) + if (!ok) { + pendingApply = true + showMessage("Phase values not sent; Stop then Start to apply.", true) + } + } + } + function editSignal(group, row, field, value) { if ((field === "magnitude" && !validMagnitude(group, value)) || (field === "phase" && !validPhase(value))) @@ -487,6 +511,8 @@ ApplicationWindow { return ok } function startScenarioSnapshot() { + // The visible 4I+4V table is the only START setpoint authority. + // Capture it immediately before the worker queues its FIFO batch. var draft = scenarioSnapshot() draft.currentScale = currentScale draft.voltageScale = voltageScale @@ -1040,7 +1066,8 @@ ApplicationWindow { text: "3-phase link" onToggled: { root.phaseLink = checked - root.markScenarioDirty() + if (checked) root.applyPhaseLink() + else root.markScenarioDirty() } font.family: root.uiFont font.pixelSize: 9 @@ -1049,11 +1076,10 @@ ApplicationWindow { } } - RowLayout { + ColumnLayout { Layout.fillWidth: true - Layout.preferredHeight: root.compactLayout ? 214 : 224 - Layout.maximumHeight: root.compactLayout ? 214 : 224 - spacing: 9 + Layout.fillHeight: true + spacing: 10 SignalMatrix { id: voltageMatrix enabled: !workflowBar.startQueued From e973ea5cd5117d6068de9237a1728aff6b443718 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 07:28:24 +0700 Subject: [PATCH 02/17] fix(studio): always send visible table with START --- apps/arstack_studio/qml/WorkflowBar.qml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/arstack_studio/qml/WorkflowBar.qml b/apps/arstack_studio/qml/WorkflowBar.qml index e49593114..6f8568b1c 100644 --- a/apps/arstack_studio/qml/WorkflowBar.qml +++ b/apps/arstack_studio/qml/WorkflowBar.qml @@ -281,9 +281,11 @@ SurfacePanel { controller.showMessage(ribbon.startReason(), false) return } - const accepted = controller.pendingApply - ? smartSession.requestStartWithScenario(controller.startScenarioSnapshot()) - : smartSession.requestStart() + // START always transmits a fresh snapshot of the visible 4I+4V + // model. The firmware may have different setpoints after reconnect, + // earlier LIVE SETs, or restoring the operator's local scenario. + const accepted = smartSession.requestStartWithScenario( + controller.startScenarioSnapshot()) if (!accepted) { controller.pendingApply = true controller.showMessage("Start rejected. Check device status.", true) From 6770eec59d91f145e8cb59bf6b24830104191e90 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 07:28:29 +0700 Subject: [PATCH 03/17] fix(studio): size SMV Injector identity at desktop scale --- 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..6ed95645b 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 ? 155 : 182 Layout.alignment: Qt.AlignVCenter spacing: 8 Rectangle { - width: 28 - height: 28 - radius: 7 + width: 38 + height: 38 + radius: 9 color: "#0d151d" border.width: 1 border.color: bar.theme.lineSoft Image { anchors.centerIn: parent - width: 15 - height: 15 + width: 24 + height: 24 source: Qt.resolvedUrl("../assets/material/smv_injector.svg") - sourceSize.width: 30 - sourceSize.height: 30 + sourceSize.width: 48 + sourceSize.height: 48 } } @@ -73,7 +73,7 @@ Rectangle { text: "SMV Injector" color: bar.theme.text font.family: bar.uiFont - font.pixelSize: 11 + font.pixelSize: 15 font.weight: Font.DemiBold elide: Text.ElideRight verticalAlignment: Text.AlignVCenter From 9869295677c6eae129f5c98961d4fb2198849b62 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 07:29:11 +0700 Subject: [PATCH 04/17] fix(studio): use dark centered text on all light Advanced button hovers --- apps/arstack_studio/qml/CalmButton.qml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/arstack_studio/qml/CalmButton.qml b/apps/arstack_studio/qml/CalmButton.qml index ddddca61c..6aee5e355 100644 --- a/apps/arstack_studio/qml/CalmButton.qml +++ b/apps/arstack_studio/qml/CalmButton.qml @@ -21,7 +21,10 @@ Button { scale: down ? 0.988 : 1.0 Behavior on scale { NumberAnimation { duration: 70; easing.type: Easing.OutCubic } } + // Consistent desktop hover affordance: light fill always pairs with dark + // caption. Applies to Advanced panels because they share CalmButton. readonly property color activeFill: !enabled ? "#10161d" : + hovered ? "#edf4fc" : tone === "success" ? (hovered ? "#1e5c43" : "#194d38") : tone === "danger" ? (hovered ? "#523039" : theme.redSoft) : tone === "accent" ? (hovered ? "#1d3c5e" : theme.accentSoft) : @@ -32,6 +35,7 @@ Button { 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 @@ -53,6 +57,8 @@ Button { text: control.text color: control.activeText font: control.font + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter Behavior on color { ColorAnimation { duration: 90 } } } From f8812494fe1a034543837d3a3e2ebb3ff290a10c Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 07:29:15 +0700 Subject: [PATCH 05/17] fix(studio): center ribbon icon and captions in both axes --- apps/arstack_studio/qml/RibbonAction.qml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/arstack_studio/qml/RibbonAction.qml b/apps/arstack_studio/qml/RibbonAction.qml index a882bb495..93c3fbcd0 100644 --- a/apps/arstack_studio/qml/RibbonAction.qml +++ b/apps/arstack_studio/qml/RibbonAction.qml @@ -39,7 +39,8 @@ Button { : tone === "danger" ? "#ffe3e6" : tone === "accent" || checked ? "#e1f1ff" : theme.textSoft contentItem: ColumnLayout { - spacing: 3 + anchors.centerIn: parent + spacing: 4 Image { visible: control.iconSource.toString().length > 0 source: control.hovered && control.enabled ? control.hoverPath : control.iconSource @@ -56,7 +57,7 @@ Button { text: control.text color: control.textColor font: control.font - Layout.alignment: Qt.AlignHCenter + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter elide: Text.ElideRight From cda23b96d6ec7c797dd227d7edc27a331f6bda7f Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 07:29:20 +0700 Subject: [PATCH 06/17] fix(studio): keep one constant-size checkbox indicator on hover --- apps/arstack_studio/qml/StudioDarkCheckBox.qml | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/apps/arstack_studio/qml/StudioDarkCheckBox.qml b/apps/arstack_studio/qml/StudioDarkCheckBox.qml index eedfce586..4675936a4 100644 --- a/apps/arstack_studio/qml/StudioDarkCheckBox.qml +++ b/apps/arstack_studio/qml/StudioDarkCheckBox.qml @@ -7,20 +7,26 @@ CheckBox { id: control property var theme property string uiFont: "Inter" - implicitHeight: 30 + implicitHeight: 32 + hoverEnabled: true spacing: 9 font.family: uiFont font.pixelSize: 11 indicator: Rectangle { - implicitWidth: 18 - implicitHeight: 18 + implicitWidth: 20 + implicitHeight: 20 + width: 20 + height: 20 x: control.leftPadding y: (control.height - height) / 2 radius: 5 color: control.checked ? control.theme.accent : control.theme.surface2 border.width: 1 + // Hover changes only the border colour, never checkbox size or + // creates a second overlay/nested platform-native indicator. 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 @@ -35,6 +41,7 @@ CheckBox { text: control.text font: control.font color: control.enabled ? control.theme.textSoft : control.theme.muted + horizontalAlignment: Text.AlignLeft verticalAlignment: Text.AlignVCenter elide: Text.ElideRight } From 766afe0b3c82d141577854c6fb16c089b5ed9515 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 07:29:24 +0700 Subject: [PATCH 07/17] fix(studio): center Advanced navigation and maintain contrast on hover --- apps/arstack_studio/qml/ConfigurationHub.qml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/arstack_studio/qml/ConfigurationHub.qml b/apps/arstack_studio/qml/ConfigurationHub.qml index fecaa51a3..c53519d52 100644 --- a/apps/arstack_studio/qml/ConfigurationHub.qml +++ b/apps/arstack_studio/qml/ConfigurationHub.qml @@ -32,15 +32,18 @@ Item { font.family: hub.uiFont font.pixelSize: 11 font.weight: checked ? Font.DemiBold : Font.Medium + hoverEnabled: true contentItem: Label { text: parent.text - color: parent.checked ? hub.theme.text : hub.theme.muted + color: parent.hovered ? "#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 ? "#edf4fc" : + parent.checked ? hub.theme.raised : "transparent" radius: 6 border.width: parent.checked ? 1 : 0 border.color: hub.theme.line From 31d23a603b0a43030c486eae8c189162326161b9 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 07:30:11 +0700 Subject: [PATCH 08/17] feat(studio): default PTP SOURCE AUTO on verified connect and balanced current table; fit stacked matrices --- apps/arstack_studio/qml/Main.qml | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/apps/arstack_studio/qml/Main.qml b/apps/arstack_studio/qml/Main.qml index 51b509add..3c4698fdb 100644 --- a/apps/arstack_studio/qml/Main.qml +++ b/apps/arstack_studio/qml/Main.qml @@ -112,13 +112,15 @@ ApplicationWindow { property string uiFont: interFont.status === FontLoader.Ready ? interFont.name : "Inter" property string monoFont: uiFont - property bool phaseLink: false + property bool phaseLink: true property bool profileDirty: false property bool pendingApply: true // Session-only operator intent. Never persist across application launches: // an explicit PTP ON can be restored after a firmware reflash/reconnect // without introducing an uncontrolled power-on publisher. - property bool ptpDesiredForSession: false + // Operator default: PTP SOURCE ON after verified connection; OFF overrides + // this for the remainder of this Studio session. + property bool ptpDesiredForSession: true property string ptpDesiredRole: "SOURCE" property bool ptpRestoreAfterFirmware: false property int ptpRestoreChecks: 0 @@ -198,6 +200,11 @@ ApplicationWindow { function onDeviceVerifiedChanged() { root.pendingApply = true root.offlinePreviewRunning = false + if (device.deviceVerified && root.ptpDesiredForSession && + !device.ptpRunning) { + root.ptpRestoreChecks = 0 + root.ptpRestoreAfterFirmware = true + } } } @@ -251,9 +258,9 @@ ApplicationWindow { ListModel { id: currentModel - ListElement { signalId: "Ia"; magnitude: 1.000; phase: 0.0; enabled: true; quality: 0; traceColor: "#ff6673" } - ListElement { signalId: "Ib"; magnitude: 1.000; phase: -120.0; enabled: true; quality: 0; traceColor: "#e6b552" } - ListElement { signalId: "Ic"; magnitude: 1.000; phase: 120.0; enabled: true; quality: 0; traceColor: "#59a7ff" } + ListElement { signalId: "Ia"; magnitude: 1.000; phase: -30.0; enabled: true; quality: 0; traceColor: "#ff6673" } + ListElement { signalId: "Ib"; magnitude: 1.000; phase: -150.0; enabled: true; quality: 0; traceColor: "#e6b552" } + ListElement { signalId: "Ic"; magnitude: 1.000; phase: 90.0; enabled: true; quality: 0; traceColor: "#59a7ff" } ListElement { signalId: "In"; magnitude: 0.000; phase: 0.0; enabled: true; quality: 0; traceColor: "#9a88df" } } @@ -1086,6 +1093,9 @@ ApplicationWindow { objectName: "studioVoltageMatrix" Layout.fillWidth: true Layout.fillHeight: true + Layout.minimumHeight: 175 + Layout.preferredHeight: root.compactLayout ? 180 : 205 + Layout.maximumHeight: 242 theme: studioTheme controller: root device: device @@ -1105,6 +1115,9 @@ ApplicationWindow { objectName: "studioCurrentMatrix" Layout.fillWidth: true Layout.fillHeight: true + Layout.minimumHeight: 175 + Layout.preferredHeight: root.compactLayout ? 180 : 205 + Layout.maximumHeight: 242 theme: studioTheme controller: root device: device From c5a71702a1d06d2e863c454b9e3a74bcbbbafae1 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 07:30:58 +0700 Subject: [PATCH 09/17] test(studio): lock latest-table injection, PTP defaults and refreshed operator layout without touching firmware --- .../tests/check_operator_workspace.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/apps/arstack_studio/tests/check_operator_workspace.py b/apps/arstack_studio/tests/check_operator_workspace.py index ecca26e80..7ac0d6736 100644 --- a/apps/arstack_studio/tests/check_operator_workspace.py +++ b/apps/arstack_studio/tests/check_operator_workspace.py @@ -54,6 +54,34 @@ def main() -> None: ordered(main_qml, "id: voltageMatrix", "id: currentMatrix", "voltage must precede current in the injection table") + require("ColumnLayout {\n Layout.fillWidth: true\n Layout.fillHeight: true\n spacing: 10\n SignalMatrix {" in main_qml, + "voltage and current must stack vertically and use available height") + require('property bool ptpDesiredForSession: true' in main_qml + and 'ptpRestoreAfterFirmware = true' in main_qml + and 'ptpDesiredForSession = false' in main_qml + and 'property bool phaseLink: true' in main_qml, + "PTP SOURCE/AUTO and three-phase link start enabled, while OFF remains authoritative") + require('var draft = scenarioSnapshot()' in main_qml + and 'const accepted = smartSession.requestStartWithScenario(' in workflow + and ': smartSession.requestStart()' not in workflow, + "every START must send the latest 4I+4V table, not stale firmware setpoints") + require('function applyPhaseLink()' in main_qml + and 'if (checked) root.applyPhaseLink()' in main_qml + and 'sendLinkedGroup(1) && sendLinkedGroup(0)' in main_qml, + "3-phase link must update the model and send coherent live values") + require('font.pixelSize: 15' in ribbon + and 'width: 38' in ribbon and 'height: 38' in ribbon, + "SMV Injector logo and title must remain legible") + require('hovered ? "#edf4fc"' in source("CalmButton.qml") + and 'hovered ? "#142334"' in source("CalmButton.qml") + and 'parent.hovered ? "#edf4fc"' in config + and 'parent.hovered ? "#142334"' in config, + "Advanced light hover must use dark caption text") + require('width: 20' in source("StudioDarkCheckBox.qml") + and 'height: 20' in source("StudioDarkCheckBox.qml") + and 'control.hovered || control.activeFocus' in source("StudioDarkCheckBox.qml"), + "checkbox may change its colour, never its size on hover") + ordered(preview, "id: voltagePhasorCanvas", "id: currentPhasorCanvas", "voltage must precede current in the phasor view") ordered(preview, "drawWaveGroup(preview.voltageModel, voltageY, false)", From 87d44e0a4c571b23bfd863cd4f05b148d41ec912 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 07:33:29 +0700 Subject: [PATCH 10/17] test(studio): initialize workflow/ribbon fixtures before visual SSOT guards --- apps/arstack_studio/tests/check_operator_workspace.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/arstack_studio/tests/check_operator_workspace.py b/apps/arstack_studio/tests/check_operator_workspace.py index 7ac0d6736..8731b22fa 100644 --- a/apps/arstack_studio/tests/check_operator_workspace.py +++ b/apps/arstack_studio/tests/check_operator_workspace.py @@ -26,6 +26,8 @@ def ordered(data: str, first: str, second: str, reason: str) -> None: def main() -> None: main_qml = source("Main.qml") + workflow = source("WorkflowBar.qml") + ribbon = source("ModernRibbon.qml") preview = source("SignalPreview.qml") tray = source("StudioTimingTray.qml") expert = source("PtpExpertPanel.qml") From f4041ebe9194e151e4990900f67e3576308fa3bc Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 07:35:36 +0700 Subject: [PATCH 11/17] test(studio): assert multiline unconditional START snapshot call instead of obsolete ternary --- 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 8731b22fa..76e5b7112 100644 --- a/apps/arstack_studio/tests/check_operator_workspace.py +++ b/apps/arstack_studio/tests/check_operator_workspace.py @@ -146,7 +146,8 @@ def main() -> None: and 'enabled: !workflowBar.startQueued; onActivated: root.zeroAll()' in main_qml, "scenario editors and shortcuts must be frozen during a pending START snapshot") require("controller.pendingApply = true" in workflow - and "requestStartWithScenario(controller.startScenarioSnapshot())" in workflow + and "smartSession.requestStartWithScenario(" in workflow + and "controller.startScenarioSnapshot())" in workflow 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") @@ -175,7 +176,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 workflow + and 'controller.startScenarioSnapshot())' in workflow 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 From 1a49569d2f3ceea16875dadb30296ee5431848e8 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 07:36:14 +0700 Subject: [PATCH 12/17] fix(studio): remove native checkbox hover overlay behind fixed custom indicator --- apps/arstack_studio/qml/StudioDarkCheckBox.qml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/arstack_studio/qml/StudioDarkCheckBox.qml b/apps/arstack_studio/qml/StudioDarkCheckBox.qml index 4675936a4..744df83b1 100644 --- a/apps/arstack_studio/qml/StudioDarkCheckBox.qml +++ b/apps/arstack_studio/qml/StudioDarkCheckBox.qml @@ -36,6 +36,9 @@ CheckBox { font.weight: Font.Bold } } + // Disable native platform hover/ripple background: the custom indicator + // is the ONLY checkbox affordance in hover and non-hover states. + background: Item {} contentItem: Text { leftPadding: control.indicator.width + control.spacing text: control.text From 2e16a79f4e4e4d47cf571663e4852d29503bfaf3 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 07:37:55 +0700 Subject: [PATCH 13/17] style(studio): fit 4I+4V stacked matrices without wasted row height --- apps/arstack_studio/qml/SignalMatrix.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/arstack_studio/qml/SignalMatrix.qml b/apps/arstack_studio/qml/SignalMatrix.qml index 1c84e2b43..aa739710b 100644 --- a/apps/arstack_studio/qml/SignalMatrix.qml +++ b/apps/arstack_studio/qml/SignalMatrix.qml @@ -196,7 +196,7 @@ Rectangle { } Layout.fillWidth: true - Layout.preferredHeight: matrix.compact ? 40 : 42 + Layout.preferredHeight: matrix.compact ? 34 : 35 color: signalRow.selected ? "#101c27" : (magnitudeField.activeFocus || phaseField.activeFocus) ? "#0f1922" : rowHover.hovered ? "#0d161e" : "transparent" From 0922e8c3c1deb74c7179eb2c4e226298b58e618a Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 07:45:53 +0700 Subject: [PATCH 14/17] =?UTF-8?q?test(studio):=20validate=20real=20stacked?= =?UTF-8?q?=20Voltage=E2=86=92Current=E2=86=92PTP=E2=86=92Quality=20geomet?= =?UTF-8?q?ry=20at=20every=20DPI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/arstack_studio/src/main.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/arstack_studio/src/main.cpp b/apps/arstack_studio/src/main.cpp index 13518af84..beb92c609 100644 --- a/apps/arstack_studio/src/main.cpp +++ b/apps/arstack_studio/src/main.cpp @@ -82,8 +82,11 @@ struct UiBounds { return false; } } - if (!(voltage.area.right() <= current.area.left() + tolerance - && tray.area.top() >= voltage.area.bottom() - tolerance + // Matrices are intentionally stacked, not side-by-side. Check the + // real on-screen order and containment at 960x620, 1240x760 and + // 1440x900 instead of applying the old horizontal contract. + if (!(voltage.area.bottom() <= current.area.top() + 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 9a428474fb8393bb4ba2aec75b8bf6c61cc74bc9 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 07:47:37 +0700 Subject: [PATCH 15/17] fix(studio): scroll stacked 4I+4V matrices at short Windows viewports without clipping PTP tray --- apps/arstack_studio/qml/Main.qml | 35 +++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/apps/arstack_studio/qml/Main.qml b/apps/arstack_studio/qml/Main.qml index 3c4698fdb..831008ac1 100644 --- a/apps/arstack_studio/qml/Main.qml +++ b/apps/arstack_studio/qml/Main.qml @@ -1083,19 +1083,34 @@ ApplicationWindow { } } - ColumnLayout { + // At the supported 960x620 viewport two full tables cannot + // occupy 400+ px without clipping the PTP controls. + // Keep both tables stacked, with bounded scrolling only + // when the viewport is shorter than their natural height. + ScrollView { + id: matrixScroll + objectName: "studioMatrixViewport" Layout.fillWidth: true Layout.fillHeight: true - spacing: 10 - SignalMatrix { + clip: true + contentWidth: availableWidth + contentHeight: matrixStack.implicitHeight + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AsNeeded + + ColumnLayout { + id: matrixStack + width: matrixScroll.availableWidth + spacing: 8 + SignalMatrix { id: voltageMatrix enabled: !workflowBar.startQueued objectName: "studioVoltageMatrix" Layout.fillWidth: true Layout.fillHeight: true - Layout.minimumHeight: 175 - Layout.preferredHeight: root.compactLayout ? 180 : 205 - Layout.maximumHeight: 242 + Layout.minimumHeight: 201 + Layout.preferredHeight: 201 + Layout.maximumHeight: 201 theme: studioTheme controller: root device: device @@ -1115,9 +1130,9 @@ ApplicationWindow { objectName: "studioCurrentMatrix" Layout.fillWidth: true Layout.fillHeight: true - Layout.minimumHeight: 175 - Layout.preferredHeight: root.compactLayout ? 180 : 205 - Layout.maximumHeight: 242 + Layout.minimumHeight: 201 + Layout.preferredHeight: 201 + Layout.maximumHeight: 201 theme: studioTheme controller: root device: device @@ -1130,8 +1145,8 @@ ApplicationWindow { symbolText: "I" unitText: root.signalFrequency === 0 ? "A DC" : "A RMS" compact: root.compactLayout + } } - } StudioTimingTray { From 50415268b63cfe210cb4de693c25d5990e53f25f Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 07:48:14 +0700 Subject: [PATCH 16/17] test(studio): verify stacked scroll viewport and unclipped PTP controls at all Windows DPIs --- apps/arstack_studio/src/main.cpp | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/apps/arstack_studio/src/main.cpp b/apps/arstack_studio/src/main.cpp index beb92c609..711fc83b3 100644 --- a/apps/arstack_studio/src/main.cpp +++ b/apps/arstack_studio/src/main.cpp @@ -63,13 +63,17 @@ struct UiBounds { const QRectF viewport{QPointF{0, 0}, QSizeF{size}}; const auto bar = findUiBounds(window, "studioWorkflowBar"); const auto surface = findUiBounds(window, "studioInjectionSurface"); + const auto matrixViewport = findUiBounds(window, "studioMatrixViewport"); const auto voltage = findUiBounds(window, "studioVoltageMatrix"); const auto current = findUiBounds(window, "studioCurrentMatrix"); const auto tray = findUiBounds(window, "studioTimingTray"); const auto quality = findUiBounds(window, "studioQualityRow"); - const std::array, 6> areas{{ + // At minimum-height desktop viewports the stacked matrix content scrolls. + // Assert containment of the scroll viewport, not offscreen scrollable + // rows. Offscreen tables remain real, ordered QML items. + const std::array, 5> areas{{ {"toolbar", bar}, {"injection workspace", surface}, - {"voltage matrix", voltage}, {"current matrix", current}, + {"matrix scroll viewport", matrixViewport}, {"PTP timing tray", tray}, {"quality editor", quality} }}; for (const auto& [label, item] : areas) { @@ -82,15 +86,21 @@ struct UiBounds { return false; } } - // Matrices are intentionally stacked, not side-by-side. Check the - // real on-screen order and containment at 960x620, 1240x760 and - // 1440x900 instead of applying the old horizontal contract. - if (!(voltage.area.bottom() <= current.area.top() + tolerance - && tray.area.top() >= current.area.bottom() - tolerance - && quality.area.top() >= tray.area.bottom() - tolerance - && quality.area.bottom() <= surface.area.bottom() + tolerance)) { + // Matrices must remain stacked with no overlap in scroll content, + // while PTP/quality remain visible OUTSIDE the scroll area. This + // accepts scrollable rows at 960x620 without tolerating a clipped + // operational PTP switch or status row. + if (!(voltage.visible && current.visible && + voltage.area.width() >= 25 && current.area.width() >= 25 && + voltage.area.height() >= 100 && current.area.height() >= 100 && + voltage.area.bottom() <= current.area.top() + tolerance && + matrixViewport.area.top() >= surface.area.top() - tolerance && + tray.area.top() >= matrixViewport.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 - << voltage.area << current.area << tray.area << quality.area; + << matrixViewport.area << voltage.area + << current.area << tray.area << quality.area; return false; } const std::array controls{{ From c35c73fe59ee45cc0d2149152c5969fb0b4a5807 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 07:48:34 +0700 Subject: [PATCH 17/17] test(studio): require scrollable 4I+4V stacks with native PTP tray visible at min viewport --- .../arstack_studio/tests/check_operator_workspace.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/apps/arstack_studio/tests/check_operator_workspace.py b/apps/arstack_studio/tests/check_operator_workspace.py index 76e5b7112..babcaee55 100644 --- a/apps/arstack_studio/tests/check_operator_workspace.py +++ b/apps/arstack_studio/tests/check_operator_workspace.py @@ -56,8 +56,16 @@ def main() -> None: ordered(main_qml, "id: voltageMatrix", "id: currentMatrix", "voltage must precede current in the injection table") - require("ColumnLayout {\n Layout.fillWidth: true\n Layout.fillHeight: true\n spacing: 10\n SignalMatrix {" in main_qml, - "voltage and current must stack vertically and use available height") + require('objectName: "studioMatrixViewport"' in main_qml + and 'contentHeight: matrixStack.implicitHeight' in main_qml + and 'ScrollBar.vertical.policy: ScrollBar.AsNeeded' in main_qml + and 'id: matrixStack' in main_qml, + "stacked voltage/current matrices must be scrollable at 960x620 without clipping PTP") + geometry = (STUDIO / "src/main.cpp").read_text(encoding="utf-8") + require('findUiBounds(window, "studioMatrixViewport")' in geometry + and 'voltage.area.bottom() <= current.area.top() + tolerance' in geometry + and 'tray.area.top() >= matrixViewport.area.bottom() - tolerance' in geometry, + "native UI viewport regression must validate stacked order and visible PTP controls") require('property bool ptpDesiredForSession: true' in main_qml and 'ptpRestoreAfterFirmware = true' in main_qml and 'ptpDesiredForSession = false' in main_qml