export const add = (a, b) => a.map((v, i) => v + b[i]); export const sub = (a, b) => a.map((v, i) => v - b[i]); export const mul = (a, s) => a.map((v) => v * s); export const dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; export const cross = (a, b) => [ a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0], ]; export const unit = (a) => mul(a, 1 / (Math.hypot(...a) || 1)); export const point = (p, i) => Array.from(p.subarray(i * 3, i * 3 + 3)); export const clamp = (n, a, b) => Math.max(a, Math.min(b, n)); // Bound apparent scale along the viewport's shorter side, in either orientation. export function clampZoom(zoom, rect) { const scale = Math.min(1, rect.width / rect.height); return clamp(zoom * scale, 0.2, 14) / scale; } // Orthographic camera: the same basis drives rendering, rays and grab planes. export function cameraBasis(yaw, pitch) { const forward = [ Math.sin(yaw) * Math.cos(pitch), Math.sin(pitch), Math.cos(yaw) * Math.cos(pitch), ]; // Analytic basis remains defined at the top/bottom cardinal views. const right = [Math.cos(yaw), 0, -Math.sin(yaw)]; return { right, up: cross(forward, right), forward }; }