1
0

fix: a harbour member is a true-metre object, not a scene-unit floor

`size(metres, floor)` took `floor` in scene units, tuned at 391 m to
the unit. On the merged California board those floors always won: a
340 m channel became 1.05 km. Floors are the metres those constants
measured on SoCal, so that board does not move and every other board
draws the same stone.

`portScale.test.ts` is the property that would have caught it: two
boards five times apart draw the same channel on the water.
This commit is contained in:
2026-08-24 22:58:28 -07:00
parent 2c59b2cfce
commit bda7a61acd
3 changed files with 185 additions and 35 deletions
+131
View File
@@ -0,0 +1,131 @@
/**
* A harbour member is a true-metre object, and every board has to agree.
*
* `size(metres, floor)` used to take `floor` in scene units, tuned at 391 m to
* the unit on Southern California. On the merged California board (1,919 m/unit)
* those floors always won: a 340 m channel became 1.05 km, and San Pedro Bay
* read as a river. The floor is metres now. This is the property that would
* have caught it: two boards five times apart draw the same channel on the
* water.
*/
import assert from "node:assert/strict";
import test from "node:test";
import * as THREE from "three";
import { createPorts } from "../../engine/ports.ts";
import type { Port } from "../../engine/types.ts";
import type { World } from "../../engine/world.ts";
/**
* One straight channel and one gantry. Widths come from the kit, not from the
* pack, so the fixture only has to exist — two points of water, two points of
* rail.
*/
const SAMPLE: Port = {
id: "USXXX",
name: "Sample",
lat: 33.74,
lng: -118.27,
harborType: "CB",
channel: [
[33.74, -118.27],
[33.75, -118.27],
],
cranes: [
{
from: [33.74, -118.26],
to: [33.741, -118.26],
count: 1,
bearing: 90,
height: 82,
outreach: 70,
idleFraction: 0,
},
],
};
function board(latScale: number): World {
const centre = { lat: 33.82, lng: -118.05 };
const lngScale = latScale * Math.cos((centre.lat * Math.PI) / 180);
const metresPerUnit = 111_320 / latScale;
return {
project(lat: number, lng: number): [number, number] {
return [(lng - centre.lng) * lngScale, -(lat - centre.lat) * latScale];
},
groundAt(): number {
return 0;
},
metres(value: number): number {
return (value / metresPerUnit) * 3.4;
},
metresPerUnit,
} as unknown as World;
}
function namedMesh(root: THREE.Object3D, name: string): THREE.Mesh {
let found: THREE.Mesh | undefined;
root.traverse((object) => {
if (object instanceof THREE.Mesh && object.name === name) found = object;
});
assert.ok(found, `no mesh named ${name}`);
return found;
}
/** The dredged strip's width, in metres of real water. */
function channelMetres(world: World): number {
const mesh = namedMesh(createPorts(world, [SAMPLE]), "ports:channel");
const pos = mesh.geometry.getAttribute("position");
assert.ok(pos && pos.count >= 2, "the channel strip has no rails to measure");
const width =
Math.hypot(pos.getX(0) - pos.getX(1), pos.getZ(0) - pos.getZ(1)) * world.metresPerUnit;
return width;
}
/**
* Distance between the two portal legs of the one gantry, in metres.
*
* `pushCraneMatrices` emits the waterside leg first and the landside leg next,
* `gauge` apart along the boom. Measuring the drawn matrices is the same move
* `craneLights.test.ts` makes for the apex: a helper that recomputes the
* number would pass while the mesh drifted.
*/
function craneGaugeMetres(world: World): number {
const mesh = namedMesh(createPorts(world, [SAMPLE]), "ports:cranes") as THREE.InstancedMesh;
assert.ok(mesh.count >= 2, "a gantry is at least two portal legs");
const a = new THREE.Matrix4();
const b = new THREE.Matrix4();
const pa = new THREE.Vector3();
const pb = new THREE.Vector3();
mesh.getMatrixAt(0, a);
mesh.getMatrixAt(1, b);
pa.setFromMatrixPosition(a);
pb.setFromMatrixPosition(b);
return Math.hypot(pa.x - pb.x, pa.z - pb.z) * world.metresPerUnit;
}
test("San Pedro's channel is the same width on a 391 m board and a 1,919 m one", () => {
const metro = channelMetres(board(285));
const state = channelMetres(board(58));
assert.ok(
Math.abs(metro - state) < 2,
`channel is ${metro.toFixed(0)} m at Los Angeles and ${state.toFixed(0)} m on California`,
);
assert.ok(
metro > 300 && metro < 400,
`channel measures ${metro.toFixed(0)} m; that is not a river`,
);
});
test("a gantry's rail gauge is the same metres on both boards", () => {
const metro = craneGaugeMetres(board(285));
const state = craneGaugeMetres(board(58));
assert.ok(
Math.abs(metro - state) < 1,
`gauge is ${metro.toFixed(1)} m at Los Angeles and ${state.toFixed(1)} m on California`,
);
assert.ok(
metro > 30 && metro < 60,
`gauge measures ${metro.toFixed(1)} m; that is not a city block`,
);
});