<!-- SOLUV · part 667 -->
 to wall 1. /soluv/N = straight into wall N.
  const startView = qs.get('view');
  const atSpot = (qs.get('at') || '').split(',').map(Number);
  if (atSpot.length === 6 && atSpot.every(Number.isFinite)) {   // back from logging in: same spot as before
    [st.tx, st.ty, st.tz, st.dist, st.yaw, st.pitch] = atSpot; applyCamera();
    const q = new URLSearchParams(location.search); q.delete('at'); history.replaceState(null, '', location.pathname + (q.toString() ? '?' + q : ''));
  }
  else if (startView && views[startView]) {
    Object.assign(st, views[startView]()); applyCamera();
    // mid / near / live need the wall's lines: re-aim once the room has loaded
    const reaim = () => { if (current.wall.info().lines > 0 || current.wall.status.state !== 'loading') { current.removeEventListener('status', reaim); if (!anim && !glide && !userMoved) { Object.assign(st, views[startView]()); applyCamera(); } } };
    current.addEventListener('status', reaim);
  }
  else if (deep) { Object.assign(st, views.wide()); applyCamera(); setTimeout(() => { if (!userMoved) flyTo(views.far(), 1600, true); }, 250); }
  else { Object.assign(st, views.map()); applyCamera(); setTimeout(() => { if (!userMoved) flyTo(views.far(), 2600, true); }, 500); }
  refreshStatus(); refreshMe();
  requestAnimationFrame(tick);

  const api = {
    get placing() { return place ? { cells: place.cells.slice(), plan: place.plan || null } : null; }, placeTap: (x, y) => placeTap(x, y), togglePlace: () => togglePlace(),
    get placed() { return walls.filter(w => w.userData.placed).map(w => w.userData.placed); },
    THREE, scene, camera, renderer, walls, views, MINIMUM, get current() { return current; }, deep, pillars,
    setView(v) { userMoved = true; stopMotion(); Object.assign(st, typeof v === 'function' ? v() : v); applyCamera(); },
    get view() { return { ...st }; }, flyTo, stopMotion, startGlide, get gliding() { return !!glide; },
    get frames() { return frames; }, readyMs: Math.round(performance.now() - t0),
    async settle(ms = 6000) { // keep rendering until every wall has loaded and drawn what this view needs
      const end = performance.now() + ms;
      while (performance.now() < end) { need = true; await new Promise(r => setTimeout(r, 150)); if (walls.every(w => { const t = w.wall.thrift(); return t.pending === 0 && w.wall.status.state !== 'loading' && w.wall.status.state !== 'idle' || !t.started && t.wallPpe < 0.8; })) { need = true; await new Promise(r => setTimeout(r, 300)); return true; } }
      return false;
    },
    lawCheck() {
      scene.updateMatrixWorld(true);
      const box = new THREE.Box3(), size = new THREE.Vector3(); let minObject = null; const violations = [];
      scene.traverse(o => { if (!o.isMesh || o.userData.text) return; box.setFromObject(o); box.getSize(size); const m = Math.min(size.x, size.y, size.z); const rec = { name: o.name || o.parent && o.parent.name, min: +m.toFixed(4) }; if (!minObject || m < minObject.min) minObject = rec; if (m < MINIMUM - 1e-6) violations.push(rec); });
      const wl = walls.map(w => w.wall.lawCheck());
      const glyph = Math.min(...wl.map(x => x.minGlyphEm));
      const av = walls[0].children.find(c => c.name.startsWith('avatar'));
      let avatarHeight = null, avatarEye = null;
      if (av) { box.setFromObject(av); box.getSize(size); avatarHeight = +size.y.toFixed(4); const eye = av.children.find(c => c.name.startsWith('eye')); box.setFromObject(eye); box.getSize(size); avatarEye = +Math.min(size.x, size.y, size.z).toFixed(4); }
      return { minimum: MINIMUM, smallest: +Math.min(minObject.min, glyph).toFixed(4), minObject, minGlyphEm: glyph, lineHeightWorld: wl[0].lineHeightWorld, avatarHeight, avatarEye, avatarInMinimums: avatarHeight && +(avatarHeight / MINIMUM).toFixed(2), violations: violations.concat(...wl.map(x => x.violations)) };
    },
    thrift() {
      const res = performance.getEntriesByType('resource');
      const sum = f => res.filter(f).reduce((a, r) => a + (r.transferSize || r.encodedBodySize || 0), 0);
      return { networkBytes: sum(() => true) + (performance.getEntriesByType('navigation')[0]?.transferSize || 0), threeBytes: sum(r => /three/.test(r.name)), fontBytes: sum(r => /woff2/.test(r.name)), roomApiBytes: sum(r => /api\/soluv-room/.test(r.name)), walls: walls.map(w => w.wall.thrift()), heapMB: performance.memory ? +(performance.memory.usedJSHeapSize / 1048576).toFixed(1) : null };
    },
    /* overlap check: every visible text surface of every wall, projected to the screen, must not intersect another */
    overlaps() {
      const out = { checked: 0, overlaps: [] }; const v = new THREE.Vector3();
      for (const w of walls) {
        const rects = w.wall.tileRects().filter(r => r.mesh.visible);
        out.checked += rects.length;
        for (let i = 0; i < rects.length; i++) for (let j = i + 1; j < rects.length; j++) { const a = rects[i], b = rects[j]; if (a.x0 < b.x1 - 1e-6 && b.x0 < a.x1 - 1e-6 && a.