summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/SomhairlesDream.Frontend/App.fs38
-rw-r--r--src/SomhairlesDream.Frontend/Bindings.fs9
-rw-r--r--src/SomhairlesDream.Server/ArtifactRunApi.fs3
3 files changed, 48 insertions, 2 deletions
diff --git a/src/SomhairlesDream.Frontend/App.fs b/src/SomhairlesDream.Frontend/App.fs
index 315a0fc..3665c43 100644
--- a/src/SomhairlesDream.Frontend/App.fs
+++ b/src/SomhairlesDream.Frontend/App.fs
@@ -269,6 +269,12 @@ let initializeView () =
let bounceLight = createHemisphereLight("#3d4a56", "#141a1e", 0.9)
scene.add(bounceLight)
+ // Coordinate ground grid so the model reads as anchored in a workspace
+ // instead of floating in a void.
+ let grid = createGridHelper (24.0, 48, "#5a4a2a", "#2a2f33")
+ setNodePositionY (grid, -1.35)
+ scene.add(grid)
+
let controls = createOrbitControls camera canvas
setControlsTarget controls 0.0 0.0 0.0
setControlsDamping controls true
@@ -361,10 +367,18 @@ let startStepLoad (view: ViewState) (owner: LoadOwner) (historical: bool) =
viewMode <- StepMode
failedLoad <- None
setNoteRetryable false
+ // Swap the viewport to a loading state immediately: drop the previous
+ // mesh so the old geometry never lingers under the spinner.
+ clearGroup view.Group
+ displayedNode <- None
+ loadedStep <- None
+ toggleHidden ("viewport-loading", false)
let loader = createGLTFLoader ()
let projectId = JS.encodeURIComponent owner.ProjectId
let runId = JS.encodeURIComponent owner.RunId
- let url = sprintf "/api/artifacts/steps?projectId=%s&runId=%s&path=%s" projectId runId (JS.encodeURIComponent owner.Path)
+ // Cache-buster: the GLB is always re-fetched from the server.
+ let stamp = int (JS.Constructors.Date.now () % 100000000.0)
+ let url = sprintf "/api/artifacts/steps?projectId=%s&runId=%s&path=%s&ts=%d" projectId runId (JS.encodeURIComponent owner.Path) stamp
if historical then
setText "viewport-note" (sprintf "历史几何加载中 · %s" owner.Path)
@@ -380,6 +394,7 @@ let startStepLoad (view: ViewState) (owner: LoadOwner) (historical: bool) =
displayedNode <- Some gltf.scene
loadedStep <- Some owner
frameObject view gltf.scene
+ toggleHidden ("viewport-loading", true)
view.Renderer.render(view.Scene, view.Camera)
if historical then
@@ -393,6 +408,7 @@ let startStepLoad (view: ViewState) (owner: LoadOwner) (historical: bool) =
pendingLoad <- None
loadedStep <- None
failedLoad <- Some(owner, historical)
+ toggleHidden ("viewport-loading", true)
setNoteRetryable true
if historical then
@@ -790,8 +806,15 @@ let loadHistoryFor (selection: RunSelection) =
match viewState with
| Some view -> loadStepArtifact view steps[steps.Length - 1] true
| None -> ()
+ else
+ // Manifest answered but nothing is replayable: close the
+ // spinner instead of leaving it spinning forever.
+ toggleHidden ("viewport-loading", true)
+ setText "viewport-note" "该运行尚无可用历史几何"
with _ ->
- ()
+ // Manifest unreachable: fail honestly rather than spin forever.
+ toggleHidden ("viewport-loading", true)
+ setText "viewport-note" "历史几何加载失败 · 清单不可用"
}
|> Async.StartImmediate
let startRun (selection: RunSelection) =
@@ -859,6 +882,17 @@ let selectModel (card: HTMLElement) =
activeModel <- selection
highlightActiveCard selection
setText "run-message" "载入模型历史"
+ // Enter the loading state before the manifest round-trip: the previous
+ // mesh is dropped and the spinner appears the instant a card is clicked.
+ (match viewState with
+ | Some view ->
+ clearGroup view.Group
+ displayedNode <- None
+ loadedStep <- None
+ | None -> ())
+
+ toggleHidden ("viewport-loading", false)
+ setText "viewport-note" "历史几何加载中"
loadHistoryFor selection
let boot () =
diff --git a/src/SomhairlesDream.Frontend/Bindings.fs b/src/SomhairlesDream.Frontend/Bindings.fs
index b6c4353..7f0ce31 100644
--- a/src/SomhairlesDream.Frontend/Bindings.fs
+++ b/src/SomhairlesDream.Frontend/Bindings.fs
@@ -163,6 +163,15 @@ let setToneMappingACES (renderer: obj) : unit = jsNative
[<Emit("$0.toneMappingExposure = $1")>]
let setToneMappingExposure (renderer: obj, exposure: float) : unit = jsNative
+[<Emit("new THREE.GridHelper($0, $1, $2, $3)")>]
+let createGridHelper (size: float, divisions: int, centerColor: string, edgeColor: string) : ThreeNode = jsNative
+
+[<Emit("$0.position.y = $1")>]
+let setNodePositionY (node: ThreeNode, y: float) : unit = jsNative
+
+[<Emit("document.getElementById($0).classList.toggle('is-hidden', $1)")>]
+let toggleHidden (id: string, hidden: bool) : unit = jsNative
+
[<Emit("$0.outputColorSpace = THREE.SRGBColorSpace")>]
let setSRGBOutput (renderer: obj) : unit = jsNative
diff --git a/src/SomhairlesDream.Server/ArtifactRunApi.fs b/src/SomhairlesDream.Server/ArtifactRunApi.fs
index 6c00d66..3a29d99 100644
--- a/src/SomhairlesDream.Server/ArtifactRunApi.fs
+++ b/src/SomhairlesDream.Server/ArtifactRunApi.fs
@@ -129,6 +129,9 @@ module ArtifactRunApi =
| Error message, _ -> error options 400 message
| _, None -> error options 400 "missing query parameter 'path'"
| Ok(projectId, runId), Some relativePath ->
+ // Step geometry must always be re-fetched: browsers and CDNs
+ // never cache GLB responses from this endpoint.
+ context.Response.Headers.["Cache-Control"] <- "no-store"
match options.Coordinator.StepArtifact(projectId, runId, relativePath) with
| Ok file -> streamArtifact context file
| Error RunNotFound -> error options 404 "run not found"