fuzzyworld

tower / files / src / tour / geo.ts

geo.ts

5.5 KB · history · blame

1/**
2 * Geometry for the scenic tour, inside Tower. See world-generation.md §19.
3 *
4 * Two ways of saying where something is. On the floor, (z, a, altM): metres along the
5 * axis, metres around it, and height above the datum — what the world's features are
6 * written in. And in the tower's own frame, a vector in metres with the axis along z —
7 * what aiming, distances and the camera need, since the floor curves up all round and a
8 * straight line through the air is not a line on the map.
9 */
10import { CIRCUMFERENCE_M, RADIUS_M } from '../canon/constants.js'
11import { type Line, nearestA } from '../terrain/shapes.js'
12
13export interface At { z: number; a: number; altM: number }
14
15/** A vector in the tower's frame, metres: x and y across the axis, z along it. */
16export interface V3 { x: number; y: number; z: number }
17
18export const v3 = (x: number, y: number, z: number): V3 => ({ x, y, z })
19export const add = (p: V3, q: V3): V3 => v3(p.x + q.x, p.y + q.y, p.z + q.z)
20export const sub = (p: V3, q: V3): V3 => v3(p.x - q.x, p.y - q.y, p.z - q.z)
21export const scale = (p: V3, k: number): V3 => v3(p.x * k, p.y * k, p.z * k)
22export const dot = (p: V3, q: V3): number => p.x * q.x + p.y * q.y + p.z * q.z
23export const len = (p: V3): number => Math.hypot(p.x, p.y, p.z)
24export const cross = (p: V3, q: V3): V3 =>
25 v3(p.y * q.z - p.z * q.y, p.z * q.x - p.x * q.z, p.x * q.y - p.y * q.x)
26export const norm = (p: V3): V3 => { const l = len(p) || 1; return scale(p, 1 / l) }
27/** p + (q − p)·t. */
28export const mix = (p: V3, q: V3, t: number): V3 => add(p, scale(sub(q, p), t))
29/** Distance from the axis. */
30export const radial = (p: V3): number => Math.hypot(p.x, p.y)
31
32export const wrapA = (a: number): number => ((a % CIRCUMFERENCE_M) + CIRCUMFERENCE_M) % CIRCUMFERENCE_M
33
34/** A point over the floor, in the tower's frame. */
35export function toV(at: At): V3 {
36 const th = at.a / RADIUS_M, r = RADIUS_M - at.altM
37 return v3(r * Math.cos(th), r * Math.sin(th), at.z)
38}
39
40/** And back. `a` comes out in [0, circumference). */
41export function fromV(p: V3): At {
42 return { z: p.z, a: wrapA(Math.atan2(p.y, p.x) * RADIUS_M), altM: RADIUS_M - radial(p) }
43}
44
45/** Up (toward the axis), spinward and deepward, at a point. */
46export function basisAt(p: V3): { up: V3; spin: V3; deep: V3 } {
47 const th = Math.atan2(p.y, p.x)
48 return { up: v3(-Math.cos(th), -Math.sin(th), 0), spin: v3(-Math.sin(th), Math.cos(th), 0), deep: v3(0, 0, 1) }
49}
50
51/** A direction as the interior view's heading and pitch, at the point it is seen from. */
52export function yawPitch(from: V3, dir: V3): { yaw: number; pitch: number } {
53 const { up, spin, deep } = basisAt(from)
54 const d = norm(dir)
55 return { yaw: Math.atan2(dot(d, spin), dot(d, deep)), pitch: Math.asin(Math.max(-1, Math.min(1, dot(d, up)))) }
56}
57
58/** The direction a heading and pitch point, from a point. */
59export function dirOf(from: V3, yaw: number, pitch: number): V3 {
60 const { up, spin, deep } = basisAt(from)
61 return add(add(scale(deep, Math.cos(pitch) * Math.cos(yaw)), scale(spin, Math.cos(pitch) * Math.sin(yaw))),
62 scale(up, Math.sin(pitch)))
63}
64
65/**
66 * The roll that turns a camera looking along `dir` with the floor's up as its up into one
67 * whose up is `want`: the angle for three's rotateZ, about the camera's own view axis.
68 */
69export function rollTo(from: V3, dir: V3, want: V3): number {
70 const { up } = basisAt(from)
71 const f = norm(dir)
72 const right = norm(cross(f, up))
73 const camUp = cross(right, f)
74 return Math.atan2(-dot(want, right), dot(want, camUp))
75}
76
77/**
78 * Heading and pitch from one point to another, on the curved floor. The floor curves up
79 * around the axis, so a summit 6 km spinward stands about 360 m higher in the eye's frame
80 * than its elevation says, and a pitch taken on a flat floor would miss it.
81 */
82export function lookAt(from: At, to: At): { yaw: number; pitch: number } {
83 const p = toV(from)
84 const q = toV({ ...to, a: nearestA(to.a, from.a) })
85 return yawPitch(p, sub(q, p))
86}
87
88/** A point `s` metres along a line, clamped to its ends. */
89export function along(line: Line, s: number): { z: number; a: number } {
90 const n = line.s.length
91 if (s <= 0) return { z: line.pts[0]!, a: line.pts[1]! }
92 if (s >= line.s[n - 1]!) return { z: line.pts[2 * n - 2]!, a: line.pts[2 * n - 1]! }
93 let lo = 0, hi = n - 1
94 while (hi - lo > 1) {
95 const mid = (lo + hi) >> 1
96 if (line.s[mid]! <= s) lo = mid
97 else hi = mid
98 }
99 const t = (s - line.s[lo]!) / (line.s[hi]! - line.s[lo]! || 1)
100 return {
101 z: line.pts[2 * lo]! + (line.pts[2 * hi]! - line.pts[2 * lo]!) * t,
102 a: line.pts[2 * lo + 1]! + (line.pts[2 * hi + 1]! - line.pts[2 * lo + 1]!) * t,
103 }
104}
105
106/** A straight line from (z, a), heading `dirRad` (deepward toward spinward), `length` metres. */
107export function straight(z: number, a: number, dirRad: number, length: number): Line {
108 const pts: number[] = [], s: number[] = []
109 for (let d = 0; d <= length; d += 50) {
110 pts.push(z + Math.cos(dirRad) * d, a + Math.sin(dirRad) * d)
111 s.push(d)
112 }
113 return { pts, s, box: { z0: 0, z1: 0, a0: 0, a1: 0 } }
114}
115
116/** How far along a line its nearest point to (z, a) is. */
117export function sAlong(line: Line, z: number, a: number): number {
118 let best = 0, bestD = Infinity
119 for (let i = 0; i < line.s.length; i++) {
120 const d = Math.hypot(line.pts[2 * i]! - z, nearestA(line.pts[2 * i + 1]!, a) - a)
121 if (d < bestD) { bestD = d; best = i }
122 }
123 return line.s[best]!
124}
125
126/** Smoothstep, for easing one aim into the next. */
127export const ease = (t: number): number => { const u = Math.max(0, Math.min(1, t)); return u * u * (3 - 2 * u) }