127 lines
4.3 KiB
TypeScript
127 lines
4.3 KiB
TypeScript
/** Deterministic, renderer-independent authored detail for freeway corridors. */
|
|
|
|
import type { TransportPack } from "./types.ts";
|
|
import { buildRoutePath, sampleRoute } from "./vehicleSim.ts";
|
|
|
|
export type FreewayIdentity = "us-highway" | "interstate";
|
|
export type RoadsideFeatureKind = "oak" | "orchard" | "power-pole" | "silo" | "route-sign";
|
|
|
|
export interface FreewayWorldSample {
|
|
distanceM: number;
|
|
lat: number;
|
|
lng: number;
|
|
headingDeg: number;
|
|
elevationPhase: number;
|
|
lanesPerDirection: number;
|
|
}
|
|
|
|
export interface FreewayRoadsideFeature {
|
|
id: string;
|
|
kind: RoadsideFeatureKind;
|
|
distanceM: number;
|
|
side: -1 | 1;
|
|
setbackM: number;
|
|
scale: number;
|
|
}
|
|
|
|
export interface FreewayRoutePlan {
|
|
routeId: string;
|
|
identity: FreewayIdentity;
|
|
shield: "101" | "5";
|
|
character: "coastal-oak" | "central-valley";
|
|
samples: readonly FreewayWorldSample[];
|
|
roadside: readonly FreewayRoadsideFeature[];
|
|
}
|
|
|
|
export interface FreewayWorldPlan {
|
|
seed: number;
|
|
routes: readonly FreewayRoutePlan[];
|
|
}
|
|
|
|
function hash(value: string): number {
|
|
let out = 2_166_136_261;
|
|
for (let index = 0; index < value.length; index += 1) {
|
|
out ^= value.charCodeAt(index);
|
|
out = Math.imul(out, 16_777_619);
|
|
}
|
|
return out >>> 0;
|
|
}
|
|
|
|
function seeded(seed: number): () => number {
|
|
let state = seed >>> 0;
|
|
return () => {
|
|
state += 0x6d2b79f5;
|
|
let value = state;
|
|
value = Math.imul(value ^ (value >>> 15), value | 1);
|
|
value ^= value + Math.imul(value ^ (value >>> 7), value | 61);
|
|
return ((value ^ (value >>> 14)) >>> 0) / 4_294_967_296;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Compile visual-world intent without Three.js or wall-clock state. The plan is
|
|
* suitable for snapshots, replay provenance, server-side validation, and a
|
|
* browser renderer. It intentionally derives only from the open transport pack.
|
|
*/
|
|
export function buildFreewayWorldPlan(
|
|
pack: TransportPack,
|
|
options: { seed?: number; sampleCount?: number } = {},
|
|
): FreewayWorldPlan {
|
|
const seed = options.seed ?? 101_005;
|
|
const sampleCount = Math.max(12, Math.min(128, Math.floor(options.sampleCount ?? 56)));
|
|
const segments = new Map(pack.segments.map((segment) => [segment.id, segment]));
|
|
const routes = pack.routes.map((route): FreewayRoutePlan => {
|
|
const path = buildRoutePath(pack, route.id);
|
|
const first = segments.get(route.segmentIds[0] ?? "");
|
|
const identity: FreewayIdentity = first?.kind === "us-highway" ? "us-highway" : "interstate";
|
|
const rand = seeded(seed ^ hash(route.id));
|
|
const samples = Array.from({ length: sampleCount }, (_, index): FreewayWorldSample => {
|
|
const distanceM = (index / (sampleCount - 1)) * path.lengthM;
|
|
const sample = sampleRoute(path, distanceM);
|
|
const segment = segments.get(sample.segmentId);
|
|
return {
|
|
distanceM,
|
|
lat: sample.lat,
|
|
lng: sample.lng,
|
|
headingDeg: sample.headingDeg,
|
|
// A reproducible visual grade cue. Actual ground height is supplied by
|
|
// the renderer; this phase only varies berm/vegetation rhythm.
|
|
elevationPhase: Math.sin(index * 0.43 + rand() * 0.35),
|
|
lanesPerDirection: Math.max(2, Math.min(5, segment?.lanesPerDirection ?? 2)),
|
|
};
|
|
});
|
|
const featureCount = identity === "us-highway" ? 96 : 88;
|
|
const roadside = Array.from({ length: featureCount }, (_, index): FreewayRoadsideFeature => {
|
|
const sign = index % 23 === 2;
|
|
const power = identity === "interstate" && index % 4 === 0;
|
|
const silo = identity === "interstate" && index % 13 === 7;
|
|
const kind: RoadsideFeatureKind = sign
|
|
? "route-sign"
|
|
: silo
|
|
? "silo"
|
|
: power
|
|
? "power-pole"
|
|
: identity === "us-highway" && index % 3 !== 1
|
|
? "oak"
|
|
: "orchard";
|
|
return {
|
|
id: `${route.id}:${String(index).padStart(2, "0")}`,
|
|
kind,
|
|
distanceM: ((index + 0.7 + rand() * 0.45) / featureCount) * path.lengthM,
|
|
side: rand() < 0.5 ? -1 : 1,
|
|
setbackM: kind === "route-sign" ? 13 : 18 + rand() * 38,
|
|
scale: 0.78 + rand() * 0.58,
|
|
};
|
|
});
|
|
return {
|
|
routeId: route.id,
|
|
identity,
|
|
shield: identity === "us-highway" ? "101" : "5",
|
|
character: identity === "us-highway" ? "coastal-oak" : "central-valley",
|
|
samples,
|
|
roadside,
|
|
};
|
|
});
|
|
return { seed, routes };
|
|
}
|