feat: add playable flight and office screen sharing
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import * as THREE from "three";
|
||||
import {
|
||||
CALIFORNIA_AIR_ROUTE,
|
||||
createSceneAircraft,
|
||||
type SceneAircraftOptions,
|
||||
} from "../aircraft/index.ts";
|
||||
|
||||
function options(overrides: Partial<SceneAircraftOptions> = {}): SceneAircraftOptions {
|
||||
return {
|
||||
project: (lat, lng) => [(lng + 121) * 20, -(lat - 36) * 20],
|
||||
groundAt: () => 2,
|
||||
route: CALIFORNIA_AIR_ROUTE,
|
||||
initialPosition: { lat: 34.0522, lng: -118.2437 },
|
||||
initialAltitudeM: 1_000,
|
||||
initialHeadingDeg: 0,
|
||||
fixedStepSeconds: 0.1,
|
||||
altitudeSceneUnitsPerMetre: 0.005,
|
||||
visualSceneUnitsPerMetre: 0.1,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("playable scene aircraft", () => {
|
||||
it("projects a stable root and stays inert until activated", () => {
|
||||
const aircraft = createSceneAircraft(options());
|
||||
const root = aircraft.root;
|
||||
assert.equal(root.name, "playable-scene-aircraft");
|
||||
assert.ok(root.getObjectByName("electric-aircraft"));
|
||||
assert.deepEqual(root.position.toArray(), [( -118.2437 + 121) * 20, 7, -(34.0522 - 36) * 20]);
|
||||
assert.equal(root.scale.x, 0.1);
|
||||
aircraft.setActions({ throttle: 1, roll: 0.8 });
|
||||
aircraft.tick(0.2);
|
||||
assert.equal(aircraft.state().elapsedSteps, 0);
|
||||
assert.equal(aircraft.root, root);
|
||||
aircraft.setActive(true);
|
||||
assert.equal(aircraft.actions().throttle, 0, "activation clears stale input");
|
||||
aircraft.setActions({ throttle: 1, roll: 0.8 });
|
||||
aircraft.tick(0.2);
|
||||
assert.equal(aircraft.state().elapsedSteps, 2);
|
||||
assert.equal(aircraft.state().mode, "manual");
|
||||
assert.equal(aircraft.view.position, aircraft.view.position);
|
||||
assert.deepEqual(aircraft.view.position.toArray(), root.position.toArray());
|
||||
aircraft.dispose();
|
||||
});
|
||||
|
||||
it("maps heading, pitch, and right-bank roll under Tera's -Z convention", () => {
|
||||
const aircraft = createSceneAircraft(options({ active: true }));
|
||||
aircraft.setActions({ throttle: 0.7, pitch: 1, roll: 1, yaw: 0.2 });
|
||||
aircraft.tick(1);
|
||||
const state = aircraft.state();
|
||||
assert.equal(aircraft.root.rotation.order, "YXZ");
|
||||
assert.ok(Math.abs(aircraft.root.rotation.x - state.pitchDeg * Math.PI / 180) < 1e-12);
|
||||
assert.ok(Math.abs(aircraft.root.rotation.y + state.headingDeg * Math.PI / 180) < 1e-12);
|
||||
assert.ok(Math.abs(aircraft.root.rotation.z + state.rollDeg * Math.PI / 180) < 1e-12);
|
||||
assert.ok(aircraft.root.rotation.z < 0, "positive controller roll presents right wing down");
|
||||
aircraft.dispose();
|
||||
});
|
||||
|
||||
it("animates articulated surfaces and fans from authoritative state", () => {
|
||||
const aircraft = createSceneAircraft(options({ active: true }));
|
||||
const leftAileron = aircraft.root.getObjectByName("electric-aircraft.aileron-left");
|
||||
const rightAileron = aircraft.root.getObjectByName("electric-aircraft.aileron-right");
|
||||
const leftFan = aircraft.root.getObjectByName("electric-aircraft.fan-left");
|
||||
assert.ok(leftAileron && rightAileron && leftFan);
|
||||
aircraft.setActions({ throttle: 1, roll: 0.8, pitch: 0.4, yaw: -0.3 });
|
||||
aircraft.tick(0.5);
|
||||
assert.ok(leftAileron.rotation.x > 0);
|
||||
assert.ok(rightAileron.rotation.x < 0);
|
||||
assert.notEqual(leftFan.rotation.z, 0);
|
||||
aircraft.dispose();
|
||||
});
|
||||
|
||||
it("publishes defensive finite chase poses for north and east headings", () => {
|
||||
const north = createSceneAircraft(options({ initialHeadingDeg: 0 }));
|
||||
const northPose = north.followPose();
|
||||
assert.ok(northPose.position.z > north.root.position.z, "camera is south/behind northbound flight");
|
||||
assert.ok(northPose.target.z < north.root.position.z);
|
||||
northPose.position.x = 999;
|
||||
assert.notEqual(north.followPose().position.x, 999);
|
||||
north.dispose();
|
||||
|
||||
const east = createSceneAircraft(options({ initialHeadingDeg: 90 }));
|
||||
const eastPose = east.followPose();
|
||||
assert.ok(eastPose.position.x < east.root.position.x);
|
||||
assert.ok(eastPose.target.x > east.root.position.x);
|
||||
assert.ok(eastPose.position.toArray().every(Number.isFinite));
|
||||
assert.ok(eastPose.target.toArray().every(Number.isFinite));
|
||||
east.dispose();
|
||||
});
|
||||
|
||||
it("clears edge actions, resumes assistance, and resets controller plus rig", () => {
|
||||
const aircraft = createSceneAircraft(options({ active: true }));
|
||||
const spawn = aircraft.state();
|
||||
aircraft.setActions({ throttle: 1, roll: 1, modeRequest: "manual", reset: false });
|
||||
aircraft.tick(0.2);
|
||||
assert.equal(aircraft.actions().modeRequest, "none");
|
||||
assert.equal(aircraft.actions().throttle, 1);
|
||||
aircraft.setActions({ modeRequest: "assisted" });
|
||||
aircraft.tick(0.1);
|
||||
assert.equal(aircraft.state().mode, "assisted");
|
||||
aircraft.reset();
|
||||
assert.deepEqual(aircraft.state(), spawn);
|
||||
assert.deepEqual(aircraft.actions(), {
|
||||
throttle: 0,
|
||||
yaw: 0,
|
||||
pitch: 0,
|
||||
roll: 0,
|
||||
modeRequest: "none",
|
||||
reset: false,
|
||||
});
|
||||
aircraft.dispose();
|
||||
});
|
||||
|
||||
it("disposes idempotently and refuses invalid projection/scaling", () => {
|
||||
const aircraft = createSceneAircraft(options());
|
||||
const parent = new THREE.Group();
|
||||
parent.add(aircraft.root);
|
||||
aircraft.dispose();
|
||||
aircraft.dispose();
|
||||
assert.equal(aircraft.root.parent, null);
|
||||
const before = aircraft.state();
|
||||
aircraft.tick(1);
|
||||
assert.deepEqual(aircraft.state(), before);
|
||||
assert.throws(() => createSceneAircraft(options({ visualSceneUnitsPerMetre: 0 })), RangeError);
|
||||
assert.throws(() => createSceneAircraft(options({ project: () => [Infinity, 0] })), RangeError);
|
||||
assert.throws(() => createSceneAircraft(options({ groundAt: () => Number.NaN })), RangeError);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user