// SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2026 Andy Dixon // 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, ]); }