Add camera AR view, GPS stable mode and wider machine range
CI/CD / test (push) Successful in 28s
CI/CD / publish (push) Successful in 17s
CI/CD / deploy (push) Successful in 4s

- Real-world mode opens the phone's rear camera as the game background;
  the 3D camera stands at the GPS position and follows the phone's
  orientation, so machines, beacons, range circles, the route and the
  robot appear in their real direction. City, traffic and family are
  hidden in this view; a toggle switches back to the 3D map, and drag
  turns the view where there is no orientation sensor
- Stable mode (default on): drops GPS jumps impossible on foot, holds the
  position until it drifts past half the GPS error, calms the compass
- Arrival range is 2.5x the GPS error (20–50 m), shown as a ground circle
- Tests for the stabilizer, AR orientation and field of view

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-26 12:14:04 +03:30
co-authored by Claude Opus 5.5
parent 9564880783
commit 6ce560089f
9 changed files with 316 additions and 30 deletions
+39 -4
View File
@@ -1,6 +1,6 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {smoothFix, arrivalRadius, insideWorld} from '../src/realworld.js';
import {smoothFix, arrivalRadius, insideWorld, createStabilizer} from '../src/realworld.js';
test('GPS fixes blend by accuracy and a stale estimate is replaced', () => {
const first = smoothFix(null, {x: 0, z: 0, accuracy: 10, time: 0});
@@ -15,10 +15,45 @@ test('GPS fixes blend by accuracy and a stale estimate is replaced', () => {
});
test('Arrival radius widens with poor GPS and bounds check the world', () => {
assert.equal(arrivalRadius(4), 12);
assert.equal(arrivalRadius(10), 20);
assert.equal(arrivalRadius(100), 40);
assert.equal(arrivalRadius(4), 20);
assert.equal(arrivalRadius(12), 30);
assert.equal(arrivalRadius(100), 50);
const bounds = {minX: -10, minZ: -10, maxX: 10, maxZ: 10};
assert.ok(insideWorld(bounds, 0, 0));
assert.equal(insideWorld(bounds, 11, 0), false);
});
test('Stable mode holds still through jitter, drops jumps and follows real walking', () => {
const s = createStabilizer();
s.push({x: 0, z: 0, accuracy: 8, time: 0, speed: 0});
// Standing still: fixes wander by a few metres, the shown position does not move.
for (let i = 1; i <= 10; i++) {
const p = s.push({x: (i % 2 ? 3 : -3), z: (i % 3) - 1, accuracy: 8, time: i * 1000, speed: 0});
assert.deepEqual([p.x, p.z], [0, 0]);
}
// A single 80 m jump one second later is impossible on foot and is dropped.
assert.deepEqual([s.push({x: 80, z: 0, accuracy: 10, time: 11000, speed: 0}).x], [0]);
// Walking east at 1.4 m/s: the shown position follows.
let p;
for (let i = 1; i <= 20; i++) p = s.push({x: 1.4 * i, z: 0, accuracy: 6, time: 11000 + i * 1000, speed: 1.4});
assert.ok(p.x > 15, `followed the walk (${p.x})`);
// With stable mode off every smoothed fix is shown.
const raw = createStabilizer();
raw.stable = false;
raw.push({x: 0, z: 0, accuracy: 8, time: 0});
assert.ok(raw.push({x: 3, z: 0, accuracy: 8, time: 1000}).x > 1);
});
test('AR camera faces north/west with the phone held upright, and the FOV fits the screen', async () => {
const {Quaternion, Vector3} = await import('three');
const {orientationQuaternion, coverFov} = await import('../src/ar.js');
const look = (alpha, beta = 90, gamma = 0, angle = 0) => new Vector3(0, 0, -1).applyQuaternion(orientationQuaternion(new Quaternion(), alpha, beta, gamma, angle));
const north = look(0);
assert.ok(north.z < -.99, 'upright phone with alpha 0 looks north (−z)');
assert.ok(look(90).x < -.99, 'alpha 90 (turned left) looks west (−x)');
assert.ok(look(0, 0).y < -.99, 'phone lying flat looks at the ground');
// Portrait 9:19.5 screen over a 3:4 portrait video: the full long-side 65° shows vertically.
assert.ok(Math.abs(coverFov(3 / 4, 9 / 19.5) - 65) < .5);
// Landscape screen wider than the 4:3 video: the width shows 65°, so the vertical view is narrower.
assert.ok(coverFov(4 / 3, 19.5 / 9) < 35);
});