summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-21 13:20:53 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-21 13:20:53 +0800
commit26bdd272e3d26040a8c9b64b302fd94d7b6be978 (patch)
tree3c0de18988b847f42c8b8ffdb6183a2774e8a993 /src
parentc332e2efa767c76037540a3fa1c649c9f2e0498e (diff)
downloadsomhairles-dream-fsharp-26bdd272e3d26040a8c9b64b302fd94d7b6be978.tar.gz
feat(viewport): orbit drag/zoom controls with idle auto-rotate pause; cache-busted script urls
Diffstat (limited to 'src')
-rw-r--r--src/SomhairlesDream.Frontend/App.fs42
-rw-r--r--src/SomhairlesDream.Frontend/Bindings.fs22
2 files changed, 57 insertions, 7 deletions
diff --git a/src/SomhairlesDream.Frontend/App.fs b/src/SomhairlesDream.Frontend/App.fs
index ec9e273..e6edd51 100644
--- a/src/SomhairlesDream.Frontend/App.fs
+++ b/src/SomhairlesDream.Frontend/App.fs
@@ -47,7 +47,14 @@ type ViewState =
Scene: ThreeNode
Camera: ThreeCamera
Group: ThreeNode
- Canvas: HTMLCanvasElement }
+ Canvas: HTMLCanvasElement
+ Controls: obj }
+
+// Auto-spin pauses while the visitor is dragging or zooming the model and
+// resumes a few seconds after the last interaction, so inspecting a stage
+// (dish, RTG, mast) is never fought by the idle animation.
+let mutable lastInteractionMs = 0.0
+let idleSpinDelayMs = 3500.0
type LoadOwner =
{ ProjectId: string
@@ -199,6 +206,14 @@ let frameObject (view: ViewState) (node: ThreeNode) =
camera.lookAt(0.0, 0.0, 0.0)
camera.updateProjectionMatrix ()
+ // Re-sync the orbit target after every auto-fit so drag/zoom
+ // pivots around the freshly centered model.
+ match view.Controls with
+ | null -> ()
+ | controls ->
+ setControlsTarget controls 0.0 0.0 0.0
+ updateControls controls
+
let resizeView (view: ViewState) =
let width = max 1.0 (float view.Canvas.clientWidth)
let height = max 1.0 (float view.Canvas.clientHeight)
@@ -230,7 +245,7 @@ let initializeView () =
scene.background <- createColor("#11171a")
let camera = createPerspectiveCamera(34.0, width / height, 0.1, 100.0)
camera.position.set(6.4, 4.6, 7.2) |> ignore
- camera.lookAt(0.0, 1.2, 0.0)
+ camera.lookAt(0.0, 0.0, 0.0)
let group = createGroup()
scene.add(group)
scene.add(createAmbientLight("#f0e6c8", 1.9))
@@ -238,23 +253,40 @@ let initializeView () =
keyLight.position.set(4.0, 7.0, 5.0) |> ignore
scene.add(keyLight)
+ let controls = createOrbitControls camera canvas
+ setControlsTarget controls 0.0 0.0 0.0
+ setControlsDamping controls true
+ setControlsAutoRotateSpeed controls 0.7
+
let view =
{ Renderer = renderer
Scene = scene
Camera = camera
Group = group
- Canvas = canvas }
+ Canvas = canvas
+ Controls = controls }
viewState <- Some view
+
freezeRotation <- (queryParam "freeze") = "1"
- exposeViewerDebug camera scene group canvas
+ setControlsAutoRotate controls (not freezeRotation)
+ exposeViewerDebug camera scene group canvas controls
renderFrame selectedFrame
window.addEventListener("resize", fun _ -> resizeView view)
- let rec animate (_: float) =
+ canvas.addEventListener("pointerdown", fun _ -> lastInteractionMs <- JS.Constructors.Date.now ())
+ canvas.addEventListener("wheel", fun _ -> lastInteractionMs <- JS.Constructors.Date.now ())
+ canvas.addEventListener("touchstart", fun _ -> lastInteractionMs <- JS.Constructors.Date.now ())
+
+ let rec animate (nowMs: float) =
+ updateControls controls
+
if not freezeRotation then
group.rotation.y <- group.rotation.y + 0.003
+ let interacting = nowMs - lastInteractionMs < idleSpinDelayMs
+ setControlsAutoRotate controls (not freezeRotation && not interacting)
+
renderer.render(scene, camera)
requestAnimationFrame animate |> ignore
diff --git a/src/SomhairlesDream.Frontend/Bindings.fs b/src/SomhairlesDream.Frontend/Bindings.fs
index e7c567d..19056a5 100644
--- a/src/SomhairlesDream.Frontend/Bindings.fs
+++ b/src/SomhairlesDream.Frontend/Bindings.fs
@@ -65,6 +65,24 @@ let createGLTFLoader () : obj = jsNative
[<Emit("$0.load($1, $2, null, $3)")>]
let loadGLTF (loader: obj) (url: string) (onLoad: GLTFScene -> unit) (onError: obj -> unit) : unit = jsNative
+[<Emit("new OrbitControls($0, $1)")>]
+let createOrbitControls (camera: obj) (canvas: obj) : obj = jsNative
+
+[<Emit("$0.target.copy({ x: $1, y: $2, z: $3 })")>]
+let setControlsTarget (controls: obj) (x: float) (y: float) (z: float) : unit = jsNative
+
+[<Emit("$0.enableDamping = $1")>]
+let setControlsDamping (controls: obj) (enabled: bool) : unit = jsNative
+
+[<Emit("$0.autoRotate = $1")>]
+let setControlsAutoRotate (controls: obj) (auto: bool) : unit = jsNative
+
+[<Emit("$0.autoRotateSpeed = $1")>]
+let setControlsAutoRotateSpeed (controls: obj) (speed: float) : unit = jsNative
+
+[<Emit("$0.update()")>]
+let updateControls (controls: obj) : unit = jsNative
+
[<Emit("new URLSearchParams(window.location.search).get($0)")>]
let queryParam (name: string) : string = jsNative
@@ -100,8 +118,8 @@ let createBox3 () : ThreeBox3 = jsNative
[<Emit("new THREE.Vector3()")>]
let createVector3 () : ThreeVector = jsNative
-[<Emit("window.SOMHAIRLES_VIEWER = { camera: $0, scene: $1, group: $2, canvas: $3 }")>]
-let exposeViewerDebug (camera: ThreeCamera) (scene: ThreeNode) (group: ThreeNode) (canvas: obj) : unit = jsNative
+[<Emit("window.SOMHAIRLES_VIEWER = { camera: $0, scene: $1, group: $2, canvas: $3, controls: $4 }")>]
+let exposeViewerDebug (camera: ThreeCamera) (scene: ThreeNode) (group: ThreeNode) (canvas: obj) (controls: obj) : unit = jsNative
[<Emit("var child; while ((child = $0.firstChild)) { $0.removeChild(child); }")>]
let clearChildren (node: obj) : unit = jsNative