summaryrefslogtreecommitdiff
path: root/public/App.js
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 /public/App.js
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 'public/App.js')
-rw-r--r--public/App.js44
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));