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 /public | |
| 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 'public')
| -rw-r--r-- | public/App.js | 44 |
1 files changed, 43 insertions, 1 deletions
diff --git a/public/App.js b/public/App.js index c5a5418..f953832 100644 --- a/public/App.js +++ b/public/App.js @@ -58,6 +58,8 @@ export let currentRunId = createAtom(""); export let loadedStepPath = createAtom(undefined); +export let displayedNode = createAtom(undefined); + export let freezeRotation = createAtom(false); export function element(id) { @@ -173,6 +175,7 @@ export function addMesh(view, frame) { export function renderFrame(frame) { selectedFrame(frame); + displayedNode(undefined); if (viewState() != null) { const view = viewState(); view.Group.clear(); @@ -181,11 +184,46 @@ export function renderFrame(frame) { } } +export function frameObject(view, node) { + const box = new THREE.Box3(); + box.setFromObject(node); + if (!box.isEmpty()) { + const center = new THREE.Vector3(); + const size = new THREE.Vector3(); + box.getCenter(center); + box.getSize(size); + const radius = 0.5 * size.length(); + if (radius > 1E-06) { + node.position.set(-center.x, -center.y, -center.z); + const camera = view.Camera; + const aspect = max(1, view.Canvas.clientWidth) / max(1, view.Canvas.clientHeight); + camera.aspect = aspect; + const fovY = (camera.fov * 3.141592653589793) / 180; + const limitingFov = min(fovY, 2 * Math.atan(Math.tan(fovY / 2) * aspect)); + const distance = (1.28 * radius) / Math.sin(limitingFov / 2); + const norm = Math.sqrt(((0.62 * 0.62) + (0.45 * 0.45)) + (0.7 * 0.7)); + camera.position.set((0.62 / norm) * distance, (0.45 / norm) * distance, (0.7 / norm) * distance); + camera.near = max(distance * 0.01, distance - (radius * 2.5)); + camera.far = (distance + (radius * 4)); + camera.lookAt(0, 0, 0); + camera.updateProjectionMatrix(); + } + } +} + export function resizeView(view) { const width = max(1, view.Canvas.clientWidth); const height = max(1, view.Canvas.clientHeight); view.Renderer.setSize(width, height, false); - renderFrame(selectedFrame()); + view.Camera.aspect = (width / height); + view.Camera.updateProjectionMatrix(); + if (displayedNode() == null) { + renderFrame(selectedFrame()); + } + else { + frameObject(view, displayedNode()); + view.Renderer.render(view.Scene, view.Camera); + } } export function initializeView() { @@ -213,6 +251,7 @@ export function initializeView() { const view = new ViewState(renderer, scene, camera, group, canvas); viewState(view); freezeRotation((new URLSearchParams(window.location.search).get("freeze")) === "1"); + window.SOMHAIRLES_VIEWER = { camera: camera, scene: scene, group: group, canvas: canvas }; renderFrame(selectedFrame()); window.addEventListener("resize", (_arg) => { resizeView(view); @@ -262,6 +301,9 @@ export function loadStepArtifact(view, stepPath) { loader.load(url, ((gltf) => { view.Group.clear(); view.Group.add(gltf.scene); + displayedNode(gltf.scene); + frameObject(view, gltf.scene); + view.Renderer.render(view.Scene, view.Camera); setText("viewport-note", toText(printf("管线几何 · %s"))(stepPath)); }), null, ((_arg) => { setText("viewport-note", toText(printf("管线几何加载失败 · %s"))(stepPath)); |
