orientation debug

This commit is contained in:
2026-07-08 20:02:19 +10:00
parent 9b181f7674
commit d745b24611
+291
View File
@@ -0,0 +1,291 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, maximum-scale=1" />
<title>Newbury — orientation debug</title>
<style>
:root { --ink:#e8eaf2; --dim:#8a93ad; --cyan:#51eaf1; --green:#4ade80; --amber:#fbbf24; --red:#f65151;
--panel:rgba(8,10,16,0.9); }
* { box-sizing:border-box; }
html,body { margin:0; height:100%; background:#05060a; color:var(--ink);
font-family:'Inter',system-ui,sans-serif; overflow:hidden; }
#video { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); }
#three,#overlay { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); }
#overlay { pointer-events:none; }
#hud { position:fixed; left:10px; top:calc(10px + env(safe-area-inset-top)); padding:11px 13px;
background:var(--panel); border:1px solid rgba(255,255,255,0.1); border-radius:12px;
font-family:ui-monospace,monospace; font-size:11px; line-height:1.65; backdrop-filter:blur(8px); max-width:88vw; }
#hud h3 { margin:0 0 6px; font-family:'Inter',sans-serif; font-size:12px; }
#hud .row { display:flex; gap:8px; }
#hud .lab { color:var(--dim); width:66px; display:inline-block; }
#hud .v { font-weight:600; }
.rgb-x{color:#ff6b6b}.rgb-y{color:#7CFF6b}.rgb-z{color:#6b9cff}
#controls { position:fixed; left:50%; bottom:calc(14px + env(safe-area-inset-bottom));
transform:translateX(-50%); display:flex; gap:8px; flex-wrap:wrap; justify-content:center; width:94vw; }
#controls button { border:0; border-radius:10px; padding:12px 14px; font-size:13px; font-weight:600;
cursor:pointer; background:#1a2033; color:var(--ink); flex:1; min-width:88px; max-width:150px; }
#controls button.rec { background:var(--red); color:#fff; }
#controls button.primary { background:linear-gradient(135deg,#529eff,#51eaf1); color:#06121f; }
#count { position:fixed; left:50%; bottom:calc(66px + env(safe-area-inset-bottom)); transform:translateX(-50%);
font-family:ui-monospace,monospace; font-size:12px; color:var(--dim); background:var(--panel);
padding:5px 12px; border-radius:8px; }
#logout { position:fixed; inset:0; background:#080a12; padding:16px; display:none; flex-direction:column; gap:10px; z-index:20; }
#logout.show { display:flex; }
#logout textarea { flex:1; width:100%; background:#0e1220; color:var(--ink); border:1px solid #26304a;
border-radius:10px; padding:12px; font-family:ui-monospace,monospace; font-size:11px; resize:none; }
#logout .row { display:flex; gap:8px; }
#logout button { flex:1; border:0; border-radius:10px; padding:13px; font-size:14px; font-weight:600; cursor:pointer; }
#copyBtn { background:linear-gradient(135deg,#529eff,#51eaf1); color:#06121f; }
#closeBtn { background:#1a2033; color:var(--ink); }
#copied { text-align:center; color:var(--green); font-size:12px; height:14px; }
#start { position:fixed; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center;
gap:16px; text-align:center; padding:26px; background:radial-gradient(circle at 50% 40%,#141a2b,#05060a 72%); }
#start h1 { font-size:22px; margin:0; } #start p { color:var(--dim); max-width:34ch; line-height:1.6; font-size:13.5px; margin:0; }
#go { border:0; cursor:pointer; font-size:16px; font-weight:600; color:#06121f;
background:linear-gradient(135deg,#529eff,#51eaf1); padding:14px 32px; border-radius:999px; }
#err { color:var(--red); font-size:12.5px; max-width:34ch; }
.hidden { display:none !important; }
</style>
</head>
<body>
<video id="video" playsinline muted autoplay></video>
<canvas id="three"></canvas>
<canvas id="overlay"></canvas>
<div id="hud" class="hidden">
<h3>Orientation debug</h3>
<div class="row"><span class="lab">marker</span><span class="v" id="dId"></span></div>
<div class="row"><span class="lab">RAW posit</span><span class="v" id="dRaw"></span></div>
<div class="row"><span class="lab">RENDER</span><span class="v" id="dRender"></span></div>
<div class="row"><span class="lab">gyro</span><span class="v" id="dGyro"></span></div>
<div class="row"><span class="lab" style="color:var(--amber)">watch</span><span id="dHint" style="color:var(--amber)">tilt phone up = pitch should ↑</span></div>
</div>
<div id="count" class="hidden">not recording</div>
<div id="controls" class="hidden">
<button id="recBtn">● Record</button>
<button id="markBtn">Mark</button>
<button id="viewBtn" class="primary">Copy log</button>
</div>
<div id="logout">
<div id="copied"></div>
<textarea id="logText" readonly></textarea>
<div class="row">
<button id="copyBtn">Copy to clipboard</button>
<button id="closeBtn">Back</button>
</div>
</div>
<div id="start">
<h1>Orientation debug</h1>
<p>Shows raw POS-IT angles vs what we render, plus a 3D gizmo on the marker. Tilt & rotate the phone; the RGB axes should stay glued to the marker. Record and send me the log.</p>
<button id="go">Start camera</button>
<div id="err"></div>
</div>
<script src="vendor/cv.js"></script>
<script src="vendor/svd.js"></script>
<script src="vendor/posit1.js"></script>
<script src="vendor/aruco.js"></script>
<script src="vendor/dictionaries/aruco_4x4_1000.js"></script>
<script type="importmap">
{ "imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"
} }
</script>
<script type="module">
import * as THREE from 'three';
import { TunedDetector, MarkerTracker } from './js/detect-tuned.js?v=1';
const $ = (id) => document.getElementById(id);
const video = $('video'), canvas = $('three'), overlay = $('overlay');
const octx = overlay.getContext('2d');
const grab = document.createElement('canvas'), gctx = grab.getContext('2d', { willReadFrequently: true });
const MARKER_SIZE_MM = 96;
let detector, tracker, posit, focalPx = 0, running = false;
let recording = false, log = [], frameN = 0;
let gyro = { alpha: null, beta: null, gamma: null };
// ---- three.js: mirror ar-head EXACTLY, but draw an AXIS GIZMO not a ghost ----
const renderer = new THREE.WebGLRenderer({ canvas, alpha: true, antialias: true });
renderer.setClearColor(0x000000, 0);
const scene = new THREE.Scene();
scene.add(new THREE.AmbientLight(0xffffff, 1.2));
let camera = new THREE.PerspectiveCamera(50, 1, 1, 5000);
// anchor object driven by pose (exactly like ar-head)
const anchor = new THREE.Object3D();
anchor.matrixAutoUpdate = false;
scene.add(anchor);
// A big RGB axis gizmo + an "UP" arrow, parented to anchor.
const gizmo = new THREE.AxesHelper(MARKER_SIZE_MM * 1.2);
anchor.add(gizmo);
// a distinct arrow pointing along marker normal (local +Z) so we see "up off table"
const arrowMat = new THREE.MeshBasicMaterial({ color: 0x51eaf1 });
const arrow = new THREE.Mesh(new THREE.ConeGeometry(10, 40, 12), arrowMat);
arrow.position.set(0, 0, MARKER_SIZE_MM); // +Z = marker normal
arrow.rotation.x = Math.PI / 2;
anchor.add(arrow);
anchor.visible = false;
const flip = new THREE.Matrix4().makeScale(1, -1, -1);
const poseMatrix = new THREE.Matrix4();
function centredCorners(m, w, h) { return m.corners.map((c) => ({ x: c.x - w / 2, y: h / 2 - c.y })); }
function applyPose(m) {
const pose = posit.pose(centredCorners(m, grab.width, grab.height));
if (!pose) return null;
const R = pose.bestRotation, t = pose.bestTranslation;
poseMatrix.set(
R[0][0], R[0][1], R[0][2], t[0],
R[1][0], R[1][1], R[1][2], t[1],
R[2][0], R[2][1], R[2][2], t[2],
0, 0, 0, 1
);
poseMatrix.premultiply(flip); // F*M (ar-head convention)
anchor.matrix.copy(poseMatrix);
anchor.matrixWorldNeedsUpdate = true;
// raw posit euler (before flip) and rendered euler (after flip)
const rawM = new THREE.Matrix4().set(
R[0][0], R[0][1], R[0][2], 0, R[1][0], R[1][1], R[1][2], 0, R[2][0], R[2][1], R[2][2], 0, 0,0,0,1);
const rawE = new THREE.Euler().setFromRotationMatrix(rawM, 'YXZ');
const renM = poseMatrix.clone(); renM.setPosition(0,0,0);
const renE = new THREE.Euler().setFromRotationMatrix(renM, 'YXZ');
return {
dist: Math.hypot(t[0], t[1], t[2]),
raw: { yaw: deg(rawE.y), pitch: deg(rawE.x), roll: deg(rawE.z) },
ren: { yaw: deg(renE.y), pitch: deg(renE.x), roll: deg(renE.z) },
};
}
const deg = (r) => Math.round(r * 180 / Math.PI);
function sizeAll() {
const vw = video.videoWidth, vh = video.videoHeight; if (!vw) return;
grab.width = vw; grab.height = vh;
const scale = Math.max(innerWidth / vw, innerHeight / vh);
const dw = vw * scale, dh = vh * scale;
for (const el of [video, canvas, overlay]) { el.style.width = dw + 'px'; el.style.height = dh + 'px'; }
renderer.setPixelRatio(Math.min(devicePixelRatio, 2)); renderer.setSize(dw, dh, false);
overlay.width = dw; overlay.height = dh;
focalPx = vw;
const fovY = 2 * Math.atan((vh / 2) / focalPx) * (180 / Math.PI);
camera.fov = fovY; camera.aspect = dw / dh; camera.updateProjectionMatrix();
posit = new POS.Posit(MARKER_SIZE_MM, focalPx);
}
async function start() {
$('err').textContent = '';
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: { ideal: 'environment' }, width: { ideal: 1280 }, height: { ideal: 720 } }, audio: false });
video.srcObject = stream; await video.play();
} catch (e) { $('err').textContent = 'Camera error: ' + (e.message || e.name); return; }
detector = new TunedDetector({ dictionaryName: 'ARUCO_4X4_1000' }).setPreset('forgiving');
tracker = new MarkerTracker({ coastMs: 150, smooth: true });
// device orientation (gyro) — needs permission on iOS
if (typeof DeviceOrientationEvent !== 'undefined' && DeviceOrientationEvent.requestPermission) {
try { await DeviceOrientationEvent.requestPermission(); } catch (_) {}
}
addEventListener('deviceorientation', (e) => { gyro = { alpha: e.alpha, beta: e.beta, gamma: e.gamma }; });
await new Promise((r) => { if (video.readyState >= 2) r(); else video.onloadeddata = () => r(); });
sizeAll(); addEventListener('resize', sizeAll);
$('start').classList.add('hidden');
for (const id of ['hud','count','controls']) $(id).classList.remove('hidden');
running = true; requestAnimationFrame(loop);
}
const sx = () => overlay.width / grab.width, sy = () => overlay.height / grab.height;
function loop(now) {
if (!running) return; requestAnimationFrame(loop);
if (video.readyState < 2 || !grab.width) return;
gctx.drawImage(video, 0, 0, grab.width, grab.height);
const img = gctx.getImageData(0, 0, grab.width, grab.height);
let raw = []; try { raw = detector.detect(img, { width: grab.width, height: grab.height }); } catch (_) {}
const markers = tracker.update(raw, now);
octx.clearRect(0, 0, overlay.width, overlay.height);
let main = markers[0] || null;
if (main) {
// draw outline + corner numbers
octx.strokeStyle = '#51eaf1'; octx.lineWidth = 2; octx.beginPath();
main.corners.forEach((p, i) => { const x = p.x*sx(), y = p.y*sy(); i ? octx.lineTo(x,y) : octx.moveTo(x,y); });
octx.closePath(); octx.stroke();
main.corners.forEach((p, i) => {
const x = p.x*sx(), y = p.y*sy();
octx.fillStyle = ['#fff','#ffd24a','#ff8a3a','#ff5a5a'][i];
octx.beginPath(); octx.arc(x, y, 6, 0, 7); octx.fill();
octx.fillStyle = '#000'; octx.font = 'bold 10px monospace'; octx.fillText(i, x-3, y+3);
});
const res = applyPose(main);
if (res) {
anchor.visible = true;
$('dId').textContent = 'id ' + main.id + (main.coasting ? ' coast' : '');
$('dRaw').innerHTML = fmt(res.raw);
$('dRender').innerHTML = fmt(res.ren);
$('dGyro').textContent = gyro.beta != null
? `β${Math.round(gyro.beta)} γ${Math.round(gyro.gamma)} α${Math.round(gyro.alpha)}` : 'n/a';
if (recording) {
frameN++;
if (frameN % 3 === 0) {
log.push({ t: Math.round(now), id: main.id, dist: Math.round(res.dist),
raw: res.raw, ren: res.ren, gyro: { b: r1(gyro.beta), g: r1(gyro.gamma), a: r1(gyro.alpha) } });
$('count').textContent = `● REC · ${log.length}`;
}
}
}
} else {
anchor.visible = false; $('dId').textContent = 'none';
}
renderer.render(scene, camera);
}
function r1(v){ return v==null?null:Math.round(v); }
function fmt(a){ return `<span class="rgb-y">P${a.pitch}</span> <span class="rgb-z">R${a.roll}</span> <span class="rgb-x">Y${a.yaw}</span>`; }
function buildLog() {
const L = [];
L.push('=== NEWBURY ORIENTATION LOG ===');
L.push('conversion: F*M (premultiply flip) marker:' + MARKER_SIZE_MM + 'mm cam:' + grab.width + 'x' + grab.height);
L.push('cols: t,id,dist, RAW(pitch,roll,yaw), RENDER(pitch,roll,yaw), GYRO(beta,gamma,alpha)');
L.push('gyro beta=front/back tilt, gamma=left/right tilt, alpha=compass');
L.push('samples:' + log.length);
L.push('');
for (const r of log) {
L.push([r.t, r.id, r.dist,
'RAW', r.raw.pitch, r.raw.roll, r.raw.yaw,
'REN', r.ren.pitch, r.ren.roll, r.ren.yaw,
'GYRO', r.gyro.b, r.gyro.g, r.gyro.a].join(','));
}
return L.join('\n');
}
$('recBtn').addEventListener('click', () => {
recording = !recording;
$('recBtn').classList.toggle('rec', recording);
$('recBtn').textContent = recording ? '■ Stop' : '● Record';
if (recording) { log = []; $('count').textContent = '● REC · 0'; }
});
$('markBtn').addEventListener('click', () => { log.push({ t: Math.round(performance.now()), mark: true, raw:{}, ren:{}, gyro:{} }); });
$('viewBtn').addEventListener('click', () => { $('logText').value = buildLog(); $('logout').classList.add('show'); });
$('copyBtn').addEventListener('click', async () => {
try { await navigator.clipboard.writeText($('logText').value); $('copied').textContent = 'Copied!'; }
catch (_) { $('logText').select(); $('copied').textContent = 'Long-press → Copy'; }
});
$('closeBtn').addEventListener('click', () => $('logout').classList.remove('show'));
$('go').addEventListener('click', start);
</script>
</body>
</html>