1
0

California gets roads, traffic, and a car to follow

This commit is contained in:
2026-08-11 18:24:53 -07:00
parent 9c9e78f6f9
commit fe58290728
43 changed files with 5593 additions and 68 deletions
+54 -2
View File
@@ -12,7 +12,7 @@ import type { Bridge, LatLng } from "./types.ts";
import type { World } from "./world.ts";
/** Resample a lat/lng path into scene-space points that ride the ground. */
function drapePath(world: World, path: LatLng[], samplesPerLeg = 14, lift = 0.05): THREE.Vector3[] {
function drapePath(world: World, path: LatLng[], samplesPerLeg = 14, lift = 0.14): THREE.Vector3[] {
const out: THREE.Vector3[] = [];
for (let i = 0; i < path.length - 1; i++) {
const from = path[i];
@@ -40,12 +40,64 @@ function ribbon(points: THREE.Vector3[], width: number, color: number): THREE.Me
return mesh;
}
/** A draped, flat road deck. A tube turns a freeway into a raised pipeline. */
function roadRibbon(
points: readonly THREE.Vector3[],
width: number,
color: number,
lift = 0,
): THREE.Mesh {
const positions: number[] = [];
const normals: number[] = [];
const indices: number[] = [];
const half = width / 2;
for (let index = 0; index < points.length; index += 1) {
const point = points[index];
const previous = points[Math.max(0, index - 1)];
const next = points[Math.min(points.length - 1, index + 1)];
if (!point || !previous || !next) continue;
const dx = next.x - previous.x;
const dz = next.z - previous.z;
const length = Math.hypot(dx, dz) || 1;
const nx = -dz / length;
const nz = dx / length;
positions.push(
point.x + nx * half, point.y + lift, point.z + nz * half,
point.x - nx * half, point.y + lift, point.z - nz * half,
);
normals.push(0, 1, 0, 0, 1, 0);
if (index < points.length - 1) {
const a = index * 2;
indices.push(a, a + 2, a + 1, a + 1, a + 2, a + 3);
}
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
geometry.setAttribute("normal", new THREE.Float32BufferAttribute(normals, 3));
geometry.setIndex(indices);
geometry.computeBoundingSphere();
const mesh = new THREE.Mesh(
geometry,
new THREE.MeshLambertMaterial({ color, side: THREE.DoubleSide }),
);
mesh.receiveShadow = true;
return mesh;
}
export function createRoads(world: World): THREE.Group {
const group = new THREE.Group();
group.name = "roads";
for (const road of world.city.roads) {
const color = road.kind === "freeway" ? 0x7d7166 : 0x8b8578;
group.add(ribbon(drapePath(world, road.path), road.width, color));
const path = drapePath(world, road.path);
group.add(roadRibbon(path, road.width, color));
if (road.kind === "freeway") {
// One warm median stroke is enough at corridor scale to read as divided
// highway without spending a textured asset or a draw call per lane.
group.add(roadRibbon(path, Math.max(0.025, road.width * 0.035), 0xd7c27c, 0.012));
}
}
return group;
}