fuzzyworld

tower / files / src / world / instruments.ts

instruments.ts

2.5 KB · history · blame

1/**
2 * Measuring instruments. See design.md §3.3.
3 *
4 * These are the reason writer mode exists: the author asks questions like "can the
5 * far cap be seen from the midpoint?" and needs an answer that is computed rather
6 * than guessed. Two such answers became canon in design.md §1.8.
7 */
8import { extinctionAtRadius } from '../canon/atmosphere.js'
9import {
10 type SurfacePoint, toCartesian, surfaceDistance, directDistance,
11 chordApexHeight, routeCrossesSlat, landStripIndex,
12} from '../canon/geometry.js'
13
14export type Clarity = 'crisp' | 'hazy' | 'ghost' | 'invisible'
15
16export interface Sightline {
17 surfaceDistanceM: number
18 directDistanceM: number
19 /** How high above the floor the sightline rises. Why looking up beats looking along. */
20 apexHeightM: number
21 opticalDepth: number
22 transmission: number
23 clarity: Clarity
24}
25
26/**
27 * Rayleigh optical depth along the chord between two surface points, integrated
28 * through Tower's rotational density profile.
29 */
30export function opticalDepth(a: SurfacePoint, b: SurfacePoint, samples = 512): number {
31 const p0 = toCartesian(a)
32 const p1 = toCartesian(b)
33 const len = Math.hypot(p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2])
34 if (len === 0) return 0
35 const step = len / samples
36 let tau = 0
37 for (let i = 0; i < samples; i++) {
38 const t = (i + 0.5) / samples
39 const x = p0[0] + (p1[0] - p0[0]) * t
40 const y = p0[1] + (p1[1] - p0[1]) * t
41 tau += extinctionAtRadius(Math.hypot(x, y)) * step
42 }
43 return tau
44}
45
46export function clarityOf(opticalDepthValue: number): Clarity {
47 if (opticalDepthValue < 0.5) return 'crisp'
48 if (opticalDepthValue < 1.5) return 'hazy'
49 if (opticalDepthValue < 3.5) return 'ghost'
50 return 'invisible'
51}
52
53export function sightline(a: SurfacePoint, b: SurfacePoint): Sightline {
54 const tau = opticalDepth(a, b)
55 return {
56 surfaceDistanceM: surfaceDistance(a, b),
57 directDistanceM: directDistance(a, b),
58 apexHeightM: chordApexHeight(a, b),
59 opticalDepth: tau,
60 transmission: Math.exp(-tau),
61 clarity: clarityOf(tau),
62 }
63}
64
65export interface Travel {
66 distanceM: number
67 crossesSlat: boolean
68 /** Journey time in seconds at the given speed. */
69 durationS: number
70}
71
72export function travel(a: SurfacePoint, b: SurfacePoint, speedMS: number): Travel {
73 const distanceM = surfaceDistance(a, b)
74 return { distanceM, crossesSlat: routeCrossesSlat(a, b), durationS: distanceM / speedMS }
75}
76
77/** Which land strip a point sits on, or null for a slat. Useful for place metadata. */
78export { landStripIndex }