Files
gods-eye-view/server/vcam/phone/orientation.test.mjs
Claude 56586a234b feat(vcam): use a phone on the network as a virtual camera
SCENES > VCAM pairs a phone through a QR code and turns it into a
handheld camera, after Unreal's Live Link VCAM and Blender's VirtuCamera:

- Android Chrome tracks 6DoF through WebXR immersive-ar; iPhone Safari,
  which has no WebXR AR, tracks rotation through DeviceOrientation, and
  thumb sticks dolly, truck, crane and pan.
- A motion scale (1:1 walk to 1:10000 orbit), smoothing, height and
  horizon locks, hold/clutch and recenter, none of which make the camera
  jump. Focal length and scale-aware depth of field with tap-to-focus.
- The phone shows the app's render as a monitor (newest-frame JPEG pull).
- REC records a take as a version-7 track shot in the selected scene.
- The rig claims the camera through the navigation seam (Cockpit refuses
  it) and holds on any other navigation, scene run or globe input.

Pairing never exposes the app: a loopback-only session request starts a
separate HTTPS listener (VCAM_PORT, default 4174) that serves only the
static phone page and a token-paired relay, and stops after the last
session. Its certificate comes from a local CA built with node:crypto, which
an iPhone can trust once; without it the phone falls back from WebSocket to
HTTPS requests, which iOS allows past a certificate warning.

scripts/qa-vcam.mjs drives it end to end: the real phone page over the
HTTPS fallback, a scripted 6DoF phone, a recorded and replayed take, depth
of field and camera handoff.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015H1iLusTzWpVKitGic2AW8
2026-10-19 04:03:02 +00:00

40 lines
1.6 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { orientationQuaternion } from './orientation.js';
import { rotateByQuaternion } from '../../../src/vcam/rig.js';
const close = (actual, expected, message) =>
actual.forEach((value, i) =>
assert.ok(Math.abs(value - expected[i]) < 1e-9, `${message}: ${actual} vs ${expected}`),
);
const axes = (q) => ({
forward: rotateByQuaternion(q, [0, 0, -1]),
up: rotateByQuaternion(q, [0, 1, 0]),
});
test('an upright portrait phone looks out of its back along the Y-up forward axis', () => {
// beta 90: stood up from flat, screen toward the user, back toward north.
const { forward, up } = axes(orientationQuaternion(0, 90, 0, 0));
close(forward, [0, 0, -1], 'faces north (-z)');
close(up, [0, 1, 0], 'top edge up');
});
test('alpha turns the phone counterclockwise seen from above', () => {
const { forward } = axes(orientationQuaternion(90, 90, 0, 0));
close(forward, [-1, 0, 0], 'faces west');
});
test('a landscape screen reports the picture\'s up, not the device\'s top edge', () => {
// Rotated a quarter turn counterclockwise: the top edge points west and the
// right edge up, the back still faces north, and the screen angle is 90.
const { forward, up } = axes(orientationQuaternion(90, 0, -90, 90));
close(forward, [0, 0, -1], 'still faces north');
close(up, [0, 1, 0], 'picture up is world up');
});
test('a phone lying flat, screen up, looks straight down', () => {
const { forward, up } = axes(orientationQuaternion(0, 0, 0, 0));
close(forward, [0, -1, 0], 'down');
close(up, [0, 0, -1], 'top edge toward north');
});