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:
@@ -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`,
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user