fix: a bridge member is a true-metre object, not a scene-unit floor
size(metres, floor) took floor in scene units, tuned at 94 m to the unit. On one California (1,919 m/unit) those floors always won: a 21 m deck half became 173 m and the Golden Gate read as a highway. The same bug the metre conversion fixed for the Vincent Thomas at 391 m/unit, reintroduced by the floor. Floors are metres now — the Bay-board values, so that board does not move. Two boards twenty times apart draw the same 21 m half. Lamp pitch follows.
This commit is contained in:
+38
-23
@@ -61,10 +61,10 @@
|
||||
* `socal.ts` asked for this in a comment: bridge members used to be constants in
|
||||
* scene units tuned at San Francisco's 94 m per unit, so at Los Angeles' 391 the
|
||||
* Vincent Thomas came out four times too heavy. Every cross-section here is
|
||||
* `metres / metresPerUnit` instead — with a minimum, because true scale on the
|
||||
* SoCal board puts the deck of the Long Beach Gateway below one pixel and an
|
||||
* invisible bridge is a worse answer than a chunky one. Heights are the
|
||||
* exception and stay on `world.metres()`, which carries the board's vertical
|
||||
* `metres / metresPerUnit` instead — with a **metre** floor, because a floor in
|
||||
* scene units is a floor that assumes one board. 0.09 units is 8.5 m on the Bay
|
||||
* and 173 m on merged California: the Golden Gate's deck became a highway.
|
||||
* Heights stay on `world.metres()`, which carries the board's vertical
|
||||
* exaggeration: a deck has to sit at the same exaggerated height as the hills it
|
||||
* lands on or it lands inside them.
|
||||
*/
|
||||
@@ -272,8 +272,9 @@ function stations(world: BridgeWorld, path: LatLng[], spacing: number): Station[
|
||||
* hangers are quantised to, and a coarser one leaves the Golden Gate with nine
|
||||
* hangers a span and a cable made of straight lines.
|
||||
*/
|
||||
function stationSpacing(deckHalf: number): number {
|
||||
return Math.max(deckHalf * 4, 0.28);
|
||||
function stationSpacing(deckHalf: number, perUnit: number): number {
|
||||
// 26 m was 0.28 scene units on the Bay board, the floor this was tuned on.
|
||||
return Math.max(deckHalf * 4, 26 / perUnit);
|
||||
}
|
||||
|
||||
/** A point offset laterally from a station, at a given height. */
|
||||
@@ -524,7 +525,15 @@ function layout(world: BridgeWorld, bridge: Bridge): BridgeLayout {
|
||||
// `world.metres`, which carries the board's vertical exaggeration. See the
|
||||
// module comment for why those two are not the same conversion.
|
||||
const perUnit = world.metresPerUnit;
|
||||
const size = (metres: number, floor: number) => Math.max(metres / perUnit, floor);
|
||||
/**
|
||||
* Cross-section in metres, never thinner than `floorM` metres.
|
||||
*
|
||||
* The second argument used to be scene units, tuned at 94 m/unit. On a board
|
||||
* 1,919 m to the unit those floors always won: a 21 m deck half became 173 m.
|
||||
* Floors are the metres those old constants measured on the Bay, so that
|
||||
* board does not move and every other board draws the same steel.
|
||||
*/
|
||||
const size = (metres: number, floorM: number) => Math.max(metres, floorM) / perUnit;
|
||||
// Three of these are wider than the real member, and the reason is the
|
||||
// board's vertical exaggeration. San Francisco draws height at 3.6× and distance at
|
||||
// 1×, so a tower at true scale is 227 m tall and 12 m thick — a 62:1 needle
|
||||
@@ -536,13 +545,13 @@ function layout(world: BridgeWorld, bridge: Bridge): BridgeLayout {
|
||||
// saddle) and where nothing else needs the room. The deck follows at 42 m
|
||||
// against a true 27, which is the same allowance `socal.ts` records for its
|
||||
// landmarks: at this scale a true-width deck is under two pixels.
|
||||
const deckHalf = size(21, 0.09);
|
||||
const deckDepth = size(14, 0.04);
|
||||
const legWidth = size(20, 0.05);
|
||||
const legDepth = size(46, 0.1);
|
||||
const cableRadius = size(8, 0.015);
|
||||
const hangerHalf = size(1.9, 0.005);
|
||||
const pierHalf = size(8, 0.025);
|
||||
const deckHalf = size(21, 8.5);
|
||||
const deckDepth = size(14, 3.8);
|
||||
const legWidth = size(20, 4.7);
|
||||
const legDepth = size(46, 9.4);
|
||||
const cableRadius = size(8, 1.4);
|
||||
const hangerHalf = size(1.9, 0.5);
|
||||
const pierHalf = size(8, 2.4);
|
||||
|
||||
const deckY = world.metres(bridge.deckHeight);
|
||||
const towerY = world.metres(bridge.towerHeight);
|
||||
@@ -564,7 +573,7 @@ function layout(world: BridgeWorld, bridge: Bridge): BridgeLayout {
|
||||
const footing = (ground: number, margin: number) =>
|
||||
Math.max(ground, seabed) - world.metres(margin);
|
||||
|
||||
const list = stations(world, bridge.path, stationSpacing(deckHalf));
|
||||
const list = stations(world, bridge.path, stationSpacing(deckHalf, perUnit));
|
||||
const towerAt = bridge.towers.map((tower) => nearestStation(world, list, tower));
|
||||
const reaches = classify(list, towerAt, bridge.towerHeight, perUnit);
|
||||
const runs = [
|
||||
@@ -580,11 +589,11 @@ function layout(world: BridgeWorld, bridge: Bridge): BridgeLayout {
|
||||
// has something to land on, and a deck held rigidly at 67 m simply disappeared
|
||||
// into the Presidio bluff with no visible touchdown.
|
||||
const total = list[list.length - 1]?.along ?? 0;
|
||||
const rampLength = Math.min(total * 0.18, Math.max(deckHalf * 12, 1.2));
|
||||
const rampLength = Math.min(total * 0.18, Math.max(deckHalf * 12, 113 / perUnit));
|
||||
const deckTopAt = (station: Station): number => {
|
||||
const fromEnd = Math.min(station.along, total - station.along);
|
||||
if (fromEnd >= rampLength) return deckY;
|
||||
const landing = Math.max(station.ground + deckDepth + size(4, 0.01), 0);
|
||||
const landing = Math.max(station.ground + deckDepth + size(4, 0.9), 0);
|
||||
if (landing >= deckY) return deckY;
|
||||
const t = rampLength <= 0 ? 1 : fromEnd / rampLength;
|
||||
// Smoothstep, so the deck leaves the shore tangentially rather than kinking.
|
||||
@@ -758,7 +767,7 @@ export function buildBridge(
|
||||
}
|
||||
|
||||
// ---- Cables and hangers ----
|
||||
const hangerSpacing = Math.max(deckHalf * 3.4, size(60, 0.22));
|
||||
const hangerSpacing = Math.max(deckHalf * 3.4, size(60, 21));
|
||||
for (const chain of runs) {
|
||||
for (const side of [-1, 1] as const) {
|
||||
const lateral = side * deckHalf;
|
||||
@@ -852,7 +861,7 @@ export function buildBridge(
|
||||
// One every so often under an approach, and none at all where the ground has
|
||||
// already risen to the deck: that is what walks the Bay Bridge onto Yerba
|
||||
// Buena instead of standing it on stilts over the top of the island.
|
||||
const pierSpacing = Math.max(deckHalf * 9, size(240, 0.7));
|
||||
const pierSpacing = Math.max(deckHalf * 9, size(240, 66));
|
||||
for (const reach of reaches) {
|
||||
if (reach.kind !== "approach") continue;
|
||||
let last = -Infinity;
|
||||
@@ -895,12 +904,16 @@ export interface BridgePlan {
|
||||
towerStations: number[];
|
||||
stationCount: number;
|
||||
deckLength: number;
|
||||
/** Deck half-width in true metres. Same on every board; see `bridgeScale.test.ts`. */
|
||||
deckHalfMetres: number;
|
||||
}
|
||||
|
||||
export function planBridge(world: BridgeWorld, bridge: Bridge): BridgePlan {
|
||||
const perUnit = world.metresPerUnit;
|
||||
const deckHalf = Math.max(15 / perUnit, 0.075);
|
||||
const list = stations(world, bridge.path, stationSpacing(deckHalf));
|
||||
// Same 21 m half as `layout`. A second, smaller number here meant the
|
||||
// classifier sampled a different deck from the one that was drawn.
|
||||
const deckHalf = Math.max(21, 8.5) / perUnit;
|
||||
const list = stations(world, bridge.path, stationSpacing(deckHalf, perUnit));
|
||||
const towerAt = bridge.towers.map((tower) => nearestStation(world, list, tower));
|
||||
const reaches = classify(list, towerAt, bridge.towerHeight, perUnit);
|
||||
return {
|
||||
@@ -911,6 +924,7 @@ export function planBridge(world: BridgeWorld, bridge: Bridge): BridgePlan {
|
||||
towerStations: towerAt,
|
||||
stationCount: list.length,
|
||||
deckLength: list[list.length - 1]?.along ?? 0,
|
||||
deckHalfMetres: deckHalf * perUnit,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -981,7 +995,8 @@ export interface BridgeLights {
|
||||
* whole Vincent Thomas.
|
||||
*/
|
||||
const DECK_LAMP_SPACING_M = 50;
|
||||
const DECK_LAMP_MIN_SPACING = 0.16;
|
||||
/** 0.16 scene units was 15 m on the Bay; a scene-unit min is 307 m on California. */
|
||||
const DECK_LAMP_MIN_SPACING_M = 15;
|
||||
|
||||
/** Lamp height above the deck slab, and the head light's clearance over the saddle. */
|
||||
const DECK_LAMP_HEIGHT_M = 12;
|
||||
@@ -999,7 +1014,7 @@ export function bridgeLights(world: BridgeWorld, bridge: Bridge): BridgeLights {
|
||||
// is set by what a *cable* needs and is three times coarser under an approach,
|
||||
// so one lamp per station would thin out over exactly the causeways that have
|
||||
// nothing else to look at.
|
||||
const spacing = Math.max(DECK_LAMP_SPACING_M / world.metresPerUnit, DECK_LAMP_MIN_SPACING);
|
||||
const spacing = Math.max(DECK_LAMP_SPACING_M, DECK_LAMP_MIN_SPACING_M) / world.metresPerUnit;
|
||||
const lift = world.metres(DECK_LAMP_HEIGHT_M);
|
||||
const total = list[list.length - 1]?.along ?? 0;
|
||||
const kerb = deckHalf * 0.92;
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* A bridge member is a true-metre object, and every board has to agree.
|
||||
*
|
||||
* `size(metres, floor)` used to take `floor` in scene units, tuned at 94 m to
|
||||
* the unit. On the merged California board (1,919 m/unit) those floors always
|
||||
* won: a 21 m deck half became 173 m, and the Golden Gate read as a highway.
|
||||
* The floor is metres now. This is the property that would have caught it:
|
||||
* two boards twenty times apart draw the same deck width on the ground.
|
||||
*/
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { planBridge } from "../../engine/bridges.ts";
|
||||
import type { Bridge } from "../../engine/types.ts";
|
||||
import type { World } from "../../engine/world.ts";
|
||||
|
||||
const GOLDEN_GATE: Bridge = {
|
||||
name: "Golden Gate Bridge",
|
||||
path: [
|
||||
[37.8025, -122.4752],
|
||||
[37.8155, -122.4783],
|
||||
[37.825, -122.479],
|
||||
[37.8375, -122.4806],
|
||||
],
|
||||
towers: [
|
||||
[37.8155, -122.4783],
|
||||
[37.825, -122.479],
|
||||
],
|
||||
towerHeight: 227,
|
||||
deckHeight: 67,
|
||||
sag: 0.55,
|
||||
color: 0xc0442c,
|
||||
};
|
||||
|
||||
function board(latScale: number): World {
|
||||
const centre = { lat: 37.7749, lng: -122.4194 };
|
||||
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;
|
||||
},
|
||||
metresPerUnit,
|
||||
} as unknown as World;
|
||||
}
|
||||
|
||||
test("the Golden Gate's deck is the same width on a 94 m board and a 1,919 m one", () => {
|
||||
const metro = planBridge(board(1180), GOLDEN_GATE);
|
||||
const state = planBridge(board(58), GOLDEN_GATE);
|
||||
assert.ok(
|
||||
Math.abs(metro.deckHalfMetres - state.deckHalfMetres) < 0.5,
|
||||
`deck half is ${metro.deckHalfMetres.toFixed(1)} m on the Bay and ` +
|
||||
`${state.deckHalfMetres.toFixed(1)} m on California`,
|
||||
);
|
||||
assert.ok(
|
||||
metro.deckHalfMetres > 15 && metro.deckHalfMetres < 30,
|
||||
`deck half measures ${metro.deckHalfMetres.toFixed(1)} m; that is not a highway`,
|
||||
);
|
||||
});
|
||||
@@ -179,12 +179,17 @@ test("a coarser board gets a smaller lamp, because it draws a smaller bridge", (
|
||||
// San Francisco at 94 m to the unit against Los Angeles at 391. A sprite size
|
||||
// in scene units tuned on the first would be several times the width of the
|
||||
// deck on the second, which is the bug `socal.ts` asked the kit to stop having.
|
||||
// The floor is metres now, so both boards draw the same 21 m half and the
|
||||
// coarser one is smaller in scene units, not clamped to 0.09 (173 m on CA).
|
||||
const fine = bridgeLights(board(1180, 3.6), GOLDEN_GATE);
|
||||
const coarse = bridgeLights(board(285, 2.2), GOLDEN_GATE);
|
||||
assert.ok(coarse.scale < fine.scale, "a coarser board draws a narrower deck");
|
||||
assert.ok(coarse.scale >= 0.09, "…but never below the legibility floor");
|
||||
// The run still reaches both ends of the crossing; it is the pitch floor that
|
||||
// keeps a 391 m/unit board from putting four lamps on a whole bridge.
|
||||
const fineM = fine.scale * (111_320 / 1180);
|
||||
const coarseM = coarse.scale * (111_320 / 285);
|
||||
assert.ok(
|
||||
Math.abs(fineM - coarseM) < 0.5,
|
||||
`deck half is ${fineM.toFixed(1)} m on the Bay and ${coarseM.toFixed(1)} m on SoCal`,
|
||||
);
|
||||
assert.ok(triples(coarse.deck).length >= 8, "a coarse board still gets a run, not a handful");
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user