diff --git a/public/js/exhibit.js b/public/js/exhibit.js index 5dbd805..8680053 100644 --- a/public/js/exhibit.js +++ b/public/js/exhibit.js @@ -5,7 +5,7 @@ */ import * as THREE from 'three'; import { createDetector, detectMarkers, quadArea } from './ar/detect.js'; -import { PoseEstimator } from './ar/pose.js'; +import { PoseEstimator, ROT_MODES, setRotMode, getRotMode } from './ar/pose.js'; import { WorldFuser } from './ar/fuse.js'; import { buildGhost } from './ghosts/loader.js'; import { ghostTransform } from './ghosts/behavior.js'; @@ -14,15 +14,19 @@ import { renderMarkdown } from './md.js'; const $ = (s) => document.querySelector(s); -/* The fused pose is a camera-in-world transform in OpenCV convention - * (+X right, +Y DOWN, +Z FORWARD along the view axis). A Three.js camera uses - * OpenGL convention (+X right, +Y UP, looks down LOCAL -Z). The correct change of - * basis between them is a 180° rotation about the camera's local Y axis: it flips - * the forward axis (+Z_cv view -> three.js -Z) AND the up axis (+Y_cv down -> - * three.js +Y up) together, leaving roll correct. (A 180° about X fixes the view - * axis too but leaves roll inverted — ghosts render upside-down when the phone is - * upright.) Applied after copying the fused quaternion each frame. */ -const CV_TO_GL = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI); +/* Camera-frame correction applied to the fused quaternion (see pose.js for the + * rotation-frame conjugation that happens upstream). Switchable for on-device A/B: + * once pose.js conjugates the rotation correctly, the right camera correction here + * may be identity, Y180, X180, or Z180 depending on the net convention. The debug + * overlay cycles CAM_MODES. */ +const CAM_CORR = { + none: new THREE.Quaternion(), + y180: new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI), + x180: new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), Math.PI), + z180: new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), Math.PI), +}; +const CAM_MODES = ['none', 'y180', 'x180', 'z180']; +let camMode = 'none'; // default: none — rely on pose.js conjugation alone let info = { mode: 'dev' }; let scene3, camera3, renderer, video, canvas2d, ctx2d; @@ -50,6 +54,7 @@ function updateDbg(fusedQuat, markerCount) { const up = _dbgV.set(0, 1, 0).applyQuaternion(camera3.quaternion).clone(); const v3 = v => `(${v.x.toFixed(2)},${v.y.toFixed(2)},${v.z.toFixed(2)})`; dbgEl.textContent = + `rot ${getRotMode()} cam ${camMode} (tap to cycle)\n` + `markers ${markerCount}\n${rawLine}\nview ${v3(view)}\nup ${v3(up)}\n` + `pos ${v3(camera3.position)}`; } @@ -131,8 +136,25 @@ async function start() { detector = createDetector(); fuser = new WorldFuser(); - // dev-mode pose readout - if (info.mode === 'dev') { dbgEl = $('#dbg'); if (dbgEl) dbgEl.style.display = 'block'; } + // dev-mode pose readout + tap-to-cycle correction modes + if (info.mode === 'dev') { + dbgEl = $('#dbg'); + if (dbgEl) { + dbgEl.style.display = 'block'; + dbgEl.style.pointerEvents = 'auto'; + // tap top ~60% cycles rotation mode; tap bottom cycles camera mode + dbgEl.addEventListener('click', (ev) => { + const r = dbgEl.getBoundingClientRect(); + if ((ev.clientY - r.top) < r.height * 0.5) { + const i = ROT_MODES.indexOf(getRotMode()); + setRotMode(ROT_MODES[(i + 1) % ROT_MODES.length]); + } else { + const i = CAM_MODES.indexOf(camMode); + camMode = CAM_MODES[(i + 1) % CAM_MODES.length]; + } + }); + } + } // network const net = new ExhibitNet(); @@ -241,8 +263,9 @@ function loop(t) { const fused = fuser.fuse(estimates); if (fused) { camera3.position.copy(fused.position); - // convert OpenCV camera frame -> Three.js (OpenGL) camera frame - camera3.quaternion.copy(fused.quaternion).multiply(CV_TO_GL); + // pose.js already conjugated rotation into the three.js frame; apply any + // residual camera-frame correction (switchable via overlay) + camera3.quaternion.copy(fused.quaternion).multiply(CAM_CORR[camMode]); updateDbg(fused.quaternion, fused.markerCount); } }