Преглед изворни кода

fix(viewer): clearance-based spawn facing the scene (no more camera buried in geometry)

resetPlayer spawned blindly at the horizontal centre → for object-centric captures
the camera started inside the mesh (dark close-up). chooseSpawn() now samples floor
points across the scene, keeps the standing spot with the most horizontal clearance,
and yaws toward the scene centre with a slight downward tilt. Bump SW cache v1→v2 so
clients pick up the new viewer.js/app.js shell.
Oivo Agent пре 1 недеља
родитељ
комит
8a5f9b7797
2 измењених фајлова са 53 додато и 11 уклоњено
  1. 1 1
      frontend/sw.js
  2. 52 10
      frontend/viewer.js

+ 1 - 1
frontend/sw.js

@@ -1,5 +1,5 @@
 // Offline app-shell cache. API calls are never cached.
-const CACHE = 'genrecon-v1';
+const CACHE = 'genrecon-v2';   // bump on shell change (app.js/viewer.js) so clients refresh
 const SHELL = [
   './', './index.html', './styles.css', './app.js', './viewer.js', './manifest.webmanifest',
   './demo.glb', './icons/icon-192.png', './icons/icon-512.png',

+ 52 - 10
frontend/viewer.js

@@ -10,6 +10,7 @@ import { Capsule } from './vendor/jsm/math/Capsule.js';
 
 let renderer, scene, camera, raf, container, clock;
 let octree, collider, startPoint, params;
+let startYaw = 0, startPitch = 0;
 const velocity = new THREE.Vector3();
 const dir = new THREE.Vector3();
 let onFloor = false;
@@ -88,16 +89,16 @@ export async function startViewer(glbUrl) {
   octree = new Octree();
   octree.fromGraphNode(model);
 
-  // Spawn STANDING on the floor: raycast straight down through the scene centre.
-  // (Dropping from the ceiling tunnels through thin reconstructed floors.)
-  const ray = new THREE.Raycaster(
-    new THREE.Vector3(center.x, box.max.y + diag * 0.1, center.z),
-    new THREE.Vector3(0, -1, 0),
-  );
-  const hit = ray.intersectObject(model, true)[0];
-  const floorY = hit ? hit.point.y : box.min.y;
-  startPoint = new THREE.Vector3(center.x, floorY + params.eye, center.z);
-  window.__params = Object.assign({ diag, floorY }, params);
+  // Spawn STANDING on the floor at the most OPEN spot, facing the scene. Blindly
+  // spawning at the horizontal centre buries the camera inside geometry for
+  // object-centric captures (a bonsai, a statue…) → a dark close-up. Sample the
+  // floor and pick the standing position with the most horizontal clearance.
+  const spawn = chooseSpawn(model, box, size, center, diag, params.eye);
+  startPoint = spawn.point;
+  startYaw = spawn.yaw;
+  startPitch = -0.12;                 // a gentle downward tilt to frame the scene
+  window.__params = Object.assign(
+    { diag, floorY: spawn.floorY, clearance: Math.round(spawn.clr * 1000) / 1000 }, params);
   resetPlayer();
 
   window.addEventListener('resize', onResize);
@@ -109,6 +110,46 @@ export async function startViewer(glbUrl) {
   animate();
 }
 
+// Find a good first-person spawn: sample floor points across the scene, keep the
+// one with the most open horizontal clearance (so the camera isn't wedged in a
+// wall), and face it toward the scene's centre of geometry. Falls back to the
+// scene centre if nothing samples cleanly.
+function chooseSpawn(model, box, size, center, diag, eye) {
+  const ray = new THREE.Raycaster();
+  const down = new THREE.Vector3(0, -1, 0);
+  const dirs = [];
+  for (let i = 0; i < 8; i++) { const a = (i / 8) * Math.PI * 2; dirs.push(new THREE.Vector3(Math.cos(a), 0, Math.sin(a))); }
+  const N = 4, inset = 0.18;
+  const cands = [];
+  const floorAt = (x, z) => {
+    ray.set(new THREE.Vector3(x, box.max.y + diag * 0.1, z), down);
+    const h = ray.intersectObject(model, true)[0];
+    return h ? h.point.y : null;
+  };
+  const sample = (x, z) => {
+    const fy = floorAt(x, z);
+    if (fy === null) return;
+    const p = new THREE.Vector3(x, fy + eye, z);
+    let clr = diag;
+    for (const d of dirs) { ray.set(p, d); const h = ray.intersectObject(model, true)[0]; if (h) clr = Math.min(clr, h.distance); }
+    cands.push({ point: p, floorY: fy, clr });
+  };
+  for (let ix = 0; ix <= N; ix++) for (let iz = 0; iz <= N; iz++) {
+    sample(box.min.x + size.x * (inset + (1 - 2 * inset) * ix / N),
+           box.min.z + size.z * (inset + (1 - 2 * inset) * iz / N));
+  }
+  sample(center.x, center.z);
+  if (!cands.length) {
+    const fy = floorAt(center.x, center.z);
+    return { point: new THREE.Vector3(center.x, (fy ?? box.min.y) + eye, center.z), yaw: 0, floorY: fy ?? box.min.y, clr: 0 };
+  }
+  cands.sort((a, b) => b.clr - a.clr);
+  const best = cands[0];
+  const dx = center.x - best.point.x, dz = center.z - best.point.z;
+  best.yaw = (Math.abs(dx) + Math.abs(dz) < diag * 1e-3) ? 0 : Math.atan2(-dx, -dz);
+  return best;
+}
+
 function resetPlayer() {
   collider = new Capsule(
     startPoint.clone().add(new THREE.Vector3(0, params.radius, 0)),
@@ -117,6 +158,7 @@ function resetPlayer() {
   );
   velocity.set(0, 0, 0);
   camera.position.copy(collider.end);
+  camera.rotation.set(startPitch, startYaw, 0);   // face the scene, not a wall
 }
 export function recenter() { if (collider) resetPlayer(); }