From 8f15d6fb347c458fee9a5d66cce91b11b1e2dbdb Mon Sep 17 00:00:00 2001 From: Yury Rodzikau Date: Fri, 11 Sep 2026 10:41:26 +0200 Subject: [PATCH 01/12] fix(svelte-client): add overflow-safe layout to DialogContent, DialogFooter and DialogTitle to keep long 3MF names and buttons inside dialogs On the Build Library page, selecting a build opened the details dialog with the 3MF file name and the "Close"/"Select" buttons running past the dialog border. DialogContent is a single-column CSS grid whose implicit auto column grows to the min-content width of its widest child. A file name without spaces is one unbreakable word, so the column grew beyond the 500px dialog box and the right-aligned footer moved with it. The fix is applied to the shared shadcn-svelte dialog components, so every dialog in the Svelte client gets it. feat(dialog-content): cap the content column and wrap long words - Add grid-cols-[minmax(0,1fr)]: the column can no longer grow wider than the dialog, so header, body and footer stay inside the border. - Add wrap-anywhere (overflow-wrap: anywhere). The property is inherited, so long names, paths and module content wrap in every dialog. Unlike wrap-break-word it also lowers the min-content width, which the grid column needs in order to shrink. Button captions keep whitespace-nowrap and stay on one line. feat(dialog-footer): wrap footer buttons - Add sm:flex-wrap. Buttons are shrink-0 and whitespace-nowrap, so several or long buttons (e.g. multiple + + {#if pointsAvailable} + + + {/if} @@ -718,7 +1037,7 @@ type="number" inputmode="numeric" min="0" - max={layerCount} + max={layerCount - 1} step="1" bind:value={layerJumpValue} onkeydown={onLayerJumpKeyDown} @@ -732,7 +1051,7 @@ type="button" class="layerview-layer-info layerview-layer-info-button" onclick={openLayerJump} - title="Click to jump to a layer" + title={sliderFixed ? 'Layer' : 'Click to jump to a layer'} aria-label={`Layer ${sliderValue} of ${layerCount}. Click to jump to a layer`} > Layer {sliderValue} / {layerCount} @@ -769,6 +1088,24 @@ {/if} + + {#if (labelVisible && (labelCaption || labelIcon)) || loadingLayer || loadingPoints} +
+ {#if labelVisible && (labelCaption || labelIcon)} +
+ + {labelCaption} +
+ {/if} + {#if loadingLayer || loadingPoints} +
+ + Loading layer data +
+ {/if} +
+ {/if} + {#if mousePosition}
@@ -776,6 +1113,14 @@
{/if} + + {#if hoverInfo.visible && !propertiesMode} +
{hoverInfo.text}
+ {/if} + {#if propertiesMode && hoverSegment}
{/if} - + {#if layerCount > 0}
+ {#if !sliderFixed} + + {/if} + {#if !sliderFixed} + + {/if}
{/if}
@@ -928,7 +1286,10 @@ position: absolute; top: 8px; left: 8px; + /* Leave room for the layer-info badge in the top-right corner */ + max-width: calc(100% - 140px); display: flex; + flex-wrap: wrap; gap: 4px; z-index: 10; } @@ -961,6 +1322,15 @@ background-color: var(--primary, #2563eb); box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.65); } + .layerview-btn:disabled { + opacity: 0.4; + cursor: default; + } + .layerview-step { + width: 24px; + height: 24px; + padding: 0; + } .layerview-layer-info { position: absolute; top: 8px; @@ -1003,6 +1373,8 @@ bottom: 8px; width: 64px; height: 64px; + border-radius: 4px; + background: rgba(0, 0, 0, 0.35); pointer-events: none; z-index: 9; } @@ -1029,6 +1401,31 @@ .coordinate-label-y { fill: #22c55e; } + .layerview-status { + position: absolute; + left: 8px; + bottom: 8px; + display: flex; + flex-wrap: wrap; + gap: 4px; + z-index: 10; + pointer-events: none; + } + .layerview-status.beside-axes { + /* Clear the coordinate axes indicator in the bottom-left corner */ + left: 80px; + } + .layerview-label { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 10px; + border-radius: 4px; + background: rgba(0, 0, 0, 0.75); + color: white; + font-size: 11px; + font-variant-numeric: tabular-nums; + } .layerview-mouse-pos { position: absolute; /* Shifted left so it clears the vertical layer slider on the right edge. */ @@ -1043,6 +1440,18 @@ pointer-events: none; z-index: 10; } + .layerview-hover-info { + position: absolute; + z-index: 20; + padding: 5px 8px; + border-radius: 4px; + background: rgba(0, 0, 0, 0.75); + color: white; + font-size: 11px; + font-variant-numeric: tabular-nums; + white-space: pre-line; + pointer-events: none; + } .layerview-segment-popup { position: absolute; min-width: 150px; @@ -1086,14 +1495,17 @@ top: 44px; bottom: 16px; display: flex; + flex-direction: column; align-items: center; justify-content: center; + gap: 6px; z-index: 10; } .layerview-slider { writing-mode: vertical-lr; direction: rtl; - height: 100%; + flex: 1; + min-height: 0; width: 20px; accent-color: var(--primary, #2563eb); } From a77fcbe1672a60fbf7c027d295be87576af27ef8 Mon Sep 17 00:00:00 2001 From: Yury Rodzikau Date: Thu, 17 Sep 2026 22:49:14 +0200 Subject: [PATCH 08/12] feat(svelte-client): add page-level vertical scrolling and flex-column page host in +page.svelte Pages whose layout is taller than the viewport (for example the System Settings grid with 1080pt of fixed-height rows) were clipped in the Svelte client: the page host had no scroll container, so the lower form fields and the Save/Cancel row were unreachable. The Vue 2 client scrolls the whole page inside its `.amc-content-card`; this change brings the Svelte shell in line with that behaviour. feat(svelte-client): make
the single page scroll container - Replace `relative` with `overflow-y-auto` on the page host `
`. - Header, sidebar and footer keep their fixed placement; only the page content scrolls. - Grids with fixed pt/px rows (AMCModule_Grid.js emits `height:100%`) now overflow into the scrollable area instead of being clipped by the `h-screen overflow-hidden` app root. - Grids with `free` rows and modules using `h-full` are unaffected and still fill exactly the viewport. feat(svelte-client): render the active page wrapper in normal flow as a flex column - Replace `absolute inset-2` with `h-full p-2 flex flex-col`. - `h-full` keeps the same box as the previous inset positioning, so full-height modules (Content, Tabs, ParameterList, LayerView) behave as before. - `flex flex-col` lets modules placed directly on a page that rely on `flex-1 min-h-0` (Logs, ParameterList, ConfigurationList) fill the viewport and scroll internally. Previously the System Logs page overflowed the block wrapper and was clipped. Signed-off-by: Yury Rodzikau --- Client/svelte/src/routes/+page.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Client/svelte/src/routes/+page.svelte b/Client/svelte/src/routes/+page.svelte index 8800d899..9df0874b 100644 --- a/Client/svelte/src/routes/+page.svelte +++ b/Client/svelte/src/routes/+page.svelte @@ -414,10 +414,10 @@ {/if} -
+
{#each pages as page (page.name)} {#if poll.v >= 0 && app.pageIsActive(page)} -
+
{#each page.modules || [] as mod (mod.uuid)} {/each} From d5cd2ed1a2e5b1bda03e2bacb85076718de4680a Mon Sep 17 00:00:00 2001 From: Yury Rodzikau Date: Fri, 18 Sep 2026 14:29:00 +0200 Subject: [PATCH 09/12] =?UTF-8?q?feat(ui-core,=20client-core,=20svelte-cli?= =?UTF-8?q?ent,=20vue2-client):=20machine-driven=20modal=20dialogs=20that?= =?UTF-8?q?=20the=20user=20cannot=20close,=20for=20the=20"Pausing=20Build?= =?UTF-8?q?=E2=80=A6"=20/=20"Canceling=20Build=E2=80=A6"=20wait=20screens?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit State machines cannot open dialogs and UI event handlers cannot set machine parameters, so a dialog could only be opened by a click and closed by the user. The pause/resume/cancel build controls need dialogs that main opens while it finishes the current layer, that cannot be dismissed, and that close by themselves once the machine is ready. This adds that generically; the ScanlabOCM machine uses it for "Pausing Build…" and "Canceling Build…". feat(ui-core): dialog attributes sync:active and closable - accepts `active` / `sync:active` and `closable` (default "1"), stored as CUIExpression in CUIDialog, validated at startup, passed through addDialog_Unsafe (signature extended). - /api/frontend emits "closable" for every dialog and "active" only for dialogs with an active expression, so event-driven dialogs stay untouched. Legacy /api/ui/state emits "closable" too. New keys AMC_API_KEY_UI_DIALOGACTIVE / AMC_API_KEY_UI_DIALOGCLOSABLE. - The server is authoritative: a dialog is open while the bound parameter is true and re-asserted on every frontend poll. feat(client-core): follow server-driven dialog state - AMCApplicationDialog initialises `closable` and `serverDriven` in the constructor (Vue 2 reactivity). - retrieveFrontendState() indexes dialog modules into frontendLookup and calls new _syncServerDrivenDialogs(), which opens a dialog via showDialog() when "active" turns true and clears dialogIsActive when it turns false. Dialogs without "active" keep the client-action driven behaviour. feat(svelte-client): non-closable dialogs - Dialog.Content gets showCloseButton, escapeKeydownBehavior and interactOutsideBehavior from dialog.closable; setDialogOpen() ignores close requests for closable="0"; dialogIsClosable() reads through the poll tick so the property mutation re-renders. feat(vue2-client): non-closable dialogs - v-dialog gets :persistent and :no-click-animation for closable === false. Backward compatible: dialogs without the new attributes behave as before. Signed-off-by: Yury Rodzikau --- Client/core/common/AMCApplication.js | 48 ++++++++++++++++++++++++ Client/core/common/AMCDialog.js | 7 ++++ Client/svelte/src/routes/+page.svelte | 18 ++++++++- Client/vue2/src/App.vue | 4 +- Implementation/API/amc_api_constants.hpp | 2 + Implementation/UI/amc_ui_dialog.cpp | 14 ++++++- Implementation/UI/amc_ui_dialog.hpp | 19 ++++++++-- Implementation/UI/amc_ui_handler.cpp | 24 ++++++++++-- Implementation/UI/amc_ui_handler.hpp | 2 +- 9 files changed, 127 insertions(+), 11 deletions(-) diff --git a/Client/core/common/AMCApplication.js b/Client/core/common/AMCApplication.js index 97195508..d9a7bf41 100644 --- a/Client/core/common/AMCApplication.js +++ b/Client/core/common/AMCApplication.js @@ -924,6 +924,20 @@ export default class AMCApplication extends Common.AMCObject { } } } + + // Dialog content is indexed too, so synced items inside a dialog refresh + // from the same payload instead of falling back to legacy polling. + if (resultJSON.data && resultJSON.data.dialogs) { + for (let dialog of resultJSON.data.dialogs) { + if (dialog.modules) { + for (let mod of dialog.modules) { + this._indexFrontendModule(mod); + } + } + } + + this._syncServerDrivenDialogs(resultJSON.data.dialogs); + } }) .catch(err => { this.API.unsuccessfulFrontendCounter = (this.API.unsuccessfulFrontendCounter || 0) + 1; @@ -1014,6 +1028,37 @@ export default class AMCApplication extends Common.AMCObject { return true; } + // Applies the server-side open state of dialogs that carry an "active" flag (dialogs + // with sync:active in the machine configuration). The server is authoritative: such a + // dialog is opened while the flag is true, even if a client action closed it in between, + // and closed as soon as the flag turns false. Dialogs without the flag are untouched. + _syncServerDrivenDialogs(dialogsJSON) { + let changed = false; + + for (let dialogJSON of dialogsJSON) { + if (typeof dialogJSON.active !== "boolean") + continue; + + let dialog = this.AppContent.DialogMap.get(dialogJSON.name); + if (!dialog) + continue; + + dialog.serverDriven = true; + dialog.closable = (dialogJSON.closable !== false); + + if (dialogJSON.active && !dialog.dialogIsActive && (dialog.visible !== false)) { + // showDialog already refreshes the content items. + this.showDialog(dialog.name); + } else if (!dialogJSON.active && dialog.dialogIsActive) { + dialog.dialogIsActive = false; + changed = true; + } + } + + if (changed) + this.updateContentItems(); + } + // Recursively index a v2 module (and its submodules) into frontendLookup by UUID. _indexFrontendModule(mod) { if (!mod || !mod.uuid) @@ -1491,6 +1536,9 @@ export default class AMCApplication extends Common.AMCObject { } } + // Only one dialog is open at a time. A server-driven dialog (see + // _syncServerDrivenDialogs) re-opens on the next frontend poll while its flag is + // set, so it wins over dialogs opened by client actions. showDialog(dialog) { this.closeAllDialogs(); diff --git a/Client/core/common/AMCDialog.js b/Client/core/common/AMCDialog.js index fdc62238..03ee82ad 100644 --- a/Client/core/common/AMCDialog.js +++ b/Client/core/common/AMCDialog.js @@ -41,6 +41,13 @@ export default class AMCApplicationDialog extends AMCApplicationPage { // dialog JSON); capture it so the client can render the dialog header. this.title = dialogJSON.title || ""; this.dialogIsActive = false; + + // "closable" (default true): a non-closable dialog has no close button and ignores + // Escape and outside clicks. "active" is only sent for server-driven dialogs; their + // open state follows the server on every frontend poll instead of client actions. + // Both are initialised here so Vue 2 tracks them. + this.closable = (dialogJSON.closable !== false); + this.serverDriven = (typeof dialogJSON.active === "boolean"); } // A dialog is never the application's active page, so the inherited page-based diff --git a/Client/svelte/src/routes/+page.svelte b/Client/svelte/src/routes/+page.svelte index 9df0874b..7208e024 100644 --- a/Client/svelte/src/routes/+page.svelte +++ b/Client/svelte/src/routes/+page.svelte @@ -266,10 +266,20 @@ if (open) { app.showDialog(dialog.name); } else { + // A non-closable dialog is only closed by the server (see + // AMCApplication._syncServerDrivenDialogs) or by a client action. + if (dialog.closable === false) return; dialog.dialogIsActive = false; bumpTick(); } } + + // Read through a poll-tick dependency: closable is a plain property mutation on a + // dialog object with stable identity, so it would not re-trigger on its own. + function dialogIsClosable (dialog: any): boolean { + poll.v; + return dialog.closable !== false; + } @@ -429,11 +439,17 @@ {#each dialogs as dialog (dialog.name)} + {@const closable = dialogIsClosable(dialog)} setDialogOpen(dialog, open)} > - + {#if dialog.title} {dialog.title} diff --git a/Client/vue2/src/App.vue b/Client/vue2/src/App.vue index 3ea316e4..8c9a5099 100644 --- a/Client/vue2/src/App.vue +++ b/Client/vue2/src/App.vue @@ -109,11 +109,13 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.