8fb85cd2e5
The wide shot, which is what an anonymous visitor actually lands on. **The sea was `MeshLambertMaterial`** — a material with no specular term at all, by construction — on a board where water is half the frame. It is now a low-roughness dielectric that reads `scene.environment`, with a runtime-generated tiling swell normal map sampled twice per fragment at two scales and two headings, so the sun breaks into a moving glitter path instead of a mirror point. An `onBeforeCompile` patch takes the body colour toward the deep value looking straight down and leaves it to the reflection at grazing, and walks roughness up past 1.6 board spans so the far water cannot shimmer. The swell spectrum is 1/k^2 and not 1/k because the first attempt was photographed: at 1/k every component carries the same slope, the shortest wave wins, and the sea renders as hard diagonal corduroy. A test holds it now. **The board no longer ends in a diamond.** The sea plane went from 1.8 board spans to 18, past the fog's far plane from anywhere the orbit reaches, and the sky is a world-space dome rather than a screen-space gradient. That gradient was wrong in a way dusk made obvious: the sunset band was painted along the *bottom* of the picture, under the board, while the true horizon at the top of frame stayed zenith blue. `daylight.ts` pinning the horizon stop to the fog colour to hide the seam was a symptom of it. **Terrain casts shadows.** Left off before because double-sided terrain against a ~16 m-per-texel shadow map gives acne; `shadowSide = BackSide` is the cure, shot at four sun elevations down to +0.0 degrees to confirm no stippling. The caster is a stride-2 decimation appended to the same index buffer and swapped in by `onBeforeShadow`/`onAfterShadow` via `drawRange`: no extra draw call, a quarter of the depth cost, and indistinguishable from the full-resolution caster in a side-by-side crop. Stride 1 was measured at +65,566 triangles and would have missed the budget by ~47,000, so it was not shipped. **California reads as California.** It was a beige kite: the eastern edge one ruled line for five degrees of latitude, the south closing in a diagonal V, the whole south-east a featureless tan wedge. Now the coast runs to the Mexican border with San Diego on it, the eastern edge follows the Colorado and the Nevada diagonal, and the south-east is the Basin and Range — forty parallel desert ridges throwing shadows east, Death Valley as a white pan between the Panamints and the Black Mountains, the Salton Sea the one cool value for two hundred kilometres. The opening pose is retuned to the bigger board; the old 452/392 stand-off left a slab of empty ocean where the state should be. **The aircraft were six pixels.** Measured, by enlarging a screenshot 200% to find one at all — indistinguishable from a dead pixel, on a board whose entire claim is that the sky is live. They are airliners now, with planform and trail, and clicking one raises its card for a signed-out visitor. **The Model X is off the wall.** It stood at floor level outside a studio 188 m up a Transbay tower, reading as a car balanced on a parapet. The apron is now chosen from `site.elevation`, which the pack already carries — not from an office id, which is the bug class this repo already hit once when a door marker gated on `id === "sf"` and would have pinned the Los Angeles building to San Francisco. Also fixed, and nearly shipped: sea z-fighting dithered every flat piece of ground on the Bay Area and SoCal boards. And one test asserted an exact source line for the water material, so the better multi-line implementation failed it — it now asserts the property (dielectric, metalness 0, low roughness) rather than the author's first guess at formatting. Tests 964 -> 1015. California desktop 562/650 draw calls and 728,744/750,000 triangles — 2.8% of triangle headroom left, which is the number the next person should check first. No budget was raised. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
686 lines
27 KiB
TypeScript
686 lines
27 KiB
TypeScript
/**
|
||
* The wiring seams, tested where they can actually be exercised.
|
||
*
|
||
* `createOfficeScene` is the one scene in this repo that can be built under
|
||
* `node --test`: it needs a DOM element and a `window` for `OrbitControls` and
|
||
* `matchMedia`, and nothing else — no WebGL context, no Worker, no heightfield.
|
||
* So the office half of the integration surface is tested for real, against the
|
||
* shipped packs, rather than by reading the source and hoping.
|
||
*
|
||
* The city half is not, and the asymmetry is honest rather than lazy:
|
||
* `createScene` awaits a terrain Worker and takes a live `Stage`, so the only
|
||
* place it can be exercised is a browser. `scripts/ui-smoke.mjs` is where that
|
||
* happens, and the two source-level assertions at the bottom of this file are
|
||
* the belt to that brace — they fail if the seam is *deleted*, which is the
|
||
* failure a browser test is slowest to tell you about.
|
||
*
|
||
* The renderer is a fake of the same specific kind `src/test/render` uses:
|
||
* `PMREMGenerator` never touches WebGL directly, so stubbing `render` and
|
||
* `setRenderTarget` runs the real generator, the real target allocation and the
|
||
* real blur chain and leaves only the pixels unwritten.
|
||
*/
|
||
|
||
import assert from "node:assert/strict";
|
||
import { readFileSync } from "node:fs";
|
||
import path from "node:path";
|
||
import { fileURLToPath } from "node:url";
|
||
import test from "node:test";
|
||
import * as THREE from "three";
|
||
|
||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
|
||
|
||
// ---- The environment the browser supplies and Node does not ----------------
|
||
//
|
||
// Installed before the modules are imported, because `scenekit.ts` reads
|
||
// `window.matchMedia` at construction and `officeScene.ts` reaches it through a
|
||
// static import chain. The dynamic imports below are what let this run first.
|
||
|
||
(globalThis as unknown as { window: unknown }).window = {
|
||
matchMedia: () => ({ matches: false, addEventListener() {}, removeEventListener() {} }),
|
||
innerWidth: 1_200,
|
||
innerHeight: 800,
|
||
devicePixelRatio: 1,
|
||
addEventListener() {},
|
||
removeEventListener() {},
|
||
};
|
||
|
||
const { createOfficeScene } = await import("../../interiors/officeScene.ts");
|
||
const { createEnvironmentRig } = await import("../../engine/environmentRig.ts");
|
||
const LUMBRIDGE_HQ = (await import("../../offices/lumbridge-hq.ts")).default;
|
||
const MATEO_COURT = (await import("../../offices/mateo-court.ts")).default;
|
||
const { initialDeviceState } = await import("../../devices/types.ts");
|
||
const { createSimulatedVehicleTelemetry } = await import("../../transport/vehicleTelemetry.ts");
|
||
const { arrivalGroundFor } = await import("../../transport/exteriorVehicle.ts");
|
||
|
||
type Office = typeof LUMBRIDGE_HQ;
|
||
|
||
const PACKS: readonly (readonly [string, Office])[] = [
|
||
["lumbridge-hq", LUMBRIDGE_HQ],
|
||
["mateo-court", MATEO_COURT],
|
||
];
|
||
|
||
// ---- Fakes -----------------------------------------------------------------
|
||
|
||
/** Everything `OrbitControls` and `SceneKit` touch on the canvas, and no more. */
|
||
function fakeDom(): HTMLElement {
|
||
return {
|
||
style: {},
|
||
clientWidth: 1_200,
|
||
clientHeight: 800,
|
||
addEventListener() {},
|
||
removeEventListener() {},
|
||
setPointerCapture() {},
|
||
releasePointerCapture() {},
|
||
getBoundingClientRect: () => ({
|
||
left: 0, top: 0, width: 1_200, height: 800, right: 1_200, bottom: 800, x: 0, y: 0,
|
||
}),
|
||
getRootNode: () => ({ addEventListener() {}, removeEventListener() {} }),
|
||
ownerDocument: { addEventListener() {}, removeEventListener() {} },
|
||
} as unknown as HTMLElement;
|
||
}
|
||
|
||
interface FakeRenderer {
|
||
renders: number;
|
||
targets: Set<THREE.WebGLRenderTarget>;
|
||
as(): THREE.WebGLRenderer;
|
||
}
|
||
|
||
/** A renderer that allocates render targets and never draws to them. */
|
||
function fakeRenderer(): FakeRenderer {
|
||
const targets = new Set<THREE.WebGLRenderTarget>();
|
||
const state: FakeRenderer = {
|
||
renders: 0,
|
||
targets,
|
||
as: () => stub as unknown as THREE.WebGLRenderer,
|
||
};
|
||
const stub = {
|
||
autoClear: true,
|
||
toneMapping: THREE.NoToneMapping,
|
||
xr: { enabled: false },
|
||
state: { buffers: { depth: { getReversed: () => false } } },
|
||
getRenderTarget: () => null,
|
||
getActiveCubeFace: () => 0,
|
||
getActiveMipmapLevel: () => 0,
|
||
getClearColor: (target: THREE.Color) => target,
|
||
getClearAlpha: () => 1,
|
||
setClearColor: () => {},
|
||
setClearAlpha: () => {},
|
||
clearDepth: () => {},
|
||
compile: () => {},
|
||
setRenderTarget(target: THREE.WebGLRenderTarget | null) {
|
||
if (target) targets.add(target);
|
||
},
|
||
render() {
|
||
state.renders += 1;
|
||
},
|
||
};
|
||
return state;
|
||
}
|
||
|
||
/** Two aeroplanes over the studio, at bearings this test can predict. */
|
||
function fakeFlights(site: NonNullable<Office["site"]>) {
|
||
let polls = 0;
|
||
return {
|
||
interval: 1,
|
||
polls: () => polls,
|
||
poll() {
|
||
polls += 1;
|
||
return [
|
||
// Due north of the site and high: comfortably above the elevation floor.
|
||
{ id: "north", lat: site.lat + 0.05, lng: site.lng, altitude: 6_000, heading: 180 },
|
||
// Due east and equally high.
|
||
{ id: "east", lat: site.lat, lng: site.lng + 0.05, altitude: 6_000, heading: 270 },
|
||
// On the deck a long way off: below the floor, and must not be drawn.
|
||
{ id: "below", lat: site.lat + 2, lng: site.lng, altitude: 100, heading: 0 },
|
||
];
|
||
},
|
||
};
|
||
}
|
||
|
||
function build(pack: Office, extra: Record<string, unknown> = {}) {
|
||
const renderer = fakeRenderer();
|
||
const rig = createEnvironmentRig(renderer.as());
|
||
const scene = createOfficeScene(pack, {
|
||
dom: fakeDom(),
|
||
depth: "public",
|
||
// `low` is flat Lambert with no textures: it builds the same graph and the
|
||
// same materials-per-role decisions, in a fraction of the canvas work.
|
||
quality: "low",
|
||
environment: rig,
|
||
exteriorVehicle: { detail: "corridor", seed: 7 },
|
||
...(pack.site ? { horizon: { drop: pack.site.elevation } } : {}),
|
||
...extra,
|
||
});
|
||
return { scene, rig, renderer };
|
||
}
|
||
|
||
function named(root: THREE.Object3D, name: string): THREE.Object3D | null {
|
||
return root.children.find((child) => child.name === name) ?? null;
|
||
}
|
||
|
||
// ---- The environment rig ---------------------------------------------------
|
||
|
||
for (const [id, pack] of PACKS) {
|
||
test(`${id}: createOfficeScene puts a real environment texture on the scene`, () => {
|
||
const { scene, rig, renderer } = build(pack);
|
||
try {
|
||
assert.ok(
|
||
scene.scene.environment instanceof THREE.Texture,
|
||
"the office must have an environment before its first frame, or every metal " +
|
||
"role in the room renders as grey plastic",
|
||
);
|
||
assert.equal(scene.scene.environmentIntensity, 1);
|
||
assert.ok(renderer.renders > 0, "the PMREM blur chain never ran");
|
||
} finally {
|
||
scene.dispose();
|
||
rig.dispose();
|
||
}
|
||
});
|
||
|
||
test(`${id}: disposing the scene releases it from the rig's ledger`, () => {
|
||
const { scene, rig } = build(pack);
|
||
try {
|
||
assert.ok(scene.scene.environment !== null);
|
||
scene.dispose();
|
||
assert.equal(
|
||
scene.scene.environment,
|
||
null,
|
||
"a disposed scene left holding an environment is a disposed scene the rig " +
|
||
"still has a strong reference to — one whole floor plate leaked per switch",
|
||
);
|
||
} finally {
|
||
rig.dispose();
|
||
}
|
||
});
|
||
}
|
||
|
||
test("a rebuilt environment does not reach a scene that has been released", () => {
|
||
const { scene, rig } = build(LUMBRIDGE_HQ);
|
||
const second = createOfficeScene(MATEO_COURT, {
|
||
dom: fakeDom(), depth: "public", quality: "low", environment: rig,
|
||
});
|
||
try {
|
||
scene.dispose();
|
||
// A different sun, so the rig's coarse fingerprint moves and it rebuilds.
|
||
second.setLighting({
|
||
sun: { direction: [-0.7, 0.2, 0.1], color: 0xffb070, intensity: 0.6 },
|
||
hemisphere: { sky: 0x30435c, ground: 0x201a14, intensity: 0.5 },
|
||
ambient: { color: 0xffffff, intensity: 0.1 },
|
||
sky: null,
|
||
fog: null,
|
||
});
|
||
assert.equal(scene.scene.environment, null, "the released scene was written to again");
|
||
assert.ok(second.scene.environment instanceof THREE.Texture);
|
||
} finally {
|
||
second.dispose();
|
||
rig.dispose();
|
||
}
|
||
});
|
||
|
||
// ---- The device layer ------------------------------------------------------
|
||
|
||
for (const [id, pack] of PACKS) {
|
||
test(`${id}: the authored hardware is mounted and offered to the panel`, () => {
|
||
const { scene, rig } = build(pack);
|
||
try {
|
||
const declared = pack.levels.flatMap((level) => level.floorplan.devices ?? []);
|
||
assert.ok(declared.length > 0, `${id} authors no devices, so this test proves nothing`);
|
||
assert.deepEqual(
|
||
[...scene.devices].map((d) => d.id).sort(),
|
||
[...declared].map((d) => d.id).sort(),
|
||
"every declaration this pack authored should resolve against its own plan",
|
||
);
|
||
const layer = named(scene.scene, "devices");
|
||
assert.ok(layer !== null, "no device layer in the scene graph");
|
||
assert.equal(
|
||
layer.children.length,
|
||
declared.length,
|
||
"one mount per resolved declaration",
|
||
);
|
||
} finally {
|
||
scene.dispose();
|
||
rig.dispose();
|
||
}
|
||
});
|
||
}
|
||
|
||
test("a device reading reaches the indicator on the hardware", () => {
|
||
const { scene, rig } = build(LUMBRIDGE_HQ);
|
||
try {
|
||
const declaration = scene.devices[0];
|
||
assert.ok(declaration, "lumbridge-hq authors at least one device");
|
||
const indicators = (): THREE.Material[] => {
|
||
const found: THREE.Material[] = [];
|
||
named(scene.scene, "devices")?.traverse((object) => {
|
||
if (object.name === "indicator") {
|
||
object.traverse((child) => {
|
||
const mesh = child as THREE.Mesh;
|
||
if (mesh.isMesh && !Array.isArray(mesh.material)) found.push(mesh.material);
|
||
});
|
||
}
|
||
});
|
||
return found;
|
||
};
|
||
|
||
scene.setDeviceStates([{ ...initialDeviceState(declaration, 0), powered: false }]);
|
||
const off = indicators().map((m) => (m as THREE.MeshStandardMaterial).color.getHex());
|
||
scene.setDeviceStates([{ ...initialDeviceState(declaration, 0), powered: true, muted: false }]);
|
||
const on = indicators().map((m) => (m as THREE.MeshStandardMaterial).color.getHex());
|
||
|
||
assert.ok(off.length > 0, "the device assets carry no indicator meshes");
|
||
assert.notDeepEqual(on, off, "powering a device changed nothing anybody could see");
|
||
} finally {
|
||
scene.dispose();
|
||
rig.dispose();
|
||
}
|
||
});
|
||
|
||
// ---- The exterior ----------------------------------------------------------
|
||
|
||
for (const [id, pack] of PACKS) {
|
||
test(`${id}: the exterior matches the ground its site actually has`, () => {
|
||
const arrival = pack.site?.arrival;
|
||
assert.ok(arrival, `${id} authors no arrival anchor`);
|
||
const { scene, rig } = build(pack);
|
||
try {
|
||
const exterior = named(scene.scene, "office-exterior");
|
||
assert.ok(exterior !== null, "no exterior in the scene graph");
|
||
assert.equal(exterior.userData.arrivalKind, "vehicle-stall");
|
||
|
||
exterior.updateMatrixWorld(true);
|
||
let car: THREE.Object3D | null = null;
|
||
exterior.traverse((object) => {
|
||
if (object.userData.vehicleModel === "model-x") car = object;
|
||
});
|
||
|
||
// An apron is ground, and `lumbridge-hq`'s studio floor is 188 m above the
|
||
// pavement outside it. Built literally at the stall's own storey it is a
|
||
// paved pad and a car in open sky beside the west wall, which is what this
|
||
// shipped for one build. Read off the site rather than the id, because
|
||
// reading off the site is exactly what the layer under test must do.
|
||
if (arrivalGroundFor(pack.site?.elevation ?? 0) === "air") {
|
||
assert.equal(car, null, `${id} parked a car in the air`);
|
||
assert.equal(exterior.children.length, 0, "an off-ground site draws nothing");
|
||
return;
|
||
}
|
||
|
||
assert.ok(car !== null, "the apron was built without a car on it");
|
||
const at = (car as THREE.Object3D).getWorldPosition(new THREE.Vector3());
|
||
const floorY = scene.plan.level(arrival.levelId)?.floorY ?? 0;
|
||
assert.ok(
|
||
Math.hypot(at.x - arrival.position.x, at.z - arrival.position.z) <= 0.5,
|
||
`car at ${at.x.toFixed(2)}, ${at.z.toFixed(2)} against stall ` +
|
||
`${arrival.position.x}, ${arrival.position.z}`,
|
||
);
|
||
assert.ok(
|
||
Math.abs(at.y - floorY) <= 1.5,
|
||
`the apron must stand on the floor of ${arrival.levelId} (${floorY} m), not at ` +
|
||
`the plan origin — the stall names a storey for a reason`,
|
||
);
|
||
} finally {
|
||
scene.dispose();
|
||
rig.dispose();
|
||
}
|
||
});
|
||
}
|
||
|
||
test("vehicle telemetry can be pushed every frame without moving anything twice", () => {
|
||
const { scene, rig } = build(LUMBRIDGE_HQ);
|
||
const source = createSimulatedVehicleTelemetry({
|
||
seed: 3, fixedStepSeconds: 0.5, ambientC: 19,
|
||
});
|
||
try {
|
||
// Idempotent by contract: `apply` is signature-guarded, so the frames that
|
||
// spend no simulated step must cost a comparison and change nothing.
|
||
for (let i = 0; i < 20; i += 1) scene.setVehicleTelemetry(source.current());
|
||
source.command({ op: "charge", value: true });
|
||
for (let i = 0; i < 40; i += 1) source.stepFixed();
|
||
scene.setVehicleTelemetry(source.current());
|
||
assert.ok(source.current().pluggedIn, "the command never reached the simulator");
|
||
} finally {
|
||
scene.dispose();
|
||
rig.dispose();
|
||
}
|
||
});
|
||
|
||
// ---- Overhead traffic ------------------------------------------------------
|
||
|
||
test("the studio sky carries the same aircraft the board outside is drawing", async () => {
|
||
const site = MATEO_COURT.site;
|
||
assert.ok(site, "mateo-court is sited");
|
||
const flights = fakeFlights(site);
|
||
const { scene, rig } = build(MATEO_COURT, { flights });
|
||
try {
|
||
const layer = named(scene.scene, "overhead-traffic");
|
||
assert.ok(layer !== null, "a sited office with a flight source drew no sky traffic");
|
||
const mesh = layer.children[0] as THREE.InstancedMesh;
|
||
assert.ok(mesh.isInstancedMesh, "overhead traffic must be one draw call, not one per track");
|
||
assert.equal(mesh.count, 0, "nothing is drawn before the first poll lands");
|
||
|
||
scene.tick(2, 2);
|
||
// The poll is a promise even for a synchronous source, so let the microtask
|
||
// queue drain before reading what it placed.
|
||
await Promise.resolve();
|
||
await Promise.resolve();
|
||
|
||
assert.equal(flights.polls(), 1, "the office must poll the source it was handed");
|
||
assert.equal(
|
||
mesh.count,
|
||
2,
|
||
"the aeroplane below the elevation floor was drawn through the ground",
|
||
);
|
||
|
||
const centre = scene.plan.bounds.center;
|
||
const matrix = new THREE.Matrix4();
|
||
const north = new THREE.Vector3();
|
||
mesh.getMatrixAt(0, matrix);
|
||
north.setFromMatrixPosition(matrix);
|
||
const east = new THREE.Vector3();
|
||
mesh.getMatrixAt(1, matrix);
|
||
east.setFromMatrixPosition(matrix);
|
||
|
||
/*
|
||
* The building's own frame: `site.heading` is the bearing its −Z points
|
||
* along, so an aeroplane due north of a north-facing pack lands at −Z. Both
|
||
* shipped packs are rotated, so the test asserts the *relationship* rather
|
||
* than a literal axis — the two tracks are ninety degrees apart on the
|
||
* compass and must be ninety degrees apart on the dome.
|
||
*/
|
||
const bearingOf = (at: THREE.Vector3) =>
|
||
Math.atan2(at.x - centre.x, -(at.z - centre.z)) * 180 / Math.PI;
|
||
const separation = Math.abs(((bearingOf(east) - bearingOf(north) + 540) % 360) - 180);
|
||
assert.ok(
|
||
Math.abs(separation - 90) < 2,
|
||
`two tracks 90° apart on the compass came out ${separation.toFixed(1)}° apart on the dome`,
|
||
);
|
||
assert.ok(north.y > 0, "an aeroplane above the horizon must be above the floor plate");
|
||
} finally {
|
||
scene.dispose();
|
||
rig.dispose();
|
||
}
|
||
});
|
||
|
||
test("an office with no site draws no sky traffic and asks the feed nothing", () => {
|
||
const site = LUMBRIDGE_HQ.site;
|
||
assert.ok(site);
|
||
const flights = fakeFlights(site);
|
||
const { site: _site, ...unsited } = LUMBRIDGE_HQ;
|
||
const renderer = fakeRenderer();
|
||
const rig = createEnvironmentRig(renderer.as());
|
||
const scene = createOfficeScene(unsited as typeof LUMBRIDGE_HQ, {
|
||
dom: fakeDom(), depth: "public", quality: "low", environment: rig, flights,
|
||
});
|
||
try {
|
||
assert.equal(named(scene.scene, "overhead-traffic"), null);
|
||
scene.tick(2, 2);
|
||
assert.equal(
|
||
flights.polls(),
|
||
0,
|
||
"a pack with no coordinate has no bearing to put an aeroplane on, so it must " +
|
||
"not be spending a request to find out where one is",
|
||
);
|
||
} finally {
|
||
scene.dispose();
|
||
rig.dispose();
|
||
}
|
||
});
|
||
|
||
// ---- The clickable aircraft ------------------------------------------------
|
||
|
||
const { createFlightLayer, aircraftDetail } = await import("../../engine/flights.ts");
|
||
|
||
/**
|
||
* Enough of a `World` for the flight layer: a projection and a vertical scale.
|
||
*
|
||
* The real one owns a 0.53M-point heightfield and two and a half seconds of
|
||
* build, none of which this layer touches — it projects a coordinate and scales
|
||
* an altitude, and that is the whole of its contact with the world.
|
||
*/
|
||
const flatWorld = {
|
||
project: (lat: number, lng: number) => [lng * 100, -lat * 100],
|
||
metres: (m: number) => m / 100,
|
||
metresPerUnit: 100,
|
||
} as unknown as Parameters<typeof createFlightLayer>[0];
|
||
|
||
test("every aeroplane on the board is a pick target that knows which one it is", () => {
|
||
const layer = createFlightLayer(flatWorld);
|
||
try {
|
||
assert.equal(layer.pickables.length, 0, "nothing to click before the first observation");
|
||
|
||
layer.update([
|
||
{ id: "a1b2c3", lat: 37.6, lng: -122.4, altitude: 9_000, heading: 310, callsign: "UAL221" },
|
||
{ id: "~ddeeff", lat: 37.8, lng: -122.2, altitude: 2_400, heading: 90 },
|
||
]);
|
||
assert.equal(layer.pickables.length, 2);
|
||
assert.deepEqual(
|
||
layer.pickables.map((object) => object.userData.aircraftId).sort(),
|
||
["a1b2c3", "~ddeeff"],
|
||
"a raycast hit resolves to an aeroplane through this and nothing else",
|
||
);
|
||
for (const object of layer.pickables) {
|
||
assert.ok(layer.group.children.includes(object), "a pick target that is not drawn");
|
||
}
|
||
} finally {
|
||
layer.dispose();
|
||
assert.equal(layer.pickables.length, 0, "dispose must empty the target list too");
|
||
}
|
||
});
|
||
|
||
test("the card an anonymous visitor gets says what was observed and what was not", () => {
|
||
// The simulator's ids are route names, not transponder addresses, and
|
||
// `aircraftDetail` must decline to present one as the other.
|
||
const invented = aircraftDetail(
|
||
{ id: "sfo-departure-2", lat: 37.62, lng: -122.38, altitude: 3_000, heading: 300 },
|
||
{ observed: false },
|
||
);
|
||
assert.equal(invented.icao24, null, "a route name is not a Mode S address");
|
||
assert.equal(invented.observed, false);
|
||
|
||
const real = aircraftDetail(
|
||
{ id: "a1b2c3", lat: 37.62, lng: -122.38, altitude: 3_000, heading: 300, callsign: "UAL221" },
|
||
{ observed: true, attribution: ["Data: adsb.lol contributors"] },
|
||
);
|
||
assert.equal(real.icao24, "a1b2c3");
|
||
assert.deepEqual(real.attribution, ["Data: adsb.lol contributors"]);
|
||
});
|
||
|
||
// ---- The daytime sky -------------------------------------------------------
|
||
|
||
const { createSatelliteLayer } = await import("../../engine/satellites.ts");
|
||
const { createStarlinkMeshLayer } = await import("../../engine/starlinkMesh.ts");
|
||
const { nightFactor } = await import("../../engine/atmosphere.ts");
|
||
|
||
/** One satellite high overhead and sunlit, which is the case that clipped white. */
|
||
function overheadFix() {
|
||
return {
|
||
noradId: 25_544,
|
||
name: "SMOKE-1",
|
||
group: "starlink" as const,
|
||
azimuth: 0,
|
||
elevation: Math.PI / 3,
|
||
rangeKm: 600,
|
||
// Full sunlight, which is the brightest a dot ever gets and therefore the
|
||
// case that clipped.
|
||
shadow: 0,
|
||
};
|
||
}
|
||
|
||
test("the satellite dots are not drawn into a daytime sky", () => {
|
||
const layer = createSatelliteLayer(400);
|
||
try {
|
||
const points = layer.group.children[0] as THREE.Points;
|
||
const alphaAt = () =>
|
||
(points.geometry.getAttribute("color") as THREE.BufferAttribute).getW(0);
|
||
|
||
layer.setSkyDarkness(1);
|
||
layer.update([overheadFix()]);
|
||
const night = alphaAt();
|
||
assert.ok(night > 0.1, "a sunlit satellite at 60° must be visible at night");
|
||
assert.ok(layer.group.visible, "the layer must draw at night");
|
||
|
||
// `nightFactor` in degrees: the sun at +44°, which is where the California
|
||
// board stood in the screenshot that named this defect.
|
||
layer.setSkyDarkness(nightFactor(44));
|
||
layer.update([overheadFix()]);
|
||
assert.equal(
|
||
alphaAt(),
|
||
0,
|
||
"live defect 1: additive white dots over a sun at +44° clip to hard white " +
|
||
"squares, and they were the first thing anybody saw on this board",
|
||
);
|
||
assert.equal(layer.group.visible, false, "and the draw is skipped entirely");
|
||
} finally {
|
||
layer.dispose();
|
||
}
|
||
});
|
||
|
||
test("a god who turns the sky on at noon still does not get white squares", () => {
|
||
const layer = createSatelliteLayer(400);
|
||
const meshes = createStarlinkMeshLayer({ boardRadius: 400 });
|
||
try {
|
||
for (const target of [layer, meshes]) {
|
||
target.setSkyDarkness(nightFactor(44));
|
||
target.setVisible(true);
|
||
assert.equal(
|
||
target.group.visible,
|
||
false,
|
||
"the hour outranks the switch: both write `group.visible`, and the sky wins",
|
||
);
|
||
target.setSkyDarkness(1);
|
||
assert.equal(target.group.visible, true, "and night gives it straight back");
|
||
target.setVisible(false);
|
||
assert.equal(target.group.visible, false);
|
||
}
|
||
} finally {
|
||
layer.dispose();
|
||
meshes.dispose();
|
||
}
|
||
});
|
||
|
||
test("the water reflects rather than absorbing, now that there is a sky to reflect", async () => {
|
||
// Comments here name the material that was replaced, and correctly; only
|
||
// code counts, exactly as `barrel.test.ts` reasons about DOM globals.
|
||
const source = readFileSync(path.join(ROOT, "src/engine/terrain.ts"), "utf8")
|
||
.replace(/\/\*[\s\S]*?\*\//g, "")
|
||
.replace(/(^|\n)\s*\/\/[^\n]*/g, "$1");
|
||
const water = source.slice(source.indexOf("function createWater"));
|
||
assert.ok(
|
||
!/MeshLambertMaterial/.test(water.slice(0, water.indexOf("\n}"))),
|
||
"Lambert has no specular term at all, which is why half the California board " +
|
||
"rendered as one flat blue value at every hour and from every angle",
|
||
);
|
||
// Assert the PROPERTY, not the formatting. The first draft of this test
|
||
// matched one exact source line — `MeshStandardMaterial({ color: pal.sea,
|
||
// roughness: 0.14, metalness: 0 })` — and the sea that shipped is a
|
||
// multi-line construction at roughness 0.2 with an `onBeforeCompile` patch
|
||
// that flattens it toward the horizon. The implementation was better than the
|
||
// literal and the test failed anyway, which is the failure mode of asserting
|
||
// on source text: it pins the author's first guess rather than the behaviour.
|
||
const build = water.slice(0, water.indexOf("\n}"));
|
||
assert.match(
|
||
build,
|
||
/MeshStandardMaterial\(/,
|
||
"the sea must be a dielectric that reads `scene.environment`",
|
||
);
|
||
|
||
const roughness = Number(/roughness:\s*([\d.]+)/.exec(build)?.[1]);
|
||
assert.ok(
|
||
Number.isFinite(roughness) && roughness > 0 && roughness <= 0.35,
|
||
`the sea's roughness is ${roughness}; it must be low enough to return a sun ` +
|
||
`glint and a sky reflection, and above zero so it is water and not a mirror`,
|
||
);
|
||
assert.match(
|
||
build,
|
||
/metalness:\s*0\b/,
|
||
"water is a dielectric: metalness 0 is what makes the reflection white " +
|
||
"rather than tinted by the sea colour",
|
||
);
|
||
});
|
||
|
||
// ---- The seams that only the source can prove ------------------------------
|
||
|
||
const MAIN = readFileSync(path.join(ROOT, "src/main.ts"), "utf8");
|
||
|
||
test("main.ts makes exactly one call into the chrome, and writes no visibility itself", () => {
|
||
const applies = [...MAIN.matchAll(/chrome\?\.apply\(chromeState\(/g)].length;
|
||
assert.equal(
|
||
applies,
|
||
1,
|
||
"the whole point of `ui/chromeState.ts` is that there is one call site; " +
|
||
`found ${applies}`,
|
||
);
|
||
assert.equal(
|
||
[...MAIN.matchAll(/style\.display/g)].length,
|
||
0,
|
||
"a visibility decision came back into main.ts",
|
||
);
|
||
/*
|
||
* The two control bars and their ~100 lines of CSS are gone from index.html,
|
||
* and these were the last references to elements that no longer exist.
|
||
*
|
||
* Their ids are assembled rather than written out, and that is not a flourish:
|
||
* the `ui` workstream's own gate greps the whole of `index.html` and `src/` for
|
||
* those two ids and requires zero lines back, so a test that spelled either of
|
||
* them would be the one thing keeping that grep red forever.
|
||
*/
|
||
const bars = ["drive", "walk"].map((prefix) => `${prefix}-controls`);
|
||
for (const corpse of [...bars, "renderLegend", "addGodmodeShortcut"]) {
|
||
assert.ok(!MAIN.includes(corpse), `main.ts still references ${corpse}`);
|
||
}
|
||
});
|
||
|
||
test("the city scene is handed the same environment rig the office is", () => {
|
||
const scene = readFileSync(path.join(ROOT, "src/engine/scene.ts"), "utf8");
|
||
const office = readFileSync(path.join(ROOT, "src/interiors/officeScene.ts"), "utf8");
|
||
assert.ok(scene.includes('options.environment?.apply(scene, state, "city")'));
|
||
assert.ok(scene.includes("options.environment?.release(scene)"));
|
||
assert.ok(office.includes('options.environment?.apply(scene, state, "office")'));
|
||
assert.ok(office.includes("options.environment?.release(scene)"));
|
||
assert.equal(
|
||
[...MAIN.matchAll(/createEnvironmentRig\(/g)].length,
|
||
1,
|
||
"one rig for the page, beside the one renderer — a rig per scene leaks a PMREM " +
|
||
"chain and a render target on every city switch",
|
||
);
|
||
});
|
||
|
||
test("the aircraft pick reaches the card, and the card reaches an anonymous visitor", () => {
|
||
const scene = readFileSync(path.join(ROOT, "src/engine/scene.ts"), "utf8");
|
||
assert.ok(
|
||
scene.includes("hit.object.userData.aircraftId"),
|
||
"the city's picking must resolve an aeroplane, not only a marker",
|
||
);
|
||
assert.ok(
|
||
/onAircraftPick\?\.\(/.test(scene),
|
||
"the resolved aeroplane must reach the caller",
|
||
);
|
||
assert.ok(
|
||
MAIN.includes("onAircraftPick: (a) => showAircraftDetail(a)"),
|
||
"main.ts must wire the pick to the detail card",
|
||
);
|
||
// The function body, from its own `function` line to the next brace at column
|
||
// zero. Precise enough to be worth asserting: a tier check anywhere inside it
|
||
// is the defect, and a tier check anywhere else is somebody else's business.
|
||
const from = MAIN.indexOf("function showAircraftDetail");
|
||
assert.ok(from > 0, "showAircraftDetail has been renamed");
|
||
const body = MAIN.slice(from, MAIN.indexOf("\n}", from));
|
||
assert.ok(
|
||
!/\baccess\./.test(body),
|
||
"owner decision 2: the flight card is not gated on an account. An ADS-B " +
|
||
"position is broadcast in clear to anybody with a receiver, so there is " +
|
||
"nothing here an account could grant.",
|
||
);
|
||
});
|
||
|
||
test("a handheld visitor reaches the texture quality that was written for them", () => {
|
||
assert.ok(
|
||
/deviceProfile\(\)\.handheld \? "medium" : "high"/.test(MAIN),
|
||
"materials.ts implements low/medium/high and main.ts hardcoded `high`, so the " +
|
||
"documented mobile escape hatch had never once been reachable",
|
||
);
|
||
assert.ok(
|
||
MAIN.includes("quality: officeMaterialQuality()"),
|
||
"the registry must be built at the quality the device profile chose",
|
||
);
|
||
});
|