diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-21 02:03:45 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-21 02:07:13 +0800 |
| commit | dd75aaf6e82799ce88d8e66b01a3d53ba05301c9 (patch) | |
| tree | ef1601be2954f7c995f5fd47544241b457a7791e /src | |
| parent | 7e7166e9a7dfe6d619dbb2d3baa54ee97dfce1b9 (diff) | |
| download | somhairles-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.fs | 48 | ||||
| -rw-r--r-- | src/SomhairlesDream.Frontend/Bindings.fs | 28 |
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 |
