From 374afdcd7172402a0be0371964fd6d4040610d5a Mon Sep 17 00:00:00 2001 From: "Somhairle H. Marisol" Date: Mon, 21 Sep 2026 20:31:37 +0800 Subject: feat(ui): loading overlay with instant mesh swap, no-store GLB, full-width viewport, ground grid, drop masthead title --- public/App.js | 25 ++++++++++++++++++++++++- public/index.html | 48 ++++++++---------------------------------------- public/styles.css | 45 ++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 76 insertions(+), 42 deletions(-) (limited to 'public') diff --git a/public/App.js b/public/App.js index eb47dbc..7aac7f0 100644 --- a/public/App.js +++ b/public/App.js @@ -330,6 +330,9 @@ export function initializeView() { scene.add(rimLight); const bounceLight = new THREE.HemisphereLight("#3d4a56", "#141a1e", 0.9); scene.add(bounceLight); + const grid = new THREE.GridHelper(24, 48, "#5a4a2a", "#2a2f33"); + grid.position.y = -1.35; + scene.add(grid); const controls = new OrbitControls(camera, canvas); controls.target.copy({ x: 0, y: 0, z: 0 }); controls.enableDamping = true; @@ -430,12 +433,17 @@ export function startStepLoad(view, owner, historical) { viewMode(new ViewMode(1, [])); failedLoad(undefined); setNoteRetryable(false); + clearGroup(view.Group); + displayedNode(undefined); + loadedStep(undefined); + document.getElementById("viewport-loading").classList.toggle('is-hidden', false); const loader = new GLTFLoader(); const projectId = encodeURIComponent(owner.ProjectId); const runId = encodeURIComponent(owner.RunId); + const stamp = ~~(Date.now() % 100000000) | 0; let url; const arg_2 = encodeURIComponent(owner.Path); - url = toText(printf("/api/artifacts/steps?projectId=%s&runId=%s&path=%s"))(projectId)(runId)(arg_2); + url = toText(printf("/api/artifacts/steps?projectId=%s&runId=%s&path=%s&ts=%d"))(projectId)(runId)(arg_2)(stamp); if (historical) { setText("viewport-note", toText(printf("历史几何加载中 · %s"))(owner.Path)); } @@ -450,6 +458,7 @@ export function startStepLoad(view, owner, historical) { displayedNode(gltf.scene); loadedStep(owner); frameObject(view, gltf.scene); + document.getElementById("viewport-loading").classList.toggle('is-hidden', true); view.Renderer.render(view.Scene, view.Camera); if (historical) { setText("viewport-note", toText(printf("历史几何 · %s"))(owner.Path)); @@ -466,6 +475,7 @@ export function startStepLoad(view, owner, historical) { pendingLoad(undefined); loadedStep(undefined); failedLoad([owner, historical]); + document.getElementById("viewport-loading").classList.toggle('is-hidden', true); setNoteRetryable(true); if (historical) { setText("viewport-note", toText(printf("历史几何加载失败 · %s"))(owner.Path)); @@ -965,10 +975,14 @@ export function loadHistoryFor(selection) { } } else { + document.getElementById("viewport-loading").classList.toggle('is-hidden', true); + setText("viewport-note", "该运行尚无可用历史几何"); return singleton.Zero(); } }) : singleton.Zero(); })), (_arg_2) => { + document.getElementById("viewport-loading").classList.toggle('is-hidden', true); + setText("viewport-note", "历史几何加载失败 · 清单不可用"); return singleton.Zero(); }))); } @@ -1032,6 +1046,15 @@ export function selectModel(card) { activeModel(selection); highlightActiveCard(selection); setText("run-message", "载入模型历史"); + if (viewState() == null) { + } + else { + clearGroup(viewState().Group); + displayedNode(undefined); + loadedStep(undefined); + } + document.getElementById("viewport-loading").classList.toggle('is-hidden', false); + setText("viewport-note", "历史几何加载中"); loadHistoryFor(selection); } diff --git a/public/index.html b/public/index.html index 267d21b..d58853c 100644 --- a/public/index.html +++ b/public/index.html @@ -5,7 +5,7 @@ Somhairle's Dream · 实时重建台 - + -
-
-
- SOMHAIRLE'S DREAM / CONTROL SURFACE -

实时重建台

-
+
+
实时链路 · 等待启动 @@ -102,41 +98,13 @@
实时几何 · V00
+
-
@@ -179,6 +147,6 @@ heritage 参考线保留占位演示 · F# / OPEN CODE
- + diff --git a/public/styles.css b/public/styles.css index d6cfbb1..bd4e3be 100644 --- a/public/styles.css +++ b/public/styles.css @@ -67,6 +67,10 @@ button:focus-visible { justify-content: space-between; } +.masthead-compact { + justify-content: flex-end; +} + .masthead { gap: 24px; padding-bottom: 30px; @@ -161,7 +165,7 @@ button:focus-visible { .workspace { display: grid; - grid-template-columns: minmax(200px, 0.72fr) minmax(360px, 1.8fr) minmax(220px, 0.82fr); + grid-template-columns: minmax(200px, 0.68fr) minmax(420px, 2.4fr); gap: 12px; } @@ -780,3 +784,42 @@ button:focus-visible { cursor: pointer; pointer-events: auto; } + +/* ---- viewport loading overlay ---- */ +.viewport-loading { + position: absolute; + inset: 0; + display: grid; + place-items: center; + gap: 10px; + align-content: center; + background: rgba(8, 11, 13, 0.55); + backdrop-filter: blur(2px); + z-index: 3; +} + +.viewport-loading.is-hidden { + display: none; +} + +.loading-ring { + width: 42px; + height: 42px; + border: 2px solid rgba(210, 175, 95, 0.18); + border-top-color: #d2af5f; + border-radius: 50%; + animation: loading-spin 0.9s linear infinite; +} + +.loading-text { + color: var(--muted); + font-family: "Fira Code", "SFMono-Regular", Consolas, monospace; + font-size: 0.62rem; + letter-spacing: 0.08em; +} + +@keyframes loading-spin { + to { + transform: rotate(360deg); + } +} -- cgit v1.2.3