Files
CodeNavigator/web/camera.js
T
Andy Dixon ae8b0a6b80 Add City mode: codebase laid out as streets and buildings
city.js lays the directory tree out at human scale: top-level folders
become districts separated by 18 m avenues, nested folders become
blocks with narrower streets and raised pavements, and files become
buildings whose footprint follows their lines and whose height follows
their complexity. The squarified treemap moves into city.js and is
shared with the landscape.

The renderer gains a second instance layer with per-instance kind,
code-atlas rectangle and selection flag, a helicopter camera in metres,
screen-to-world rays, and geometry lookup per mode. City mode supports
orbit, pan, dolly, ray picking, focusing a building and district names.
Node tests cover layout invariants, heights, ray/box hits and the
spatial index.
2026-05-10 15:50:23 +01:00

75 lines
4.1 KiB
JavaScript

// Minimal column-major 4x4 matrix helpers and the cameras built on them. World axes: x right,
// y "down" the 2D map, z up. Every 3D view renders and projects through one view-projection
// matrix, so WebGL, the overlay and picking agree exactly.
// The world is left-handed (y runs down the map, z up), so x is negated to keep the map unmirrored.
export function perspective(fovY, aspect, near, far) {
const f = 1 / Math.tan(fovY / 2), nf = 1 / (near - far);
return new Float32Array([-f / aspect, 0, 0, 0, 0, f, 0, 0, 0, 0, (far + near) * nf, -1, 0, 0, 2 * far * near * nf, 0]);
}
export function lookAt([ex, ey, ez], [tx, ty, tz], [ux, uy, uz] = [0, 0, 1]) {
let zx = ex - tx, zy = ey - ty, zz = ez - tz;
let len = Math.hypot(zx, zy, zz) || 1; zx /= len; zy /= len; zz /= len;
let xx = uy * zz - uz * zy, xy = uz * zx - ux * zz, xz = ux * zy - uy * zx;
len = Math.hypot(xx, xy, xz);
if (len < 1e-6) { xx = 1; xy = 0; xz = 0; len = 1; } // looking straight along up
xx /= len; xy /= len; xz /= len;
const yx = zy * xz - zz * xy, yy = zz * xx - zx * xz, yz = zx * xy - zy * xx;
return new Float32Array([
xx, yx, zx, 0, xy, yy, zy, 0, xz, yz, zz, 0,
-(xx * ex + xy * ey + xz * ez), -(yx * ex + yy * ey + yz * ez), -(zx * ex + zy * ey + zz * ez), 1,
]);
}
export function multiply(a, b) {
const out = new Float32Array(16);
for (let c = 0; c < 4; c++) for (let r = 0; r < 4; r++) {
out[c * 4 + r] = a[r] * b[c * 4] + a[4 + r] * b[c * 4 + 1] + a[8 + r] * b[c * 4 + 2] + a[12 + r] * b[c * 4 + 3];
}
return out;
}
// Clip coordinates of a world point.
export function transform(m, x, y, z) {
return [
m[0] * x + m[4] * y + m[8] * z + m[12],
m[1] * x + m[5] * y + m[9] * z + m[13],
m[2] * x + m[6] * y + m[10] * z + m[14],
m[3] * x + m[7] * y + m[11] * z + m[15],
];
}
// Unit view direction for a yaw (around z, 0 = looking toward -y) and pitch (up positive).
export function direction(yaw, pitch) {
return [-Math.sin(yaw) * Math.cos(pitch), -Math.cos(yaw) * Math.cos(pitch), Math.sin(pitch)];
}
// Orbit camera matching the original landscape projection: focal length 2.7, world scaled by
// 1/scale, with the view lifted by `lift` along the camera's up axis. `pitch` is the elevation.
export function orbitPose({ x, y, yaw, pitch, distance }, scale = 420, lift = .35) {
const cy = Math.cos(yaw), sy = Math.sin(yaw), cp = Math.cos(pitch), sp = Math.sin(pitch), d = distance * scale;
const up = [-sy * sp, -cy * sp, cp], shift = lift * scale;
const target = [x + up[0] * shift, y + up[1] * shift, up[2] * shift];
return { eye: [target[0] + sy * cp * d, target[1] + cy * cp * d, target[2] + sp * d], target, up };
}
export const ORBIT_FOV = 2 * Math.atan(1 / 2.7);
// General 4x4 inverse (null when singular), for turning screen points back into world rays.
export function invert(m) {
const [a00, a01, a02, a03, a10, a11, a12, a13, a20, a21, a22, a23, a30, a31, a32, a33] = m;
const b00 = a00 * a11 - a01 * a10, b01 = a00 * a12 - a02 * a10, b02 = a00 * a13 - a03 * a10, b03 = a01 * a12 - a02 * a11;
const b04 = a01 * a13 - a03 * a11, b05 = a02 * a13 - a03 * a12, b06 = a20 * a31 - a21 * a30, b07 = a20 * a32 - a22 * a30;
const b08 = a20 * a33 - a23 * a30, b09 = a21 * a32 - a22 * a31, b10 = a21 * a33 - a23 * a31, b11 = a22 * a33 - a23 * a32;
let det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;
if (!det) return null;
det = 1 / det;
return new Float32Array([
(a11 * b11 - a12 * b10 + a13 * b09) * det, (a02 * b10 - a01 * b11 - a03 * b09) * det, (a31 * b05 - a32 * b04 + a33 * b03) * det, (a22 * b04 - a21 * b05 - a23 * b03) * det,
(a12 * b08 - a10 * b11 - a13 * b07) * det, (a00 * b11 - a02 * b08 + a03 * b07) * det, (a32 * b02 - a30 * b05 - a33 * b01) * det, (a20 * b05 - a22 * b02 + a23 * b01) * det,
(a10 * b10 - a11 * b08 + a13 * b06) * det, (a01 * b08 - a00 * b10 - a03 * b06) * det, (a30 * b04 - a31 * b02 + a33 * b00) * det, (a21 * b02 - a20 * b04 - a23 * b00) * det,
(a11 * b07 - a10 * b09 - a12 * b06) * det, (a00 * b09 - a01 * b07 + a02 * b06) * det, (a31 * b01 - a30 * b03 - a32 * b00) * det, (a20 * b03 - a21 * b01 + a22 * b00) * det,
]);
}