summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-21 02:03:45 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-21 02:07:13 +0800
commitdd75aaf6e82799ce88d8e66b01a3d53ba05301c9 (patch)
treeef1601be2954f7c995f5fd47544241b457a7791e /src
parent7e7166e9a7dfe6d619dbb2d3baa54ee97dfce1b9 (diff)
downloadsomhairles-dream-fsharp-dd75aaf6e82799ce88d8e66b01a3d53ba05301c9.tar.gz
feat(viewer): fit camera to loaded model and add checked-in browser acceptance driver
- add three.js Box3/Camera bindings and bbox-driven camera framing (fov-fit with margin, recenters the model so group rotation orbits in place); refit on canvas resize instead of reverting to replay placeholder geometry - expose window.SOMHAIRLES_VIEWER debug handle for numeric verification - add tools/acceptance/e2e_acceptance.py: self-provisioning playwright driver (isolated port/artifact root, unique run ids) asserting framing, frozen-screenshot determinism, GLB hash equality, failure surfacing - gitignore scratch/ and tools/acceptance/deps/; document the clean-checkout verification sequence (fable before build)
Diffstat (limited to 'src')
-rw-r--r--src/SomhairlesDream.Frontend/App.fs48
-rw-r--r--src/SomhairlesDream.Frontend/Bindings.fs28
2 files changed, 73 insertions, 3 deletions
diff --git a/src/SomhairlesDream.Frontend/App.fs b/src/SomhairlesDream.Frontend/App.fs
index fa52a9f..64fb61f 100644
--- a/src/SomhairlesDream.Frontend/App.fs
+++ b/src/SomhairlesDream.Frontend/App.fs
@@ -33,7 +33,7 @@ type RunSelection =
type ViewState =
{ Renderer: ThreeRenderer
Scene: ThreeNode
- Camera: ThreeNode
+ Camera: ThreeCamera
Group: ThreeNode
Canvas: HTMLCanvasElement }
@@ -46,6 +46,7 @@ let mutable eventSource: EventSource option = None
let mutable currentProjectId = ""
let mutable currentRunId = ""
let mutable loadedStepPath: string option = None
+let mutable displayedNode: ThreeNode option = None
let mutable freezeRotation = false
let element id = document.getElementById(id)
@@ -112,6 +113,7 @@ let addMesh (view: ViewState) (frame: ReplayFrame) =
let renderFrame (frame: ReplayFrame) =
selectedFrame <- frame
+ displayedNode <- None
match viewState with
| None -> ()
@@ -120,11 +122,49 @@ let renderFrame (frame: ReplayFrame) =
addMesh view frame
view.Renderer.render(view.Scene, view.Camera)
+let frameObject (view: ViewState) (node: ThreeNode) =
+ let box = createBox3 ()
+ box.setFromObject node |> ignore
+
+ if not (box.isEmpty ()) then
+ let center = createVector3 ()
+ let size = createVector3 ()
+ box.getCenter center |> ignore
+ box.getSize size |> ignore
+ let radius = 0.5 * size.length ()
+
+ if radius > 0.000001 then
+ node.position.set(-center.x, -center.y, -center.z) |> ignore
+ let camera = view.Camera
+ let width = max 1.0 (float view.Canvas.clientWidth)
+ let height = max 1.0 (float view.Canvas.clientHeight)
+ let aspect = width / height
+ camera.aspect <- aspect
+ let fovY = camera.fov * Math.PI / 180.0
+ let fovX = 2.0 * atan (tan (fovY / 2.0) * aspect)
+ let limitingFov = min fovY fovX
+ let distance = 1.28 * radius / sin (limitingFov / 2.0)
+ let dirX, dirY, dirZ = 0.62, 0.45, 0.70
+ let norm = sqrt (dirX * dirX + dirY * dirY + dirZ * dirZ)
+ camera.position.set(dirX / norm * distance, dirY / norm * distance, dirZ / norm * distance)
+ |> ignore
+ camera.near <- max (distance * 0.01) (distance - radius * 2.5)
+ camera.far <- distance + radius * 4.0
+ camera.lookAt(0.0, 0.0, 0.0)
+ camera.updateProjectionMatrix ()
+
let resizeView (view: ViewState) =
let width = max 1.0 (float view.Canvas.clientWidth)
let height = max 1.0 (float view.Canvas.clientHeight)
view.Renderer.setSize(width, height, false)
- renderFrame selectedFrame
+ view.Camera.aspect <- width / height
+ view.Camera.updateProjectionMatrix()
+
+ match displayedNode with
+ | Some node ->
+ frameObject view node
+ view.Renderer.render(view.Scene, view.Camera)
+ | None -> renderFrame selectedFrame
let initializeView () =
let canvas = document.getElementById("viewport-canvas") :?> HTMLCanvasElement
@@ -161,6 +201,7 @@ let initializeView () =
viewState <- Some view
freezeRotation <- (queryParam "freeze") = "1"
+ exposeViewerDebug camera scene group canvas
renderFrame selectedFrame
window.addEventListener("resize", fun _ -> resizeView view)
@@ -204,6 +245,9 @@ let loadStepArtifact (view: ViewState) (stepPath: string) =
(fun gltf ->
view.Group.clear()
view.Group.add gltf.scene
+ displayedNode <- Some gltf.scene
+ frameObject view gltf.scene
+ view.Renderer.render(view.Scene, view.Camera)
setText "viewport-note" (sprintf "管线几何 · %s" stepPath))
(fun _ -> setText "viewport-note" (sprintf "管线几何加载失败 · %s" stepPath))
diff --git a/src/SomhairlesDream.Frontend/Bindings.fs b/src/SomhairlesDream.Frontend/Bindings.fs
index 9dbfa00..a0276cc 100644
--- a/src/SomhairlesDream.Frontend/Bindings.fs
+++ b/src/SomhairlesDream.Frontend/Bindings.fs
@@ -8,6 +8,7 @@ type ThreeVector =
abstract x: float with get, set
abstract y: float with get, set
abstract z: float with get, set
+ abstract length: unit -> float
[<Erase>]
type ThreeNode =
@@ -19,6 +20,22 @@ type ThreeNode =
abstract background: obj with get, set
[<Erase>]
+type ThreeBox3 =
+ abstract setFromObject: node: ThreeNode -> ThreeBox3
+ abstract isEmpty: unit -> bool
+ abstract getCenter: target: ThreeVector -> ThreeVector
+ abstract getSize: target: ThreeVector -> ThreeVector
+
+[<Erase>]
+type ThreeCamera =
+ inherit ThreeNode
+ abstract fov: float with get, set
+ abstract aspect: float with get, set
+ abstract near: float with get, set
+ abstract far: float with get, set
+ abstract updateProjectionMatrix: unit -> unit
+
+[<Erase>]
type ThreeRenderer =
abstract setPixelRatio: ratio: float -> unit
abstract setSize: width: float * height: float * updateStyle: bool -> unit
@@ -71,7 +88,16 @@ let createScene () : ThreeNode = jsNative
let createGroup () : ThreeNode = jsNative
[<Emit("new THREE.PerspectiveCamera($0, $1, $2, $3)")>]
-let createPerspectiveCamera (fieldOfView: float, aspect: float, nearPlane: float, farPlane: float) : ThreeNode = jsNative
+let createPerspectiveCamera (fieldOfView: float, aspect: float, nearPlane: float, farPlane: float) : ThreeCamera = jsNative
+
+[<Emit("new THREE.Box3()")>]
+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("new THREE.WebGLRenderer($0)")>]
let createRenderer (options: obj) : ThreeRenderer = jsNative