tower / interior-flight / files / decisions.md
Branch interior-flight · default branch · all branches
decisions.md
866c7f96 2026-09-07 Pavel Safronov | 1 | # Tower — Decisions |
| 2 | ||
| 3 | Standing record of choices made and why, so they can be revisited deliberately | |
| 4 | rather than rediscovered. Newest sections append at the end. | |
| 5 | ||
| 6 | --- | |
| 7 | ||
| 8 | ## D-001 — Cylinder geometry: r = 50 km, L = 540 km | |
| 9 | ||
| 10 | **Decided.** Radius 50 km, length 540 km, giving 169,646 km² of inner surface. | |
| 11 | ||
| 12 | Considered three shapes, all matching Wisconsin's area at 1 g: | |
| 13 | ||
| 14 | | | radius | length | spin period | character | | |
| 15 | |---|---|---|---|---| | |
| 16 | | Long & narrow | 8 km | 3,375 km | 3.0 min | ceiling, not sky; absurd aspect ratio | | |
| 17 | | **Balanced** | **50 km** | **540 km** | **7.5 min** | far side visible overhead through haze | | |
| 18 | | Wide & short | 150 km | 180 km | 13.0 min | a drum; far cap looms as a wall | | |
| 19 | ||
| 20 | **Why balanced.** It is the only one where the sky reads as *sky*. At 8 km the | |
| 21 | roof is 16 km up and oppressive; at 150 km the far side is 300 km away and lost | |
| 22 | entirely. At 50 km the opposite land is 100 km overhead — far enough to be pale | |
| 23 | and dreamlike, near enough to be unmistakably *land above you*. That image is the | |
| 24 | point of the whole project, and this geometry is what produces it. | |
| 25 | ||
| 26 | The 7.5-minute spin also sits in a good window: fast enough that star motion is | |
| 27 | obvious in a few seconds of watching, slow enough not to look like a fairground | |
| 28 | ride. | |
| 29 | ||
| 30 | --- | |
| 31 | ||
| 32 | ## D-002 — Slats sized so land area equals Wisconsin's land area | |
| 33 | ||
| 34 | **Decided.** 8 slats, 6.80 km wide, leaving 140,268 km² of land. | |
| 35 | ||
| 36 | The brief specified a Wisconsin-sized inner surface. Wisconsin has two areas — | |
| 37 | total 169,635 km² and land 140,268 km². Sizing the slats to consume exactly the | |
| 38 | difference makes both true at once: **Tower's total inner surface is Wisconsin's | |
| 39 | total area, and Tower's habitable land is Wisconsin's land area. The slats are its | |
| 40 | lakes.** | |
| 41 | ||
| 42 | This is a coincidence discovered while computing, not a constraint imposed. It | |
| 43 | costs nothing and it is too good to pass up. | |
| 44 | ||
| 45 | *Superseded by D-017: there are no slats, so Tower's land is its entire inner | |
| 46 | surface, 169,646 km² — Wisconsin's total area. The second coincidence recorded | |
| 47 | below no longer applies.* | |
| 48 | ||
cf37225c 2026-09-07 Pavel Safronov | 49 | **Slat count is left as a tunable constant.** *The runtime control is disabled and |
| 50 | hidden as of D-033; the derivation below is unchanged and still tested.* The width | |
| 51 | follows from the count: | |
866c7f96 2026-09-07 Pavel Safronov | 52 | |
| 53 | | slats | each | land strips between | | |
| 54 | |---|---|---| | |
| 55 | | 6 | 9.07 km | 43.3 km | | |
| 56 | | **8** | **6.80 km** | **32.5 km** | | |
| 57 | | 12 | 4.53 km | 21.7 km | | |
| 58 | | 24 | 2.27 km | 10.8 km | | |
| 59 | ||
| 60 | 8 chosen because 32.5 km strips read as *regions* — a day's travel across, room | |
| 61 | for real geography. At 24 the world becomes narrow ribbons where every point is | |
| 62 | within 5 km of a wall of stars, which is striking but claustrophobic. Revisit | |
| 63 | after seeing the map view; this is cheap to change. | |
| 64 | ||
| 65 | *"Revisit after seeing the map view" is now literal — D-014 makes the count a | |
| 66 | runtime control from 0 to 16, and 8 is the default rather than a fixed value.* | |
| 67 | ||
| 68 | --- | |
| 69 | ||
| 70 | ## D-003 — Widen the beam to 10 km; the physics now works unbroken | |
| 71 | ||
| 72 | **Decided.** The beam is 10 km in circumference, carrying 54.3 TW at 5,000× | |
| 73 | concentration. No physical law is bent. *This supersedes an earlier version of | |
| 74 | D-003 that accepted an étendue violation in order to keep a 1 km beam.* | |
| 75 | ||
| 76 | **The mistake in the first pass.** I treated the 1 km beam as fixed and concluded | |
| 77 | that Earth-like brightness was impossible without an active, physics-breaking | |
| 78 | waveguide. That was wrong, because it conflated two independent parameters: | |
| 79 | ||
| 80 | - **Collector aperture** sets *how much power* is gathered. This was never limited | |
| 81 | by anything but engineering scale. | |
| 82 | - **Beam cross-section** sets *how hard the light must be concentrated* to fit | |
| 83 | through. This is where étendue binds — passive optics cannot exceed the sun's own | |
| 84 | surface radiance, ~46,223× at 1 AU, or 62.9 MW/m². | |
| 85 | ||
| 86 | Holding the beam at 1 km forced 313,000× and broke the second law. Widening the | |
| 87 | beam relaxes the concentration requirement quadratically and costs nothing else. | |
| 88 | ||
| 89 | **Where the number came from.** Targeting 54.3 TW — Earth clear-sky noon at local | |
| 90 | midday, see design §1.3 — against candidate concentrations: | |
| 91 | ||
| 92 | | concentration | beam circumference needed | status | | |
| 93 | |---|---|---| | |
| 94 | | 46,223× (étendue limit) | 3.29 km | thermodynamically impossible to exceed | | |
| 95 | | 10,000× | 7.08 km | beyond current practice | | |
| 96 | | **5,000×** | **10.01 km** | **Odeillo solar furnace reaches ~10 MW/m² today** | | |
| 97 | | 1,000× | 22.39 km | routine, but a conspicuously fat beam | | |
| 98 | ||
| 99 | **5,000× chosen.** It lands the beam on a round 10 km, it is *demonstrated* | |
| 100 | terrestrial technology rather than an extrapolation, and it sits at 11 % of the | |
| 101 | étendue limit — 9.2× of headroom in reserve. Diffuser mirrors at 99.9 % | |
| 102 | reflectivity reach thermal equilibrium at 316 °C in that beam, an ordinary | |
| 103 | radiator problem rather than an exotic one. | |
| 104 | ||
| 105 | **Cost to the imagery: none.** At 1,592 m radius seen from 50 km below, the beam is | |
| 106 | 3.65° wide — about seven sun-widths. It still reads as a luminous rod running the | |
| 107 | length of the sky, which is the image the 1 km figure was protecting. | |
| 108 | ||
| 109 | **The unlocked consequence.** Because Tower now has *surplus* capability rather | |
| 110 | than a deficit, brightness becomes a **setting** rather than a fixed value. The | |
| 111 | diffusers are software-controlled; illumination can run anywhere from 30 W/m² | |
| 112 | permanent dusk to 320 W/m² clear-sky noon. The "embrace the dimness" option | |
| 113 | weighed and rejected in the first pass is no longer an alternative to this | |
| 114 | decision — it is a position on its slider. The app should expose that control. | |
| 115 | ||
| 116 | ## D-004 — Terminator orbit, actively precessed | |
| 117 | ||
| 118 | **Decided.** Orbital plane held normal to the Earth–Sun line, precessing 360°/year. | |
| 119 | ||
| 120 | The brief requires that one cap face the sun permanently. Any orbit whose plane | |
| 121 | contains the sun line puts Tower through Earth's shadow once per revolution, which | |
| 122 | would interrupt the beam. A terminator orbit never enters shadow. | |
| 123 | ||
| 124 | The cost is that the plane must be reoriented ~1°/day against the gyroscopic | |
| 125 | stiffness of a spinning 540 km cylinder. This is enormous and continuous. Canon | |
| 126 | adopts it as a feature: Tower's existence is a permanent act of maintenance, not a | |
| 127 | stable configuration. That is more interesting than a free lunch. | |
| 128 | ||
| 129 | --- | |
| 130 | ||
| 131 | ## D-005 — Vanilla TypeScript, no UI framework | |
| 132 | ||
| 133 | **Decided.** Vite + TypeScript + Three.js, no React/Vue/Svelte. | |
| 134 | ||
| 135 | The app is three canvases, a picker and a control panel, sharing one small state | |
| 136 | object. A framework would add build weight and a rendering model that fights | |
| 137 | Three.js's own loop, in exchange for conveniences this UI does not need. Revisit | |
| 138 | only if the control panel grows genuinely complex. | |
| 139 | ||
| 140 | Canvas 2D rather than Three.js for the map: it is a flat unrolled rectangle with no | |
| 141 | projection distortion, and 2D canvas draws that natively. | |
| 142 | ||
| 143 | --- | |
| 144 | ||
| 145 | ## D-006 — Interior fidelity: real geometry, simple ground | |
| 146 | ||
| 147 | **Decided.** The shell, curvature, haze, beam, diffusers, slats and stars are all | |
| 148 | genuinely modelled and correct. The ground is procedural terrain and texture only — | |
| 149 | no scenery meshes, no settlements, no vegetation. | |
| 150 | ||
| 151 | The illusion of standing inside Tower comes almost entirely from the sky: land | |
| 152 | curving up into blue, the lit shaft overhead, stars wheeling past every 7.5 | |
| 153 | minutes. Detailed ground scenery adds cost proportional to realism and contributes | |
| 154 | little to that illusion. Deferred, not abandoned. | |
| 155 | ||
| 156 | --- | |
| 157 | ||
| 158 | ## D-007 — All canon derived from four constants | |
| 159 | ||
| 160 | **Decided.** `canon/constants.ts` exposes radius, length, slat count and target | |
| 161 | land area as free parameters. Everything else — spin period, rim speed, Coriolis | |
| 162 | parameter, slat width, angular spacing, axis pressure, beam power — is computed. | |
| 163 | ||
| 164 | The alternative, transcribing the figures in `design.md` as literals, would drift | |
| 165 | the moment any parameter is revisited (and D-002 and D-003 both flag likely | |
| 166 | revisits). Derivation makes the whole app answer coherently to a changed radius. | |
| 167 | ||
| 168 | --- | |
| 169 | ||
| 170 | ## D-008 — Diffusers are a 1.2 cm swarm; the beam glows from air, not from them | |
| 171 | ||
| 172 | **Decided.** 78 billion 1.2 cm mirrorlets, and the beam's visible glow is | |
| 173 | atmospheric scattering that does not depend on them. | |
| 174 | ||
| 175 | **The question.** Would a modest number of large diffusers read as a string of | |
| 176 | separate suns rather than a continuous glowing rod? | |
| 177 | ||
| 178 | **Yes.** At 50 km, naked-eye resolution is 14.5 m; anything larger shows a visible | |
| 179 | disc. But shrinking below 14.5 m is not sufficient, which was the non-obvious part. | |
| 180 | A flat mirror preserves radiance, so an aimed mirrorlet shows the *sun's own | |
| 181 | surface brightness* over a small angle — unresolved but blinding, exactly as stars | |
| 182 | are unresolved and plainly visible. Magnitudes: 1 m → −13.6 (brighter than a full | |
| 183 | moon), 10 cm → −8.6, **1.2 cm → −4.0**, which finally sits under the daylight | |
| 184 | visibility threshold. The binding constraint is flux, not angular size, and it is | |
| 185 | two orders of magnitude tighter than the resolution limit implies. | |
| 186 | ||
| 187 | **The better answer, found while checking.** The beam glows regardless. Rayleigh | |
| 188 | scattering of a 6.8 MW/m² beam through 3.18 km of 5.2 kPa axis air gives 779 | |
| 189 | W/m²/sr — 26× blue sky, 25,700× below the solar disc. The continuous-glow imagery | |
| 190 | is therefore a robust property of beam plus atmosphere, not a consequence of | |
| 191 | getting the diffuser design right. Canon leans on this; the swarm sizing is then | |
| 192 | only about keeping individual glints out of the sky. | |
| 193 | ||
| 194 | **Consequences taken.** 11.3 km² of mirror at 1.2 cm is 78 billion units massing | |
| 195 | 158 tonnes, spaced 3.8 m apart. Optical depth 1.41 leaves 24 % of the light | |
| 196 | reaching the far cap, which satisfies the brief's "shines to the far end" without | |
| 197 | stipulating it. *The passive drift-and-recirculate mechanism first recorded here is | |
| 198 | superseded by D-009 — it does not work.* | |
| 199 | ||
| 200 | **Story consequence.** 78 billion independently aimable mirrors make illumination a | |
| 201 | render target rather than a schedule. Sunsets are compositing, not optics. | |
| 202 | ||
| 203 | --- | |
| 204 | ||
| 205 | ## D-009 — Tower flies every foil itself, because nothing else works | |
| 206 | ||
| 207 | **Decided.** All 78 billion foils are under continuous individual control by Tower. | |
| 208 | There is no recirculation machinery at either cap. *This supersedes the passive | |
| 209 | drift mechanism in D-008.* | |
| 210 | ||
| 211 | **Origin.** Proposed by the author as characterisation — Tower is an intelligent | |
| 212 | being, so it controls each foil directly and moves the swarm partly for its own | |
| 213 | enjoyment. Checking the physics turned this from a flourish into a requirement. | |
| 214 | ||
| 215 | **A passive swarm is impossible.** D-008 assumed the axis was effectively | |
| 216 | weightless. It is not. At the beam's edge, 1,592 m out, g = ω²s = **0.312 m/s²**. A | |
| 217 | 2 mg foil sinks outward at 0.26 m/s and **falls clear of the beam in 1.7 hours**. A | |
| 218 | drifting swarm dissolves in an afternoon. Continuous active flight is the only | |
| 219 | configuration that holds together at all. | |
| 220 | ||
| 221 | **They can easily afford it.** Each foil intercepts 979 W; harvesting 0.1 % yields | |
| 222 | ~1 W on a 2 mg body — 489,600 W/kg, ~50× a jet engine. Flying up-beam at 1.5 m/s | |
| 223 | costs 15 µW against 14,400 µW harvestable from ambient world-light: **a 1000× | |
| 224 | margin, without touching the beam.** So the foils fly home unaided, no structure is | |
| 225 | required at the far cap, and the exterior render is simpler than D-008 implied. | |
| 226 | ||
| 227 | **The interesting part is the slack.** With 1000× of margin, efficiency is not a | |
| 228 | constraint on Tower's behaviour. It can move the swarm in spirals, shoals and | |
| 229 | patterns that cost energy for no reason except preference, and nothing about the | |
| 230 | world degrades. Play is not a luxury Tower affords despite the physics — it is | |
| 231 | budget the physics hands over for free. The weather is therefore not a system | |
| 232 | Tower operates but something closer to body language. | |
| 233 | ||
| 234 | **Falling out of this: aiming is human-scale.** A 1.2 cm mirror is | |
| 235 | diffraction-limited to 4.6×10⁻⁵ rad, giving a **2.3 m spot** at 50 km. Tower can | |
| 236 | place a glint on one person that a bystander five metres away cannot see. Offered | |
| 237 | to the author as a story mechanic; canon records the optics either way. | |
| 238 | ||
| 239 | **And: mirrorfall.** A failed foil sinks out of the beam, takes ~2 days to descend, | |
| 240 | and lands at 0.48 m/s — snowflake speed. A permanent slow glitter falls out of the | |
| 241 | sky. | |
| 242 | ||
| 243 | --- | |
| 244 | ||
| 245 | ## D-010 — The app is a companion to a novel, not a tech demo | |
| 246 | ||
| 247 | **Decided.** Tower is the setting of a novel the author is writing. The app serves | |
| 248 | that purpose first. | |
| 249 | ||
| 250 | Two consequences, recorded because they change priorities rather than mechanics: | |
| 251 | ||
| 252 | - **Coherence outranks spectacle.** These documents are series-bible material and | |
| 253 | the prose is downstream of them. A number that looks better but contradicts | |
| 254 | something is worse than a duller number that holds. This is already how the | |
| 255 | physics has been handled; it is now the stated rule. | |
| 256 | - **Shareable permalinks to a point move into scope** (phase 4, previously | |
| 257 | deferred). "Stand here and look at this" is the entire value of a companion | |
| 258 | piece, and a link is how a reader receives it. Named locations from the novel | |
| 259 | should be markable on the map for the same reason. | |
| 260 | ||
| 261 | Not yet decided, and deliberately left open: whether the app should ever surface | |
| 262 | narrative content — place names, events, characters — or remain a pure physical | |
| 263 | model that the prose refers to from outside. That is the author's call and does not | |
| 264 | need answering to proceed. | |
| 265 | ||
| 266 | --- | |
| 267 | ||
| 268 | ## D-011 — Writer mode: the app is an instrument, and the world file is git | |
| 269 | ||
| 270 | **Decided.** Two modes over one committed `world.json`. Readers get a finished map; | |
| 271 | the author gets editing plus a set of measuring instruments. No backend. | |
| 272 | ||
| 273 | **Origin.** The author intends to use the app to *write* — placing things and asking | |
| 274 | questions like "can the far face be seen from the midpoint?", "where does a central | |
| 275 | embassy go?", "can a lake overhang?". That makes measurement, not rendering, the | |
| 276 | core feature. D-010 said the app serves the novel; this is what that costs. | |
| 277 | ||
| 278 | **Why those questions justify the build.** Two of the three had non-obvious answers | |
| 279 | that only fell out of computation, and both became canon (§1.8): the far cap is | |
| 280 | invisible from the midpoint at 4 % transmission, and — unexpectedly — the far side | |
| 281 | of the world overhead is *clearer* than the ground at the same distance (78 % vs | |
| 282 | 31 % at 100 km), because a sightline across the interior rises through thin axis | |
| 283 | air. An author cannot guess these. The instrument earns its place immediately. | |
| 284 | ||
| 285 | **Static, file-based, no backend.** GitLab Pages serves static files, so: edit in | |
| 286 | browser → export → commit → publish. The world lives in git beside the prose, | |
| 287 | diffable and versioned, with no service to run or pay for. Drafts sit in | |
| 288 | `localStorage` between exports. | |
| 289 | ||
| 290 | **Coordinates in the file are canon (θ, z), never map pixels,** so the world | |
| 291 | survives any change to how the map is drawn. Consistent with D-007. | |
| 292 | ||
| 293 | **Writer mode is a UI mode, not a security boundary.** `?writer=1` is guessable and | |
| 294 | that is fine — an editor with no persistence and no backend can only alter the | |
| 295 | visitor's own copy. Recorded explicitly so it is never mistaken for access control | |
| 296 | and never load-bearing for privacy. | |
| 297 | ||
| 298 | **The real privacy risk is the repo, not the flag.** Working notes must not ship to | |
| 299 | readers, but anything committed to a public repository is public whatever a | |
| 300 | `"public": false` field says. Hence two export files: `world.json` (public entries, | |
| 301 | committed) and `world.private.json` (everything, gitignored). Whether private notes | |
| 302 | should be version-controlled anyway — private repo, or a second one — is left to | |
| 303 | the author and does not block anything. | |
| 304 | ||
| 305 | **Sequenced early.** The sightline probe lands in phase 6 but should be built first | |
| 306 | within it: it is useful the day it exists, and §1.8 is evidence the author is | |
| 307 | already asking questions only it can answer. | |
| 308 | ||
| 309 | --- | |
| 310 | ||
| 311 | ## D-012 — Ids follow the name until pinned by hand | |
| 312 | ||
| 313 | **Decided.** A place's id is re-derived from its name while it still matches it, | |
| 314 | and freezes permanently once the author edits the id field. | |
| 315 | ||
| 316 | **The tension.** design.md §3.2 makes ids permalink targets, which argues for | |
| 317 | never changing them. But places are created from a placeholder name ("New city"), | |
| 318 | so a never-changing id produces a world file of `new-city`, `new-city-2`, | |
| 319 | `new-city-7` — unreadable in git diffs and unusable for hand-editing, both of which | |
| 320 | D-011 counts as reasons for the file-based design in the first place. | |
| 321 | ||
| 322 | **Resolution.** During world-building, readability wins; after publication, | |
| 323 | stability wins; and the author is the only one who knows which phase a place is in. | |
| 324 | So the id tracks the name automatically until they say otherwise, and the id field | |
| 325 | is shown in the editor so saying otherwise is one edit. `isAutoId()` in | |
| 326 | `src/world/schema.ts` is the test, and is unit-tested directly. | |
| 327 | ||
| 328 | **Found by running it.** The first implementation derived the id once at creation | |
| 329 | and never updated it, which the browser check caught immediately — every exported | |
| 330 | place was called `new-city`. Two further bugs surfaced the same way: renaming | |
| 331 | blanked the editor (the store re-rendered before the selection moved to the new id), | |
| 332 | and clicking an existing marker while a creation tool was active dragged the old | |
| 333 | place instead of creating a new one. None were visible in the unit tests. | |
| 334 | ||
| 335 | --- | |
| 336 | ||
| 337 | ## D-013 — The map is bounded, and cut at a slat centre | |
| 338 | ||
| 339 | **Decided.** The map shows the inner surface once, as a bordered rectangle, rather | |
| 340 | than tiling vertically. The cut falls at the centre of the last slat. | |
| 341 | ||
| 342 | **Why not tile.** The circumference wraps, so tiling is the honest rendering and it | |
| 343 | makes places near the seam continuous. But it also makes the world look boundless: | |
| 344 | with no edge, the author cannot see the extent of what they are building, and | |
| 345 | repeated strip labels read as more strips rather than as repeats. For a map — a | |
| 346 | thing whose job is to show where things are relative to the whole — legible extent | |
| 347 | beats a hidden seam. | |
| 348 | ||
| 349 | **Why cut there.** Any bounded map needs an edge, and the edge destroys whatever it | |
| 350 | falls on. Land strip 0 is centred on θ = 0, so the obvious cut would slice a whole | |
| 351 | strip in half across the top and bottom margins. Cutting at a slat centre instead | |
| 352 | leaves every land strip whole and splits only a band of stars, which carries no | |
| 353 | geography. This is exactly why Earth's maps are cut at the antimeridian. With zero | |
| 354 | slats there is nothing to protect, so the cut returns to θ = 0. | |
| 355 | ||
| 356 | **Consequences taken.** Panning is clamped so the surface cannot be lost off-screen | |
| 357 | — without tiling there is nothing to pan back into. A sightline whose short route | |
| 358 | crosses the edge is drawn as two segments, entering and leaving opposite edges, so | |
| 359 | the drawn path always matches the measured distance rather than contradicting it. | |
| 360 | ||
| 361 | --- | |
| 362 | ||
| 363 | ## D-014 — Slat count is a runtime control, 0 to 16 | |
| 364 | ||
| 365 | **Decided.** `SLAT_COUNT` stops being a compile-time constant. The count lives in | |
| 366 | `src/canon/slats.ts`, adjustable from 0 to 16, persisted per browser, with every | |
| 367 | slat-derived figure computed from it. | |
| 368 | ||
| 369 | **Why.** D-002 already called this "the most tunable number in canon" and said to | |
| 370 | revisit it after seeing the map. Making it a slider makes that revisiting real | |
| 371 | rather than a code edit, and the author is choosing the shape of a world rather | |
| 372 | than configuring software. | |
| 373 | ||
| 374 | **The invariant that makes it safe.** D-002 fixes the *total* slat width from the | |
| 375 | land-area target, and that total does not depend on how many slats it is divided | |
| 376 | into. So **land area stays exactly Wisconsin's 140,268 km² at every count from 1 to | |
| 377 | 16** — the coincidence survives the whole range, and only the per-slat width moves | |
| 378 | (18.1 km at 3 slats, 3.40 km at 16). | |
| 379 | ||
| 380 | **Zero is a genuinely different world, and the app says so.** With no slats the | |
| 381 | land becomes the entire inner surface, 169,646 km², and nobody in Tower can ever | |
| 382 | see a star. That breaks the D-002 coincidence, so the control states it plainly | |
| 383 | instead of silently reporting a changed number. Rounding that away would have been | |
| 384 | the easy option and the wrong one — it is exactly the kind of quiet inconsistency | |
| 385 | the documentation gate exists to catch. | |
| 386 | ||
| 387 | **Cost.** Slat geometry moved from constants to functions, which rippled through | |
| 388 | `geometry.ts`, the map, the instruments and the tests. Worth it: D-007's principle | |
| 389 | was always that canon is derived rather than transcribed, and this is that | |
| 390 | principle extended one parameter further. | |
| 391 | ||
| 392 | --- | |
| 393 | ||
| 394 | ## D-015 — Hydrology: shaped terrain, no sideways Coriolis, climate from the slats | |
| 395 | ||
| 396 | **Decided.** Recorded as canon in design.md §1.9, and it amends §1.4. | |
| 397 | ||
| 398 | **The provable part.** The floor's tangent plane is spanned by θ̂ and ẑ and the spin | |
| 399 | axis is ẑ, so surface flow gets `ẑ × ẑ = 0` axially and a purely radial `ẑ × θ̂ = −ŝ` | |
| 400 | circumferentially. The Coriolis force has **no component inside the surface** at any | |
| 401 | heading. Tower's rivers do not curve, and Earth's entire rotational repertoire — | |
| 402 | hemispheres, Baer's law, gyres, cyclones — is absent. This is not a simplification; | |
| 403 | it falls out of the geometry. | |
| 404 | ||
| 405 | **Where the force goes instead.** Into vertical motion. Waterfalls lean retrograde | |
| 406 | as h^1.5 (133 m over a 1 km drop, 377 m over 2 km), and rain hangs at a permanent | |
| 407 | 1.47° slant that never varies. Rising things lean the opposite way. A world where | |
| 408 | every falling thing drifts one way and every rising thing the other is a strong, | |
| 409 | cheap, always-visible signature. | |
| 410 | ||
| 411 | **The absence that matters more.** Potential depends only on radius, so the floor is | |
| 412 | one equipotential and a perfect Tower has *no rivers at all*. Every river exists | |
| 413 | because the floor was shaped, there is no tectonic uplift, and erosion is therefore | |
| 414 | one-way — ~30 Myr to erase a 3 km range. Tower's hydrology is maintenance, which is | |
| 415 | D-004's theme arriving from a new direction. | |
| 416 | ||
| 417 | **An inconsistency found and fixed, not smoothed over.** §1.4 specified a | |
| 418 | glass/**diamond** composite slat fill. Diamond conducts ~2,000 W/m·K; such a slat | |
| 419 | would radiate 385 W/m² to space, exceeding Tower's entire 320 W/m² mean insolation | |
| 420 | and freezing the world. Canon now specifies low-conductivity glass, which loses | |
| 421 | ~28 W/m² through 5 m. Raised explicitly per the documentation gate rather than | |
| 422 | quietly retconned. | |
| 423 | ||
| 424 | **The gift in that loss.** Tower has no latitude — every point is equidistant from | |
| 425 | the light, so there is no natural climate gradient whatsoever, which is a real | |
| 426 | worldbuilding hole. The leaking slats fill it: eight cool belts running the full | |
| 427 | 540 km, parallel to the axis. **Tower's climate bands run lengthwise, not around**, | |
| 428 | and the slat count sets the climate as well as the geography — further support for | |
| 429 | D-014 making it a control. | |
| 430 | ||
| 431 | --- | |
| 432 | ||
| 433 | ## D-016 — Slat orientation: axial or rings — OPEN | |
| 434 | ||
| 435 | **Not decided.** Canon keeps axial strips for now. Rings are a live alternative and | |
| 436 | this records what the choice costs, so it can be made deliberately. | |
| 437 | ||
| 438 | **Why it matters more than it looks.** Slat layout turns out to set three things at | |
| 439 | once — the shape of Tower's countries, its entire climate map, and what its people | |
| 440 | can see of the universe. It is not a detail of the hull. | |
| 441 | ||
| 442 | **Geometry.** At equal total area: rings are 11.69 km wide with 55.8 km land bands. | |
| 443 | Each land region is then 314 × 55.8 km rather than 32.5 × 540 km — the same area at | |
| 444 | aspect 5.6:1 instead of 16.6:1, a country instead of a corridor. Ring bands are also | |
| 445 | **closed loops**: you can walk 314 km around your whole region and come home without | |
| 446 | crossing a window. Axial strips are ribbons with two dead ends at the caps. | |
| 447 | ||
| 448 | **Climate.** Axial strips put the cool belts lengthwise, so a 540 km river runs its | |
| 449 | whole length through identical country. Rings put them crosswise, giving Tower a | |
| 450 | genuine latitude analogue — a long river crosses all eight bands like the Nile. | |
| 451 | ||
| 452 | **The sky, which is the deep one.** The spin axis is the celestial pole, so stars | |
| 453 | move along the celestial equator. Orientation decides whether a window lies across | |
| 454 | that motion or along it: | |
| 455 | ||
| 456 | - *Axial*: 3.90° across the motion — each star crosses in **4.9 s**, but the strip | |
| 457 | spans pole to pole so essentially the whole sky passes through, strobing. | |
| 458 | - *Rings*: 6.70° along the motion — a star rides the band horizon to horizon over | |
| 459 | **3.7 minutes**, but only the 5.8 % of sky within 3.35° of the celestial equator | |
| 460 | is *ever* visible. The rest is never seen at all. | |
| 461 | ||
| 462 | Earth follows the same rule: a five-second flicker under axial slats, a slow bright | |
| 463 | traveller under rings. The sun is at the celestial pole and invisible through both. | |
| 464 | ||
| 465 | **The cost of rings.** Hoop stress dominates the structure — ρv² is 785 MPa in a | |
| 466 | composite shell plus ~500 MPa of air pressure — and it runs along θ. Axial slats sit | |
| 467 | across that path, so ribs bridge the gap and the glass carries only local pressure. | |
| 468 | A ring runs along the path and is continuous in θ, so **the window itself must carry | |
| 469 | ~1.2 GPa**. Glass cannot; it would need hoop cables through every pane, putting bars | |
| 470 | across every view forever. Characterful, but a real price axial slats do not pay. | |
| 471 | ||
| 472 | **Consequence for the app if rings are chosen.** D-013 cuts the map at a slat centre | |
| 473 | because there is an empty θ to cut at. With rings there is not — every θ is land — | |
| 474 | so the map seam would have to slice through inhabited country, or the map would need | |
| 475 | reorienting. Worth knowing before the choice is made rather than after. | |
| 476 | ||
| 477 | --- | |
| 478 | ||
| 479 | ## D-017 — The hull is sealed; the only views out are at the caps | |
| 480 | ||
| 481 | **Decided by the author.** No slats, in any orientation. The cylinder wall is | |
| 482 | opaque and unbroken for its full 540 km. Supersedes D-002's slat sizing, D-014's | |
| 483 | default, and closes D-016. | |
| 484 | ||
| 485 | **What it costs.** The wheeling star field was canon's answer to "what is most alien | |
| 486 | about standing inside Tower" (§1.2) and the interior view's headline feature. It is | |
| 487 | gone. Tower also loses its only passive climate driver and its only natural source | |
| 488 | of geographic variation. | |
| 489 | ||
da6bcb0e 2026-09-08 Pavel Safronov | 490 | *Partly recovered by D-045, and worth reading as a correction to the paragraph |
| 491 | above rather than a reversal of the decision. The wall is still opaque and unbroken | |
| 492 | for its full 540 km — that is what D-017 decided and it stands. What came back is | |
| 493 | the pinwheel, through the far cap window, where §1.4 had always said it would be: | |
| 494 | one sky, at one end of the world, at night. It is no longer everywhere, which is | |
| 495 | the whole difference between the slats and a window.* | |
| 496 | ||
866c7f96 2026-09-07 Pavel Safronov | 497 | **What it buys, which is more.** |
| 498 | ||
| 499 | - **One unbroken surface.** 169,646 km² of continuous land — Wisconsin's *total* | |
| 500 | area, the coincidence in §1.1 now standing alone and cleaner. Nothing divides the | |
| 501 | world into strips; you can walk anywhere, and a river can circle the whole 314 km | |
| 502 | circumference and close on itself. | |
| 503 | - **The spin inverts.** With no windows and a co-rotating far side, *nothing in the | |
| 504 | sky moves at all*. But rain still hangs at a permanent 1.47° slant and a 1 km | |
| 505 | waterfall still lands 133 m off its cliff. **The sky is still and the water is | |
| 506 | not** — a stranger signature than wheeling stars, and one that shows up in every | |
| 507 | puddle rather than only when you look up. | |
| 508 | - **The caps become the only view, and it is a good one.** A cap window looks along | |
| 509 | the spin axis — the celestial pole — so stars do not sweep but *rotate about a | |
| 510 | fixed centre*, one turn per 7.5 minutes, one motionless star at the middle. The | |
| 511 | caps are also walls rather than floors: gravity there is tangent to their surface, | |
| 512 | so each is a 50 km cliff at the end of the world with its windows at the base. | |
| 513 | - **Nobody can ever see Earth.** The terminator orbit (D-004) puts Earth ~90° off | |
| 514 | the axis, out the side, which is now solid hull. A habitat in Earth orbit whose | |
| 515 | people have never seen the planet they orbit. | |
| 516 | ||
| 517 | **The consequence the author accepted explicitly.** With no latitude and no windows, | |
| 518 | nothing breaks the thermal uniformity, so there is no passive weather whatsoever. | |
| 519 | Every cloud, every rainfall and every jet stream is Tower deliberately aiming light. | |
| 520 | This is D-009 reaching its end point: the swarm was already Tower's body language, | |
| 521 | and is now its lungs. There is no autonomic layer beneath. **If Tower's attention | |
| 522 | lapses, the rain stops.** | |
| 523 | ||
| 524 | --- | |
| 525 | ||
| 526 | ## D-018 — Rivers are a closed, pumped circulation, and Tower can easily afford it | |
| 527 | ||
| 528 | **Decided.** Tower's rivers follow the architecture the author proposed: reservoir → | |
| 529 | shaped descent → lake → lift back to the reservoir. Source and sink are one system. | |
| 530 | ||
| 531 | **Why it is the only option.** The floor is an equipotential (D-015) so there is no | |
| 532 | natural slope, and the sealed hull (D-017) leaves no natural water cycle. Water in | |
| 533 | Tower moves only because something moves it. | |
| 534 | ||
| 535 | **Why it is not a compromise.** Costed against the 54.3 TW budget: | |
| 536 | ||
| 537 | | | | | | |
| 538 | |---|---|---| | |
| 539 | | Earth-like rainfall, 1,000 mm/yr | 12.2 TW | **22.4 %** — Earth spends the same share | | |
| 540 | | Recirculating all runoff (1,792 m³/s, a Rhine) against 500 m | 8.8 GW | **0.016 %** | | |
| 541 | ||
| 542 | Pumping the entire hydrology of the world is a rounding error — 6,000× headroom. | |
| 543 | And full Earth-intensity weather is affordable at the same fraction of budget Earth | |
| 544 | itself spends. Tower does not have to choose. | |
| 545 | ||
| 546 | **Canon uses both, because they give different rivers.** A pumped river *begins at a | |
| 547 | machine* — walk to its source and find a wall of water leaving a structure. A | |
| 548 | rain-fed river begins at a cloud Tower made over the highlands, and looks entirely | |
| 549 | natural while being entirely deliberate. Made weather runs the world's general | |
| 550 | hydrology; pumps run the watercourses Tower wants under precise control. | |
| 551 | ||
| 552 | **Two consequences worth keeping.** A closed loop has no net descent, so a river | |
| 553 | circling the world must be pumped — which is the same architecture, closed the whole | |
| 554 | way round. And since nothing evaporates to space, **Tower's water inventory is | |
| 555 | conserved and exactly known, forever.** | |
| 556 | ||
| 557 | --- | |
| 558 | ||
| 559 | ## D-019 — Geography is authored as data; terrain is generated from it | |
| 560 | ||
| 561 | **Decided.** `content/geography.json` holds the authored geography. `npm run terrain` | |
| 562 | (tools/terrain.mjs) reads it and emits a height map, a colour map, a corridor detail | |
| 563 | render, and a seed `world.json` of places. | |
| 564 | ||
| 565 | **Why data first.** The author asked to work the geography out before returning to | |
| 566 | app code. A JSON source plus a generator means the map is re-derivable from one | |
| 567 | readable file: move the river by editing eighteen waypoints, not by repainting. | |
| 568 | Coordinates are canon (θ, z) throughout, consistent with D-007 and D-011. | |
| 569 | ||
| 570 | **No dependencies.** Node's `zlib` is enough to write PNGs directly (tools/png.mjs, | |
| 571 | ~60 lines for 16-bit greyscale and 8-bit RGB), so the tool adds nothing to | |
| 572 | `package.json`. Terrain noise is a small value-noise/fBm module. | |
| 573 | ||
| 574 | **Three bugs the renders caught that no test would have.** | |
| 575 | ||
| 576 | - *A float hash.* The noise hash multiplied large integers in floating point, | |
| 577 | silently losing precision and streaking the whole map with axis-aligned lines. | |
| 578 | Fixed with `Math.imul` throughout. | |
| 579 | - *A fractional wrap period.* The circumference is not a round multiple of any | |
| 580 | useful feature size, so the noise lattice did not close on itself and tore at the | |
| 581 | seam. Sample coordinates on the θ axis are now snapped to a whole number of | |
| 582 | cells; the resulting few-percent anisotropy is invisible. | |
| 583 | - *A discontinuous river.* Valley geometry switched between reaches abruptly, | |
| 584 | stepping the valley from 5.4 km wide to 18.2 km in one pixel column and drawing a | |
| 585 | visible wall across the map. Reach parameters now blend along the river. A | |
| 586 | `--probe` flag prints elevation jumps along a row, which is how it was found. | |
| 587 | ||
| 588 | **Known gap, deliberately not fixed here.** The seed `world.json` contains | |
| 589 | `polyline` geometry (the river, the canyon) and kinds the app does not yet know. | |
| 590 | `parseWorld` accepts only points and polygons, so those two places would be dropped | |
| 591 | silently on load. App work is paused by request; this is the first thing to fix when | |
| 592 | it resumes. *Closed by D-022.* | |
| 593 | ||
| 594 | --- | |
| 595 | ||
| 596 | ## D-020 — The world is belts, and the desert rings it | |
| 597 | ||
| 598 | **Decided by the author.** The desert and its three ridges ring the world; the | |
| 599 | capital's water becomes a sea running along the axis; both end caps get ring oceans; | |
| 600 | the range feeds four parallel rivers. | |
| 601 | ||
| 602 | **The ring desert is a barrier no Earth desert can be.** 22,000 km², 13% of the | |
| 603 | surface — modest against Earth's ~33% — but on a cylinder there is no way *around* a | |
| 604 | band. Combined with three ringing ridges it is three concentric walls across Tower's | |
| 605 | waist. | |
| 606 | ||
| 607 | **The drainage was the hard part, and Earth answered it.** Many rivers each cutting | |
| 608 | their own canyon would destroy the chokepoint. But on Earth most drainages do not | |
| 609 | cut through a rising ridge — they are defeated and diverted, and only an antecedent | |
| 610 | river keeps its course. So three of the four rivers are turned by First Ridge, run | |
| 611 | along its front gathering each other, and leave with the fourth through the one gap. | |
| 612 | **The Narrows carries the entire world's runoff, ~392 m³/s.** The chokepoint is | |
| 613 | strengthened by the extra rivers rather than diluted, and the hydrology is more | |
| 614 | realistic than a single river would have been. | |
| 615 | ||
| 616 | **The Deep's orientation is the decision, not its size.** Long down the axis and | |
| 617 | narrow around, so it is crossed the *short* way — around θ, where the floor curves | |
| 618 | up and the far shore stands 2.6 km above eye level. Lengthwise it would be dead | |
| 619 | flat. It also gives Tower back a thermal flywheel: 19× the diurnal heat storage of | |
| 620 | bare ground, 5.9 days per kelvin at full power. D-017's "if Tower blinks the rain | |
| 621 | stops" becomes "the world has about a week of momentum" — a lapse now has time to | |
| 622 | become visible before it becomes fatal. | |
| 623 | ||
| 624 | **The cap seas were not chosen.** Gravity on an end cap is tangent to its surface, | |
| 625 | so water cannot rest there and runs to the rim. Both ring oceans are consequences, | |
| 626 | not designs. | |
| 627 | ||
| 628 | **Skiing came out of §1.9.2 unprompted.** A descending skier has radial velocity and | |
| 629 | therefore real Coriolis: a steady antispinward push on axial fall lines, and a | |
| 630 | weight change of up to 11% on circumferential ones. A retrograde run gets lighter as | |
| 631 | it accelerates; a prograde run grips harder. Every slope in Tower has a handedness, | |
| 632 | which is a sport that cannot exist on Earth. | |
| 633 | ||
| 634 | **Tool work this required:** `ring` footprints, a `flood` op for troughs and bands, | |
| 635 | and a `tarn` op that fills from the bowl floor rather than the rim — the first | |
| 636 | version took its level from the rim mean, so water spilled downhill and every | |
| 637 | mountain lake rendered as a half-moon. | |
| 638 | ||
| 639 | --- | |
| 640 | ||
| 641 | ## D-021 — The terrain tool reports progress, and got 13× faster doing it | |
| 642 | ||
| 643 | **Prompted by the author:** `npm run terrain` ran for a long time saying nothing. | |
| 644 | Adding progress output meant first working out where the time went, which turned a | |
| 645 | logging task into a performance one. | |
| 646 | ||
| 647 | **86 s → 6.4 s.** Three causes, all found by measuring rather than guessing: | |
| 648 | ||
| 649 | - **No spatial index on the river carve.** `opCarve` tested all 592 smoothed river | |
| 650 | segments against all 2.4 million pixels — ~2.9 billion distance tests across both | |
| 651 | renders. Segments are now binned into a 4 km grid and each pixel tests only its | |
| 652 | own cell. *86 s → 17.8 s.* | |
| 653 | - **Deflate level 9 on the colour maps.** Hillshade noise compresses poorly, so | |
| 654 | level 9 spent 4.2 s on one map for a few percent of size. RGB now encodes at | |
| 655 | level 6: 8× faster for 10% larger files, which is the right trade for a tool run | |
| 656 | on every edit. The 16-bit height map stays at level 9 — smooth terrain under the | |
| 657 | Up filter is nearly all zero deltas, and it costs 0.1 s. *17.8 s → 8 s.* | |
| 658 | - **Domain warps computed before range checks.** Several ops evaluated two noise | |
| 659 | functions per pixel *then* discovered the pixel was nowhere near the footprint. | |
| 660 | A conservative unwarped-z rejection now runs first. *8 s → 6.4 s.* | |
| 661 | ||
| 662 | **The log is a table, not a stream.** One row per relief op with its timing, then | |
| 663 | relief/colourise totals per view, then each PNG with its size, then the seed. The | |
| 664 | point is to see which feature is expensive when the file grows, not just that | |
| 665 | something is happening. `--quiet` collapses per-feature rows. | |
| 666 | ||
| 667 | **Worth recording as a habit:** the request was for logging, and the logging is what | |
| 668 | exposed that the colour PNG cost more than the entire mountain range. Instrumenting | |
| 669 | first and optimising second found three real problems in one pass. | |
| 670 | ||
| 671 | --- | |
| 672 | ||
| 673 | ## D-022 — The app catches up with the decisions | |
| 674 | ||
| 675 | **Implemented.** Five outstanding decisions, plus two bugs found while doing it. | |
| 676 | ||
| 677 | **The schema now matches the world** (closes D-019's gap). `polyline` geometry | |
| 678 | joins point and polygon — a polygon needs three vertices for an interior, a | |
| 679 | polyline only needs two to go somewhere — and the kind list grows from six to | |
| 680 | thirteen (`mountain`, `river`, `canyon`, `forest`, `desert`, `tower`, `town`), with | |
| 681 | `AREA_KINDS` / `LINE_KINDS` deciding whether a traced shape closes. Before this the | |
| 682 | seed silently dropped The Long Water and The Narrows. A test now loads | |
62458672 2026-09-07 Pavel Safronov | 683 | `static/world.json` and asserts nothing is lost, so the two files cannot drift apart |
866c7f96 2026-09-07 Pavel Safronov | 684 | again unnoticed. |
| 685 | ||
| 686 | **The rendered terrain is drawn under the map**, which is what the whole tool was | |
| 687 | for. Area regions become outlines rather than fills when it is showing: the raster | |
| 688 | already says where the desert is, and a translucent rectangle over it only fights. | |
| 689 | ||
| 690 | **A desync that would have been baffling.** The app cut the map at θ = 0 with no | |
| 691 | slats; `content/geography.json` renders against θ = π. The raster would have landed | |
| 692 | **half a world out of register** with its own place markers. Both now use | |
| 693 | `MAP_CUT_WITHOUT_SLATS = π` — opposite the capital, which is D-013's antimeridian | |
| 694 | principle applied to habitation rather than geology — and a test asserts the two | |
| 695 | files agree. | |
| 696 | ||
| 697 | **Reader and writer modes** (D-011). `?writer=1` reveals the tools, editor, canon | |
| 698 | controls and import/export; readers get the map, the place list, a read-only detail | |
| 699 | card and the sightline probe. **A bug worth recording:** `renderEditor` un-hid the | |
| 700 | editor whenever a place was selected, overriding the mode — so readers were shown | |
| 701 | the edit fields *including private notes*. Mode is now decided inside | |
| 702 | `renderEditor`, not applied around it. Hiding a panel elsewhere is not the same as | |
| 703 | never rendering it. | |
| 704 | ||
| 705 | **Permalinks** (D-010). `#place=<id>` selects and centres a place; `#at=θ,z` restores | |
| 706 | a sightline endpoint. This is why D-012 made ids follow names only until pinned — | |
| 707 | a permalink is worthless if the id churns. | |
| 708 | ||
| 709 | **Labels are placed by priority and skipped on collision.** Settlements outrank | |
| 710 | regions; overlapping labels are dropped rather than drawn on top of each other. With | |
| 711 | 22 seeded places the mountain stations were an unreadable smear. | |
5f38efd0 2026-09-07 Pavel Safronov | 712 | |
| 713 | --- | |
| 714 | ||
| 715 | ## D-023 — The exterior view, and what it is honest about | |
| 716 | ||
| 717 | **Implemented.** Phase 3: Tower from outside, with the hull, both caps, the | |
| 718 | collector, the converging beam, the spin, and picking on the hull. | |
| 719 | ||
| 720 | **Canon numbers are the code's numbers.** Scene units are kilometres and the axis | |
| 721 | lies along Z with the sun cap at z = 0, so a canon point is literally | |
| 722 | `(R cos θ, R sin θ, z)`. No conversion layer to get wrong. | |
| 723 | ||
| 724 | **Picking has to undo the spin.** The hull turns, so a ray hit is in world space | |
| 725 | while the answer wanted is canon θ, which is fixed to the hull. Resolving the hit | |
| 726 | through `worldToLocal` handles it. Verified by clicking the same screen pixel at two | |
| 727 | spin phases 34° apart and confirming the reported θ moved by 0.588 rad against a | |
| 728 | 0.593 rad phase change, with z identical — the pixel really is landing on different | |
| 729 | ground as Tower turns beneath it. | |
| 730 | ||
11a13dbf 2026-09-07 Pavel Safronov | 731 | **Three departures from physics, all stated in the code.** Sunlight runs parallel to |
5f38efd0 2026-09-07 Pavel Safronov | 732 | the axis, so the flank is edge-on and correctly renders almost black; fill lights |
| 733 | make it readable. And in vacuum the beam between collector and cap has nothing to | |
| 734 | scatter off, so it would be invisible; it is drawn faintly because it is the point | |
| 735 | of the view. Same licence as the map's hillshade, and worth naming rather than | |
| 736 | letting a reader mistake either for a physical claim. | |
| 737 | ||
| 738 | **Three.js loads on first use of the view.** It is by far the largest dependency and | |
| 739 | starts an animation loop; a reader who only opens the map should not pay for it. | |
| 740 | ||
| 741 | **Earth is not drawn.** It is ~90° off the axis and hundreds of thousands of km | |
| 742 | away; at true scale it is invisible, and at any visible scale it is a lie about | |
| 743 | distance. Given how much of this world came out of taking distances seriously, a | |
| 744 | compressed Earth would be the wrong kind of shortcut. The sunward glow is honest — | |
| 745 | the sun really is that way — and D-017's point stands: from inside, Earth is never | |
| 746 | visible at all. | |
11a13dbf 2026-09-07 Pavel Safronov | 747 | |
| 748 | --- | |
| 749 | ||
| 750 | ## D-024 — The terrain is wrapped onto the hull as an x-ray | |
| 751 | ||
| 752 | **Implemented at the author's request.** The exterior view paints the rendered inner | |
| 753 | surface onto the outside of the cylinder, alongside the place markers. | |
| 754 | ||
| 755 | **It is a projection, and the code says so.** From outside, Tower is structure; the | |
| 756 | texture shows what is *inside*. That makes it the frankest of the view's three | |
| 757 | reading aids — nobody could mistake a forest on the outer hull for a physical claim, | |
| 758 | where the fill lighting and the visible beam are subtler about it. It earns its | |
| 759 | place because the hull is opaque: this is the only view that shows the whole world | |
| 760 | at once. | |
| 761 | ||
| 762 | **The UVs are rewritten, not derived.** Three's cylinder runs u around the | |
| 763 | circumference and v along the axis; the raster is the other way round, x = z and | |
| 764 | y = theta. The obvious approach — recovering the angle from each vertex position — | |
| 765 | is wrong, because the seam vertices are duplicated and `atan2` gives both the same | |
| 766 | angle, smearing one column of triangles across the entire image. Remapping the | |
| 767 | original uv attribute stays linear and lets RepeatWrapping absorb the offset. | |
| 768 | ||
| 769 | **Alignment was verified, not eyeballed.** The raster was sampled at each place's | |
| 770 | computed position and the colour classified: four of four mountains landed on rock, | |
| 771 | both cap seas on water, the desert on desert. Judging this by eye would have been | |
| 772 | hopeless — the whole map is bands of green and tan, and a half-world error looks | |
| 773 | much like a correct one from most angles. | |
| 774 | ||
| 775 | **Two fixes the terrain forced.** Small bright dots read fine against a bare hull | |
| 776 | and vanish into forest and desert, so markers gained a dark outline shell; and they | |
| 777 | are now scaled per frame to a roughly constant screen size, because 4 km is | |
| 778 | sub-pixel at the 1,200 km the default view sits at. The terrain also needed an | |
| 779 | emissive map: sunlight runs along the axis, so most of the hull faces away from | |
| 780 | every light and the map would otherwise be legible only on a sliver. | |
| 781 | ||
| 782 | **Also fixed here:** selecting a place from the list moved the panel but not the | |
| 783 | camera — only permalinks called `focusOn`. Both paths now do. | |
3b520afd 2026-09-07 Pavel Safronov | 784 | |
| 785 | --- | |
| 786 | ||
| 787 | ## D-025 — An empty draft is not a draft | |
| 788 | ||
| 789 | **Fixed after the author hit it.** The map showed one place where twenty-two were | |
| 790 | expected. The local draft was empty and, because any draft beat the seed, the | |
| 791 | published world had been shadowed since the first time the app was opened. | |
| 792 | ||
| 793 | **How it happened.** `public/world.json` originally shipped as `{"places": []}`. | |
| 794 | Opening the app once in that window persisted an empty world to `localStorage` — | |
| 795 | `pagehide` flushes on close — and from then on `load()` saw a draft, took it, and | |
| 796 | never fetched the seed again. Twenty-two places were sitting in the served file, and | |
| 797 | nothing in the UI suggested anything was wrong: an empty world and a deliberately | |
| 798 | emptied world look identical. | |
| 799 | ||
| 800 | **The fix.** A stored world with no places is treated as no draft, so the seed | |
| 801 | loads. There is nothing to protect in an empty world, and the opposite failure — a | |
| 802 | deliberately emptied world refilling on reload — is visible and one click to undo, | |
| 803 | where this one was silent and permanent. | |
| 804 | ||
| 805 | **Plus an explicit way back.** *Reload published world* in the Data section discards | |
| 806 | the draft and refetches `world.json`, for the case where a draft has drifted rather | |
| 807 | than emptied. Verified end to end: an empty draft now recovers 22 places on reload, | |
| 808 | and reseeding after a local edit returns 23 to 22. | |
| 809 | ||
| 810 | **The general shape is worth remembering.** Local-first storage that silently wins | |
| 811 | over shipped content will, sooner or later, win with nothing in it. Precedence rules | |
| 812 | between a cache and its source need a defined answer for the empty case, and a | |
| 813 | manual override for every other case. | |
04d6779e 2026-09-07 Pavel Safronov | 814 | |
| 815 | --- | |
| 816 | ||
| 817 | ## D-026 — Two grids that did not agree, both removed | |
| 818 | ||
| 819 | **Removed at the author's request.** The exterior had structural ribs every 20 km; | |
| 820 | the map had a graticule every 50 km. Neither was wrong on its own, but side by side | |
| 821 | they invited a comparison that does not hold — twenty rings against ten lines, | |
| 822 | counting nothing in common. | |
| 823 | ||
| 824 | Both are gone. Neither was load-bearing: the map keeps its scale bar and the | |
| 825 | exterior its spin readout, and with the terrain drawn the ground itself carries the | |
| 826 | sense of distance far better than either grid did. A reference grid that has to be | |
| 827 | mentally reconciled with another reference grid is worse than none. | |
| 828 | ||
| 829 | **Worth generalising:** two independent scales shown in two views of the same object | |
| 830 | must agree or one must go. It is not enough for each to be individually correct. | |
| 831 | ||
| 832 | --- | |
| 833 | ||
| 834 | ## D-027 — A band is its own geometry, not a polygon | |
| 835 | ||
| 836 | **Fixed.** Regions that ring the world — the desert, the forest, both cap seas — are | |
| 837 | now `{ type: 'band', zFrom, zTo }` rather than four-cornered polygons. | |
| 838 | ||
| 839 | **Found while removing the graticule.** With the grid lines gone, a stray hairline | |
| 840 | was left across the middle of the map. A band written as a polygon runs theta from | |
| 841 | 0 to 2pi, and both of those normalise to *the same map row* — so all four corners | |
| 842 | land on one line and the region collapses into a one-pixel streak. It had been | |
| 843 | hiding among the graticule lines the whole time. | |
| 844 | ||
| 845 | **Why a new type rather than a workaround.** On a cylinder, "the whole circumference | |
| 846 | between two z" is a genuinely different shape from a polygon: it has no theta | |
| 847 | extent to give, because it has all of it. Tower's geography is built out of these — | |
| 848 | D-020 made the desert ring the world precisely because a band cannot be walked | |
| 849 | around — so it deserves to be a first-class shape rather than a polygon that happens | |
| 850 | to wrap. The map draws it as a full-height rectangle, hit-testing is a z range, and | |
| 851 | the editor reports it as *rings the world, z 148.0 km to 218.0 km (70.0 km deep)*. | |
| 852 | ||
| 853 | Tests assert the four ringing regions survive as bands through parse and public | |
| 854 | export, and that a band with unusable bounds is rejected like any other geometry. | |
8b6287ba 2026-09-07 Pavel Safronov | 855 | |
| 856 | --- | |
| 857 | ||
| 858 | ## D-028 — The beam was inverted, and cones fringe | |
| 859 | ||
| 860 | **Found because the author asked what the brown cone and the white circle were.** | |
| 861 | That question is its own answer: the two most important objects in the exterior view | |
| 862 | were unlabelled and one of them was drawn backwards. | |
| 863 | ||
| 864 | **The beam pointed the wrong way.** `CylinderGeometry(radiusTop, radiusBottom)` | |
| 865 | combined with `rotateX(-pi/2)` — which sends +Y to −Z — put the *first* radius at | |
| 866 | the collector. Writing the pair in the order the eye reads them gave a cone wide at | |
| 867 | the sun cap and narrow at the mirror: light converging **on** the collector instead | |
| 868 | of into the aperture, with a 122 km mouth swallowing a 50 km hull. It had been that | |
| 869 | way since the view was built, and the flat brown wash it rendered as hid it. | |
| 870 | ||
| 871 | **The brown was a second bug.** A uniform additive layer at 0.085 opacity over a | |
| 872 | black sky sums to about RGB(43,39,30) — a dull brown that reads as a solid object, | |
| 873 | which is the opposite of a beam. | |
| 874 | ||
| 875 | **Cones fringe, so the beam is rays now.** Brightening it exposed bright dashes | |
| 876 | scattered across the collector's rim. Isolated by bisection: removing the cone | |
| 877 | cleared the disc, removing the stars did not, and shortening the cone moved the | |
| 878 | dashes with its mouth. At the mouth the cone's shell is edge-on, so its triangles | |
| 879 | project to slivers and the additive blend stacks them. Rays from the mirror to the | |
| 880 | aperture have no silhouette to alias, need no alpha ramp to fake convergence, and | |
| 881 | read more like light than a translucent solid ever did. | |
| 882 | ||
| 883 | **Everything is captioned now.** Collector, beam, sun cap, far cap, as screen-space | |
| 884 | labels. If the person who commissioned the world cannot identify an object in it, a | |
| 885 | reader has no chance. | |
| 886 | ||
| 887 | --- | |
| 888 | ||
| 889 | ## D-029 — The view persists, which surfaced a boot-order bug | |
| 890 | ||
| 891 | **Implemented at the author's request.** Map or exterior is remembered in | |
| 892 | `localStorage` and carried in the permalink hash, so a shared link can say *look at | |
| 893 | this from outside*. The hash wins; storage is the fallback for a bare URL. | |
| 894 | ||
| 895 | **And it immediately broke the exterior on load.** Restoring that view un-hides the | |
| 896 | canvas and constructs the renderer in the same task, before the browser has laid the | |
| 897 | element out — so `resize()` measured a zero-sized box, bailed out, and the renderer | |
| 898 | kept its default 300×150 buffer stretched across a full-size canvas. Clicking into | |
| 899 | the view had always worked because layout had long since settled by then; only | |
| 900 | restoring it on boot hit the window. | |
| 901 | ||
| 902 | Fixed with a `ResizeObserver` on the canvas rather than another guess about timing, | |
| 903 | which also covers the sidebar and window resizing for free. | |
15d933c9 2026-09-07 Pavel Safronov | 904 | |
| 905 | --- | |
| 906 | ||
| 907 | ## D-030 — The camera persists, and the caps are places | |
| 908 | ||
| 909 | **Both at the author's request.** | |
| 910 | ||
| 911 | **Camera.** Position and target are stored per browser and restored on load, so a | |
| 912 | reload returns to the vantage you left rather than the default framing. Every field | |
| 913 | is validated before use — three finite numbers each, and a distance inside the | |
| 914 | controls' own limits — because a corrupt entry here would leave the view pointing at | |
| 915 | nothing with no obvious way back, and silently-bad persisted state has already cost | |
| 916 | this project once (D-025). Saves are debounced: OrbitControls fires `change` every | |
| 917 | frame of a drag. | |
| 918 | ||
| 919 | **Caps.** The sun cap and far cap are now features in `content/geography.json`, so | |
| 920 | they appear in the place list and can be focused like anywhere else. Two things had | |
| 921 | to be special-cased, both for the same underlying reason — *a cap is not a patch of | |
| 922 | ground*: | |
| 923 | ||
| 924 | - **Its marker belongs on the axis.** Placed on the hull at theta = 0 like every | |
| 925 | other marker, it sat on the cap's rim and rode round the edge as Tower turned. | |
| 926 | A cap's location is its centre. | |
| 927 | - **Its framing is different.** The usual from-the-side shot shows an end cap edge-on | |
| 928 | as a line. Standing off along the axis instead is worse — it looks down the barrel | |
| 929 | and foreshortens all 540 km behind an unlit disc. What works is the default view's | |
| 930 | oblique angle slid onto the cap, and a light from deepward so the far cap's outer | |
| 931 | face is not a black hole exactly when you focus on it. | |
| 932 | ||
| 933 | **A note on the debugging.** Verifying camera persistence took several wrong turns | |
| 934 | because the test kept invalidating itself: `location.reload()` fires `pagehide`, | |
| 935 | which saved the live camera over the value the test had just planted. The | |
| 936 | persistence was working the whole time. Worth remembering that a `pagehide` save | |
| 937 | makes "set state, reload, check state" an invalid experiment. | |
0e0c2dba 2026-09-07 Pavel Safronov | 938 | |
| 939 | --- | |
| 940 | ||
| 941 | ## D-031 — Sidebar: an accordion, and a label that stopped lying | |
| 942 | ||
| 943 | **Five UI corrections from the author, all of them about the panel saying the wrong | |
| 944 | thing or being in the wrong place.** | |
| 945 | ||
| 946 | **"Canon" was a section heading for one slider.** It read *Canon → Star slats — 0*, | |
| 947 | which framed an exploration control as if the whole section were canon. The section | |
| 948 | is now **Star slats**, its value is a **Count**, and *canon* is a chip on the | |
| 949 | summary that appears only when the count is 0 — because that is the one value canon | |
| 950 | actually asserts (D-017). A label should describe what it labels; "canon" is a | |
| 951 | property of a value here, not a category of setting. | |
| 952 | ||
| 953 | **Order follows scope.** *View* moved to the top: it changes the whole app, not one | |
| 954 | panel. *About* moved to the bottom, where the least-used thing belongs. | |
| 955 | ||
5e405272 2026-09-07 Pavel Safronov | 956 | **Foldable sections.** Seven panels in a fixed column pushed the useful ones |
| 957 | off-screen. They are `<details>` elements now — native disclosure, keyboard and | |
| 958 | screen-reader behaviour for free. *This first shipped as an accordion, one open at a | |
| 959 | time, with the Selected panel opening itself on selection; both were removed — see | |
| 960 | D-032.* | |
0e0c2dba 2026-09-07 Pavel Safronov | 961 | |
| 962 | **Mode gating moved from a CSS class to the `hidden` property**, since `<details>` | |
| 963 | already carries open/closed state and stacking a `.hidden` class on top of it was a | |
| 964 | second, conflicting notion of visibility. | |
| 965 | ||
| 966 | **And a claim removed.** The About text said the canon was "worked out in the open", | |
| 967 | with links to design.md and decisions.md. Those documents are the author's working | |
| 968 | notes on an unpublished novel, not a public invitation, and the links went to a repo | |
| 969 | a reader may not have. Now it says what is true and no more: *Tower is the setting | |
| 970 | of a novel. This map is its companion piece.* | |
5e405272 2026-09-07 Pavel Safronov | 971 | |
| 972 | --- | |
| 973 | ||
| 974 | ## D-032 — No automatic opening or closing | |
| 975 | ||
| 976 | **Reverted at the author's request:** the accordion was annoying, and nothing should | |
| 977 | open or close by itself. | |
| 978 | ||
| 979 | **The two behaviours were one mistake.** Exclusive opening meant that clicking a | |
| 980 | place could collapse the panel about to describe it, so the Selected panel had to | |
| 981 | open itself to compensate. Removing the accordion removed the need for the | |
| 982 | compensation: sections the author opens stay open, so a selection simply appears in | |
| 983 | a panel that is already there. A workaround that only exists to undo another | |
| 984 | behaviour is a sign the first behaviour is wrong. | |
| 985 | ||
| 986 | **What remains is state, not behaviour.** View and World start open, and Selected | |
| 987 | starts open but stays hidden until something is selected — so the first selection | |
| 988 | shows without anything having moved. After that the panels are wherever the author | |
| 989 | put them: closing the editor keeps it closed through every later selection. | |
| 990 | ||
| 991 | **The general rule this is worth remembering by:** an interface that rearranges | |
| 992 | itself in response to a click costs the user their place. Prefer a sensible initial | |
| 993 | state and then hold still. | |
cf37225c 2026-09-07 Pavel Safronov | 994 | |
| 995 | --- | |
| 996 | ||
| 997 | ## D-033 — The slat control is disabled and its UI removed | |
| 998 | ||
| 999 | **Removed at the author's request:** changing the count did not fully work. It did | |
| 1000 | not, and for two independent reasons, both worth writing down because this is | |
| 1001 | deferred rather than abandoned. | |
| 1002 | ||
| 1003 | **The raster has no slats in it.** `terrain-color.png` is generated from | |
| 1004 | `content/geography.json` at canon's 0 slats. Raising the count drew slat bands as a | |
| 1005 | vector overlay on top of terrain that shows unbroken land underneath them — two | |
| 1006 | layers asserting different worlds. The corridor is authored the same way: Snowcrown, | |
| 1007 | the rivers and The Narrows all assume no slats, so bands above 0 cut straight | |
| 1008 | through them. | |
| 1009 | ||
| 1010 | **And the map's cut angle moves with the count.** `mapThetaOrigin()` returns | |
| 1011 | `slatCentre(n-1)` when slats exist and π when they do not (D-013, D-024). The raster | |
| 1012 | is drawn against π, so every non-zero count rotates the terrain out of register with | |
| 1013 | its own place markers — by 120° at 3 slats, 157° at 8, up to **137 km** of | |
| 1014 | displacement. This is the same desync D-024 fixed for the zero case, and the test | |
| 1015 | added there only guards the zero case. | |
| 1016 | ||
| 1017 | **What was done.** The section is gone from the sidebar and `loadSlatCount()` is no | |
| 1018 | longer called, so a stored non-zero count from before cannot leak back in. What was | |
| 1019 | *kept* is everything that makes the count correct: `src/canon/slats.ts`, the | |
| 1020 | derivation, and the tests covering counts 1 to 16 — including that land stays | |
| 1021 | exactly Wisconsin's 140,268 km² across the whole range, and that the map cut lands | |
| 1022 | on a slat centre at every count. | |
| 1023 | ||
| 1024 | **What re-enabling would need.** The terrain tool would have to bake slats into the | |
| 1025 | raster from the same count the app uses, which means the count becomes an input to | |
| 1026 | `npm run terrain` rather than a runtime toggle — and the geography would need | |
| 1027 | re-authoring around wherever the bands fall. That is a content decision as much as a | |
| 1028 | code one, which is the real reason it is parked. | |
28f9a6bc 2026-09-07 Pavel Safronov | 1029 | |
| 1030 | --- | |
| 1031 | ||
| 1032 | ## D-034 — The interior view: real geometry, and haze doing the work | |
| 1033 | ||
| 1034 | Phase 5, and the piece design.md §2.4 said would carry the illusion. The scope held: | |
| 1035 | **real geometry, simple ground.** The shell is the actual cylinder, displaced by the | |
| 1036 | height raster and painted with the colour raster; there is no scenery, no buildings | |
| 1037 | and no vegetation. | |
| 1038 | ||
| 1039 | **Nothing in the scene rotates, and that is the point.** The observer, the ground and | |
| 1040 | the air all co-rotate, so from inside a sealed hull the spin is genuinely invisible. | |
| 1041 | No stars wheel overhead because there are no stars (§1.4). What is left is | |
| 1042 | `src/canon/motion.ts`, and it says something the design did not anticipate: | |
| 1043 | ||
| 1044 | | | lean, antispinward | | |
| 1045 | |---|---| | |
| 1046 | | rain at terminal velocity | **1.5°** — fixed, whatever the cloud height | | |
| 1047 | | a 1 km waterfall, at the bottom | **11.3°**, and 133 m of drift | | |
| 1048 | ||
| 1049 | The waterfall figure is the *velocity* at the bottom, which is the angle a streak of | |
| 1050 | falling water is drawn at. §1.9.2 tabulates 7.6° for the same drop, which is the | |
| 1051 | tilt of the *path* — where it lands against how far it fell. Both are right and they | |
| 1052 | differ by exactly half again in tangent (`freeFallPathLeanRad` alongside | |
| 1053 | `freeFallLeanRad`, pinned in test/motion.test.ts), and noticing they were not the | |
| 1054 | same number is the only reason this is written down rather than silently | |
| 1055 | contradicting the canon. | |
| 1056 | ||
| 1057 | Rain barely leans, and it leans by a *constant* angle because drag holds the fall | |
| 1058 | speed constant. Only free fall keeps accelerating, so only free fall keeps leaning. | |
| 1059 | Design.md §2.4 offered "slanted rain and a leaning waterfall" as a pair; they are not | |
| 1060 | a pair. **The waterfall is the compass.** The rain is drawn, and the readout prints | |
| 1061 | 1.5° beside it so the smallness reads as the finding it is rather than as a bug. | |
| 1062 | ||
| 1063 | **Haze is integrated per fragment**, 24 midpoint samples through the rotational | |
| 1064 | density profile, and it reproduces the §1.8 table from the other direction: the | |
| 1065 | readout prints 78.1% straight up against 31.3% for the same 100 km along the ground. | |
| 1066 | The extinction is carried per channel (Rayleigh goes as λ⁻⁴, so red survives twice | |
| 1067 | as far as green and blue half as far), which is what makes distance blue rather than | |
| 1068 | merely grey. | |
| 1069 | ||
| 1070 | **The airlight is neutral, deliberately.** Tinting it sky blue is the obvious move | |
| 1071 | and it is wrong: blue already scatters 2.4× harder in the per-channel extinction, so | |
| 1072 | a blue source colours the haze twice and every distant thing turns mauve regardless | |
| 1073 | of what it is made of — desert included, which is how the bug was spotted. With a | |
| 1074 | neutral source the blue falls out of the physics and the far side stays recognisably | |
| 1075 | forest and desert at the 78% canon promises. | |
| 1076 | ||
| 1077 | ## D-035 — Standing on the ground requires the ground on the CPU | |
| 1078 | ||
| 1079 | The shell is displaced in the vertex shader, which is fine until somebody has to | |
| 1080 | stand on it. The first build put the camera 1.7 m above the *undisplaced* cylinder — | |
| 1081 | underground everywhere the land is higher than 1.7 m, which is everywhere. From | |
| 1082 | inside the rock the ground is invisible: its underside is a front face and the shell | |
| 1083 | is drawn back-side only. The view showed sky below the horizon and nothing else, and | |
| 1084 | at 366 m of eye height it looked perfect, which is what made it puzzling. | |
| 1085 | ||
| 1086 | There is no way to ask the GPU where the ground is, so `terrain-height.png` is now | |
| 1087 | read back through a canvas and sampled here. Two mismatches had to be handled: | |
| 1088 | ||
| 1089 | - **The mesh is coarser than the raster.** Cells are 0.7 km, texels are 0.26 km, so | |
| 1090 | the surface actually drawn is the raster sampled at the cell corners and stretched | |
| 1091 | flat between them. The lookup interpolates the same four corners rather than | |
| 1092 | sampling the raster directly, which would put the observer's feet tens of metres | |
| 1093 | off in rough country. | |
| 1094 | - **Underground is much worse than hovering.** Half the spread across the cell is | |
| 1095 | added as clearance: nil on the plains, and on a ridge it lifts you clear of the | |
| 1096 | facet instead of dropping you inside it. | |
| 1097 | ||
| 1098 | The readback is 8-bit — browsers decode 16-bit PNGs down to 8 — so elevations | |
| 1099 | quantise to 19.5 m steps. Both the shader and this lookup read the same quantised | |
| 1100 | data, so they agree with each other, which is what matters for not being buried. | |
| 1101 | ||
| 1102 | ## D-036 — A custom ShaderMaterial writes linear colour into an sRGB canvas | |
| 1103 | ||
| 1104 | Three inserts its colour-space handling into its own materials and never into a | |
| 1105 | custom one, and the first build silently got both halves wrong: the colour raster | |
| 1106 | was sampled as if it were linear, and linear output was written straight to the | |
| 1107 | canvas. Everything read dark and oversaturated, and the far side of the world came | |
| 1108 | out navy where it should have been hazy green. | |
| 1109 | ||
| 1110 | The fix is both halves explicitly — decode the raster with a `pow(c, 2.2)` and end | |
| 1111 | each fragment shader with `#include <colorspace_fragment>`. The trap on the way: | |
| 1112 | adding `#include <colorspace_pars_fragment>` too fails to link, because Three | |
| 1113 | already puts that chunk in the prefix it builds for *every* material in order to | |
| 1114 | define `linearToOutputTexel`. The duplicate definitions produced no console error at | |
| 1115 | all — only `INVALID_OPERATION: no valid shader program in use` once per frame, and a | |
| 1116 | window filled with clear colour. | |
| 1117 | ||
| 1118 | ## D-037 — A shared `#view=` link was erased before it was read | |
| 1119 | ||
| 1120 | Found while opening the interior view by URL. `App`'s constructor rendered before it | |
| 1121 | decided which view to open, and `render()` rewrites the hash from current state — so | |
| 1122 | `#view=interior` was replaced by an empty hash a few lines before `initialView()` | |
| 1123 | went looking for it. Every shared link that named a view had been landing on the map | |
| 1124 | since D-029, and it went unnoticed because the last-used view is *also* persisted in | |
| 1125 | `localStorage`, which restored the right view for whoever had visited before. The | |
| 1126 | view is now decided before the first render. | |
| 1127 | ||
| 1128 | ## D-038 — Selecting a place walks you there | |
| 1129 | ||
| 1130 | Turning to face a place was the wrong default, and standing in the view for a minute | |
| 1131 | makes it obvious: at ground level the haze horizon closes at 100–150 km (§1.8), so | |
| 1132 | "face the Deepward Sea" from the capital points at 270 km of nothing. Selecting a | |
| 1133 | place now *moves* the observer to it, which is what "focus" already meant in the | |
| 1134 | other two views — the map centres, the exterior swings round, the interior walks. | |
| 1135 | ||
| 1136 | Places with extent also get a heading: stand at the head of a river and look down | |
| 1137 | it, taking the centroid of the traced points as the aim. A point has no direction of | |
| 1138 | its own, so arriving is the whole answer and the heading is left alone. The separate | |
| 1139 | "Stand at the selected place" button is gone, since selection now does exactly that. | |
| 1140 | ||
| 1141 | ## D-039 — The height raster gets a declared working range | |
| 1142 | ||
| 1143 | `terrain-height.png` was normalised 0..5,000 m against a fixed `maxElevationM`, and | |
| 1144 | both halves of that were wrong. | |
| 1145 | ||
| 1146 | **The floor mattered more than the ceiling.** Clamping at zero flattened everything | |
| 1147 | below the datum, so every sea and lake floor was a plane — the Deepward Sea read as | |
| 1148 | 0 m where it is actually −320 m. `content/geography.json` now declares | |
| 1149 | `minHeightM` / `maxHeightM` and the raster is stretched across that range. | |
| 1150 | ||
| 1151 | **The range is declared, not fitted.** Fitting it to the terrain would silently | |
| 1152 | change what every grey level means whenever the geography is edited, which is the | |
| 1153 | kind of drift D-024 came from. Instead the generator prints the range it found | |
| 1154 | against the range declared, warns and clamps if the terrain has outgrown it, and | |
| 1155 | test/raster.test.ts asserts the declared range still contains the terrain. | |
| 1156 | ||
| 1157 | **The precision win is smaller than it looks**, and worth stating plainly: the | |
| 1158 | terrain spans −508..4,392 m, so tightening the range to −520..4,400 m moves the step | |
| 1159 | from 19.6 m to 19.3 m. The quantisation is not set by the range at all — it is set | |
| 1160 | by browsers decoding a 16-bit PNG down to 8 bits, which leaves 256 levels however | |
| 1161 | wide the span. The fix for *that* is a second byte in a second channel, which is a | |
| 1162 | change to the encoder and the shader, and is not made here. | |
| 1163 | ||
| 1164 | ## D-040 — The axial sightline names the distance it measured | |
| 1165 | ||
| 1166 | The readout compared "100 km up" against "100 km on" — except the along-axis probe | |
| 1167 | was clamped to the far cap, so standing 22 km from the end it measured 22 km and | |
| 1168 | still called it 100. It reported 77.5% where the honest figure for 100 km is 31%, | |
| 1169 | which is exactly backwards from the point the line exists to make. It now measures | |
| 1170 | whichever axial direction has the most room and prints the distance it used. | |
| 1171 | ||
| 1172 | ## D-041 — The caps get a pattern, and it is polar | |
| 1173 | ||
da6bcb0e 2026-09-08 Pavel Safronov | 1174 | *Half superseded by D-045: the far cap is now a window, and this pattern survives |
| 1175 | on it as the window's mullions rather than as a filled checker. Everything below | |
| 1176 | still holds — the tessellation, the pitch, the mipmapping and the reason for all | |
| 1177 | three are unchanged, and the sun cap is still exactly this.* | |
| 1178 | ||
28f9a6bc 2026-09-07 Pavel Safronov | 1179 | The end caps were flat tinted discs. They are the only surfaces in Tower that are |
| 1180 | not landscape — 100 km of structure closing each end — and blank they read as fog | |
| 1181 | with an edge. A checker fixes that, and does something more useful besides: it is | |
| 1182 | the one thing in the world with a **known cell size**, so a glance at the far cap | |
| 1183 | tells you roughly how far away it is. That is worth having in a world where §1.8 | |
| 1184 | says distance is otherwise unreadable. | |
| 1185 | ||
| 1186 | **Polar, not square.** A cap is a disc on a spinning cylinder, so its natural | |
| 1187 | coordinates are radius and angle; a square grid meets the rim at every possible | |
| 1188 | angle and reads as something laid over the geometry rather than part of it. Cells | |
| 1189 | stay roughly square by doubling the sector count each time the radius doubles, which | |
| 1190 | leaves a seam at each doubling — which is what a segmented diaphragm would have | |
| 1191 | anyway. The alternative is slivers at the hub or slabs at the rim. | |
| 1192 | ||
| 1193 | **One texture, both views.** Drawn once into a canvas and shared, so the exterior | |
| 1194 | and the interior cannot drift apart. Three maps UVs across the bounding box for both | |
| 1195 | `CircleGeometry` and `RingGeometry`, so the same image lands correctly on the far | |
| 1196 | cap's full disc and on the sun cap's annulus. | |
| 1197 | ||
| 1198 | Two things had to be walked back after looking at it: | |
| 1199 | ||
| 1200 | - **It must be mipmapped.** A high-contrast geometric pattern is the worst case for | |
| 1201 | point sampling. My first pass disabled mipmaps on the theory that resolving the | |
| 1202 | checker at distance was the whole point; minified, the far cap moiréd into a dark | |
| 1203 | cross-hatch that read as damage. Averaging to grey once the cells fall below a | |
| 1204 | pixel is both correct and honest — you cannot resolve them from 540 km either. | |
| 1205 | - **Six rings, not ten.** 5 km cells looked better face-on from inside and collapsed | |
| 1206 | from outside, where D-030 deliberately frames the caps near edge-on and | |
| 1207 | foreshortens them ten to one. A pattern has to survive its worst viewing angle. | |
| 1208 | ||
| 1209 | The cap framing also moved to ~40° off the axis rather than ~17°, since there is now | |
| 1210 | something on the face worth turning to see, and the caps carry an emissive copy of | |
| 1211 | the pattern for the same reason the hull carries one of the terrain: sunlight runs | |
| 1212 | along the axis, so a cap is edge-on to every real light and renders as a dark disc | |
| 1213 | exactly when it is being looked at. | |
fc11b064 2026-09-08 Pavel Safronov | 1214 | |
6364748a 2026-09-08 Pavel Safronov | 1215 | ## D-042 — Screenshots are taken through the UI, and diffed per shot |
| 1216 | ||
| 1217 | `npm run screenshots` drives the real application in a headless browser and | |
| 1218 | photographs it: nine views across two viewports, plus a contact sheet of all nine. | |
| 1219 | ||
| 1220 | **Through the UI, on purpose.** Every shot clicks the view buttons, picks places out | |
| 1221 | of the list, and drags to orbit and to look up, rather than posing a renderer | |
| 1222 | directly. Posing is easier and it defeats the point. A screenshot taken by using the | |
| 1223 | application fails when the application fails, so a black canvas or a sidebar that | |
| 1224 | has swallowed the window ends up *in the picture* instead of hiding behind a passing | |
| 1225 | unit test. The tests already pin the canon numbers; nothing was checking that the | |
| 1226 | thing still draws. | |
| 1227 | ||
| 1228 | **The contact sheet is for a human.** `overview.png` exists to be looked at once, | |
| 1229 | before committing — one image that answers "is it still there?". It is built by | |
| 1230 | laying the shots out as an HTML page and screenshotting that, because the browser is | |
| 1231 | already running and already knows how to lay images out; `tools/png.mjs` only | |
| 1232 | encodes, and this project has no reason to gain a PNG decoder. | |
| 1233 | ||
| 1234 | **Diffed per shot, not per sheet.** The first version measured divergence on the | |
| 1235 | contact sheet, and that was the wrong unit: rewriting two of the nine panels | |
| 1236 | outright moved the whole-sheet mean by 0.6%, because a mean over nine panels on a | |
| 1237 | dark page dilutes everything. Per shot the same change reads in tens of percent. | |
| 1238 | ||
| 1239 | **The 3% threshold is measured, at both ends.** Seven of the ten shots come out | |
| 1240 | byte-identical run to run — the rain is the only randomised thing in Tower and is | |
| 1241 | seeded on the way in, the exterior spin is switched off, and each shot gets a fresh | |
| 1242 | browser context so no camera leaks into the next. The two exterior shots are the | |
| 1243 | exception, drifting up to 0.98% because OrbitControls damps toward its target | |
| 1244 | asymptotically and lands a hair short of the same place each time. Against that, | |
| 1245 | hiding both 3D canvases scored 7–31%; a view going entirely black scores 12% to 39%. | |
| 1246 | 3% is three times the worst noise and less than half the mildest real failure. | |
| 1247 | ||
| 1248 | It is a smoke alarm, not a verdict: when it fires the answer is to look at the | |
| 1249 | sheet. And it only catches *regressions* — a view that never worked scores 0% when | |
| 1250 | it breaks further, which is exactly how mobile-exterior behaved during the sabotage | |
| 1251 | test that calibrated this, because at the time it was already a black slot. | |
| 1252 | ||
| 1253 | **Two browsers, one tour.** The contact sheet gets its own browser instance. In the | |
| 1254 | one that has just taken the tour — nine WebGL contexts and two Three.js scenes — | |
| 1255 | `Page.captureScreenshot` on the sheet fails outright with "Unable to capture | |
| 1256 | screenshot", and the sheet is plain HTML and images, so the only thing wrong with it | |
| 1257 | is its neighbours. | |
| 1258 | ||
| 1259 | **What it found immediately.** The mobile row: the sidebar was a fixed 340 px | |
| 1260 | against a 390 px viewport, so the world got about fifty pixels and both 3D views | |
| 1261 | rendered into a black slot. Fixed in D-043, which the same tool then verified — | |
| 1262 | the six desktop shots moved 0.00% and the three mobile ones moved 10–33%, which is | |
| 1263 | exactly the shape a layout fix should have. | |
| 1264 | ||
fc11b064 2026-09-08 Pavel Safronov | 1265 | ## D-043 — On a phone the sidebar is a drawer |
| 1266 | ||
| 1267 | The layout was one grid, `1fr 340px`, with no media query anywhere. On a 390 px | |
| 1268 | phone that is a 340 px panel and fifty pixels of world, and nothing to hide the | |
| 1269 | panel with — the application was, on a phone, a control surface for something you | |
| 1270 | could not see. | |
| 1271 | ||
| 1272 | **A drawer, not a stack.** The other obvious move is to put the panel below the | |
| 1273 | world in a single column and let the page scroll. That is wrong here: the world is a | |
| 1274 | `position: absolute` canvas filling `main`, all three views are drag-to-look, and | |
| 1275 | `touch-action: none` means every gesture over the world belongs to the world. A | |
| 1276 | scrolling page and a canvas that eats touches are in permanent conflict. A drawer | |
| 1277 | keeps the canvas full-bleed and the gesture unambiguous. | |
| 1278 | ||
| 1279 | **Below 720 px**, where the panel would otherwise be taking more than half the | |
6364748a 2026-09-08 Pavel Safronov | 1280 | window. Above it nothing changes at all — the desktop shots come out of the |
| 1281 | screenshot tour byte-identical. | |
fc11b064 2026-09-08 Pavel Safronov | 1282 | |
| 1283 | **Never the full width.** `min(340px, 86vw)` leaves a strip of world showing, which | |
| 1284 | is what tells you the panel is over the map rather than a page you navigated to. | |
| 1285 | That strip is live canvas, though, and in writer mode a stray tap on it drops a | |
| 1286 | marker, so a scrim covers it and closes the drawer instead. | |
| 1287 | ||
| 1288 | **Two buttons, not one.** The opener is fixed at the top right, opposite the view | |
| 1289 | switcher. The closer is in the panel header, in normal flow. A single fixed | |
| 1290 | toggle would have to float over the panel's own scrolling content once open, and | |
| 1291 | would occlude whatever scrolled under it; this way each button is only ever over the | |
| 1292 | thing it belongs to. The spin readout, which also lives at the top right, drops to | |
| 1293 | `top: 54px` to clear the opener. | |
| 1294 | ||
| 1295 | **Picking a place closes it.** In every view selecting a place moves you: the map | |
| 1296 | centres, the exterior swings round, the interior walks there (D-038). On a phone the | |
| 1297 | list *is* the drawer, so staying open means watching none of it happen. | |
| 1298 | ||
| 1299 | **The closed drawer is `inert`.** It is only translated off-screen, so without that | |
| 1300 | its fields stay in the tab order — typing into a panel nobody can see, scrolling it | |
| 1301 | under the world. It is set from the same media query the CSS uses, and crossing that | |
| 1302 | breakpoint (a phone turned on its side) resets to closed, which is also what clears | |
| 1303 | `inert` on the way back to the desktop column. | |
| 1304 | ||
| 1305 | **One thing came along with it.** The standing readout is ~65 monospace characters | |
| 1306 | of aligned columns and overran 390 px at 11 px type. It shrinks to 9 px on the same | |
| 1307 | breakpoint rather than wrapping — wrapping those columns destroys the thing they are | |
| 1308 | for. | |
de65ab10 2026-09-08 Pavel Safronov | 1309 | |
| 1310 | ## D-044 — Tower's wall is a kilometre thick | |
| 1311 | ||
| 1312 | Canon had no figure for it. Both 3D views drew the hull, the caps and the collector | |
| 1313 | as zero-thickness surfaces, which is invisible until something is seen edge-on and | |
| 1314 | then reads as a paper cutout rather than a structure. | |
| 1315 | ||
| 1316 | **1 km, floor to outer skin** — 2% of the radius. The alternatives were a 200 m | |
| 1317 | technical minimum, which is defensible on shielding and services and is sub-pixel at | |
| 1318 | every exterior zoom, and 5 km, which is unmistakable and a great deal of mass to | |
| 1319 | justify later. A kilometre reads as built without making Tower a pipe. | |
| 1320 | ||
| 1321 | **It is only visible at the openings**, which is worth stating because it took a | |
| 1322 | false start to notice. A closed can has no visible thickness from outside — the | |
| 1323 | silhouette of a 50 km cylinder does not change when you give its wall a kilometre. | |
| 1324 | Depth shows only where there is a hole: the beam aperture, and now the far cap | |
| 1325 | window. So the caps sit at the **outer** face of the wall with a 1 km collar | |
| 1326 | standing between them and the inner surface, and it is the collar you see down. | |
| 1327 | ||
| 1328 | **The exterior camera had to be let closer.** A kilometre is 1% of the cap's | |
| 1329 | diameter, so at the old 120 km minimum distance the collar was two pixels and the | |
| 1330 | near plane sat at 8 km — the depth was being drawn correctly and could not be got | |
| 1331 | close enough to to see. 60 km and a 2 km near plane make the choice legible; without | |
| 1332 | that change the constant would have been a number in a file and nothing else. | |
392bb8a7 2026-09-08 Pavel Safronov | 1333 | |
769726ea 2026-09-08 Pavel Safronov | 1334 | ## D-045 — The far cap is a window, and the checker becomes its frame |
| 1335 | ||
| 1336 | §1.4 has said since the first draft that the caps are the only views out of Tower | |
| 1337 | and that the sky through them turns rather than sweeps — a pinwheel about the axis | |
| 1338 | with one motionless star at the middle, one revolution every 7 min 29 s. It was | |
| 1339 | written and never built: §2.4 said flatly "no stars", and D-041 had meanwhile made | |
| 1340 | the far cap a checkered structural plate. | |
| 1341 | ||
| 1342 | **Glazed whole, not windows at its foot.** §1.4 put the windows at the base of a | |
| 1343 | 50 km cliff. A strip at the bottom of something 540 km away is nearly nothing to | |
| 1344 | look at, and the canon fact worth having in the view is the pinwheel, which needs | |
| 1345 | sky. So the whole far cap is a window. The sun cap is not — it faces the sun and | |
| 1346 | needs glare management rather than a view, and that asymmetry is already canon. | |
| 1347 | ||
| 1348 | **The checker survives as the mullions.** Deleting it would have retired the only | |
| 1349 | object in Tower with a known cell size, which is the only rule anyone has for | |
| 1350 | judging distance (D-041, §1.8). Instead the same polar tessellation at the same | |
| 1351 | pitch is drawn as the frame around transparent panes: the cells are exactly where | |
| 1352 | they were, and you look through them rather than at them. The rule now reads against | |
| 1353 | the sky, which is a better place for it than against a plate. | |
| 1354 | ||
| 1355 | **The physics fell out of the shader that was already there.** A pane is black with | |
| 1356 | stars in it, so the existing haze integration does the rest by itself: by day the | |
| 1357 | airlight term swamps everything and the cap is a disc of blue, which is what §1.5 | |
| 1358 | already said would happen to anything 540 km off; at night the airlight collapses | |
| 1359 | with the beam and what is left is the sky. Nothing needed special-casing. | |
| 1360 | ||
| 1361 | **The stars are not scaled by the exposure, and that is the whole point.** Nothing | |
| 1362 | Tower does to its own beam changes how bright Vega is. What does change is the eye, | |
| 1363 | so a separate gain carries that half: full at night and below, 2% at temperate. The | |
| 1364 | stars are there at noon and unseeable, which is a fact about eyes and not glass. | |
| 1365 | ||
| 1366 | **Two projections were wrong before the third.** Gnomonic (xy/z) about the axis puts | |
| 1367 | the pole star at the origin and blows up as the ray goes perpendicular to the axis — | |
| 1368 | which is the ordinary grazing view from the foot of the cap, not an edge case. | |
| 1369 | Painting the field on the cap plane instead is stable everywhere and has no fixed | |
| 1370 | angular scale: from a kilometre away the entire 60° field lands inside one star | |
| 1371 | cell and the sky comes out empty, which is exactly what the first screenshot showed. | |
| 1372 | Azimuthal-equidistant has neither failure. It is defined for every direction but | |
| 1373 | exactly sunward, stars keep the same angular size however far off the window is — | |
| 1374 | which is what being at infinity means — and the pinwheel is not animated but falls | |
| 1375 | out of the projection, because azimuth plus spin phase *is* the rotation. | |
| 1376 | ||
| 1377 | The pole star is placed rather than left to the hash. It is the fixed point the | |
| 1378 | whole phenomenon is read against, and a sky that happened not to roll one there | |
| 1379 | would be missing the only part of it anybody in Tower would have named. | |
| 1380 | ||
da6bcb0e 2026-09-08 Pavel Safronov | 1381 | **Settled by D-048:** the surviving quarter of the beam does not go through the |
| 1382 | window at all. It arrives collimated in a 3.2 km spot — a thousandth of the cap — | |
| 1383 | and that spot is a mirror, not glass. | |
769726ea 2026-09-08 Pavel Safronov | 1384 | |
392bb8a7 2026-09-08 Pavel Safronov | 1385 | ## D-046 — Night is the beam banked, not the swarm aimed away |
| 1386 | ||
| 1387 | The obvious way to make night in a habitat lit by a steerable mirror swarm is to | |
| 1388 | aim the swarm off. It does not work here, and §1.3's own numbers say so. | |
| 1389 | ||
| 1390 | The beam glows **by itself**: §1.3 gives the glow to the beam and the air rather | |
| 1391 | than to the diffusers, and says outright it "would be there if the beam were empty". | |
| 1392 | 779 W/m²/sr over a strip 3.65° wide running horizon to horizon is about 0.2 sr, and | |
| 1393 | with the mean cosine of a strip through the zenith that is **~100 W/m² on the | |
| 1394 | floor** — half of temperate. A lit beam is a light source no matter where the | |
| 1395 | mirrorlets are pointed, so a dark world requires a dark beam. There is a test for | |
| 1396 | this figure in `test/canon.test.ts`, because the conclusion rests entirely on it. | |
| 1397 | ||
| 1398 | So **night is the beam banked to a thousandth of full, 0.32 W/m²**: a rod you can | |
| 1399 | still find overhead and a world you can just make out under it. And **dark is the | |
| 1400 | beam shut down** — no airlight, no lit ground, no rod, and the only thing left in | |
| 1401 | the world is the sky through the far cap (D-045). The two settings are genuinely | |
| 1402 | different states rather than two points on a dimmer, which is what made both worth | |
| 1403 | having. | |
| 1404 | ||
| 1405 | The beam's brightness scales with the exposure already, so it banks and goes out on | |
| 1406 | its own; neither setting needed the beam shader touched. | |
a3f01bd7 2026-09-08 Pavel Safronov | 1407 | |
| 1408 | ## D-047 — The exterior hull reads two ways, and they are different claims | |
| 1409 | ||
| 1410 | The exterior had a "Show terrain" checkbox that wrapped the raster onto the hull. | |
| 1411 | It was doing something stronger than a checkbox admits: from outside Tower is | |
| 1412 | structure, so terrain on the outside is not what Tower looks like — it is a diagram | |
| 1413 | of what is inside, drawn on the outside because the wall is opaque. Useful, and not | |
| 1414 | a view. The two readings are different claims about the same object, so the control | |
| 1415 | is now a two-way switch that names both: | |
| 1416 | ||
| 1417 | - **True** — the outer skin. Bare hull, checkered sun cap, glazed far cap. | |
| 1418 | - **X-ray** — the inner surface projected onto the outside. | |
| 1419 | ||
| 1420 | **The wall is drawn as two meshes now, not one double-sided one.** Its faces show | |
| 1421 | different things: the outer skin changes with the mode and the inner surface is | |
| 1422 | always the land, and a single `DoubleSide` material has no way to say so. The inner | |
| 1423 | mesh is emissive for the same reason the x-ray always was — it is lit by a beam this | |
| 1424 | scene does not model, and unlit it reads as a hole rather than a world. | |
| 1425 | ||
| 1426 | **This is what made the far cap window worth having from outside.** D-045 glazed it | |
| 1427 | for the sake of the interior, where the payoff is the pinwheel. From outside the | |
| 1428 | payoff is different and better: the panes are clear, so in true mode you look in | |
| 1429 | through the glass and the world is really there, mountains and desert and lakes | |
| 1430 | receding 540 km down the tube behind a grid of mullions. It is the only line of | |
| 1431 | sight into Tower that is not a projection, and it exists only because the wall got a | |
| 1432 | window and the hull got two faces in the same week. | |
| 1433 | ||
| 1434 | The dark backing disc the first version of D-045 put behind the glazing is gone. It | |
| 1435 | was there to stop the panes showing the inside of the tube — which turns out to be | |
| 1436 | exactly the thing worth showing. | |
da6bcb0e 2026-09-08 Pavel Safronov | 1437 | |
| 1438 | ## D-048 — The far cap's bullseye is a mirror | |
| 1439 | ||
| 1440 | **Decided by the author**, from four options: mirror it, let it out, absorb it | |
| 1441 | behind fins, or grade the swarm so it never arrives. | |
| 1442 | ||
| 1443 | **The shape of the load is the argument.** §1.3.1 leaves 24% of the beam | |
| 1444 | unscattered after 540 km, and it survives *collimated* — so this was never 13 TW | |
| 1445 | spread over a 7,854 km² cap. It is **1.7 MW/m² in a 7.96 km² spot**, about 1,200 | |
| 1446 | suns, and an absorber there sits at **2,330 K**: survivable for tungsten, and for | |
| 1447 | no structure. But the spot is **a thousandth of the cap's area**, which means the | |
| 1448 | whole problem lives on a 3.2 km bullseye and 99.9% of the far cap needs nothing | |
| 1449 | done to it at all. That is what makes D-045's window and this coexist. | |
| 1450 | ||
| 1451 | **A mirror is the cheap thermal answer, not the expensive one.** At 99.9% it | |
| 1452 | absorbs 1,665 W/m² — about 1.2 Earth solar constants, ordinary sunlight. The thing | |
| 1453 | that would have glowed white is merely warm. The absorber-with-fins option needed | |
| 1454 | the entire cap radiating at 400 K *and* 13 TW moved 50 km sideways from the spot to | |
| 1455 | get there; it was strictly worse than every alternative. | |
| 1456 | ||
| 1457 | **It also recovers the energy, and fixes a number that was already wrong.** The | |
| 1458 | return trip makes the optical column 1,080 km rather than 540, so utilisation is | |
| 1459 | 1 - e^(-2τ) = **94%** against 76%. That matters more than it looks, because canon | |
| 1460 | had a contradiction sitting in it: §1.3 sets full brightness at 54.3 TW ÷ 169,646 | |
| 1461 | km² = **320 W/m²**, which assumes the whole beam reaches the inner surface, while | |
| 1462 | §1.3.1's optical depth says only 76% does — implying **243 W/m²**. Both were | |
| 1463 | printed, and a test pinned 320. With the cavity closed at both ends (the mirror at | |
| 1464 | one, the collector at the other, returning what still escapes) essentially all | |
| 1465 | 54.3 TW is eventually absorbed, and **320 is right as it stands**. No test moved. | |
| 1466 | ||
| 1467 | **What it does not do is make weather.** Aiming light at a region to warm it stays a | |
| 1468 | separate lever, and §1.9.4 already says that is how every climate in Tower is made. | |
| 1469 | The mirror is about not wasting the beam; the Dry Reach is about where the heat is | |
| 1470 | wanted. Keeping them separate means neither constrains the other. | |
| 1471 | ||
| 1472 | **A geometric trap, recorded because I fell into it.** The mirror sits on the axis, | |
| 1473 | so it looks like it must occult the pinwheel's motionless centre — I wrote that into | |
| 1474 | canon before checking. It does not. The pinwheel turns about the direction | |
| 1475 | *parallel* to the axis, and that direction lands on the window at the observer's | |
| 1476 | **own radius**, not at the cap's middle. From the floor the spin axis is horizontal, | |
| 1477 | so the centre of Tower's only moving sky is a point on the **horizon**, dead ahead | |
| 1478 | at eye level — while the mirror is ~10° above it from 270 km. The two coincide only | |
| 1479 | for an observer standing on the axis, and nobody can stand there. | |
e27aaf20 2026-09-08 Pavel Safronov | 1480 | |
| 1481 | ## D-049 — Rain is not drawn any more | |
| 1482 | ||
| 1483 | Rain existed in the interior view for one reason: with the hull sealed it was the | |
| 1484 | only moving thing in the sky, and therefore the only evidence Tower turns. §1.9.2 | |
| 1485 | then undercut it — drag pins a drop's speed, so rain leans a fixed **1.5°** whatever | |
| 1486 | the cloud, against a waterfall's 11°. The readout printed the angle to stop the | |
| 1487 | smallness reading as a bug, which is an admission that the picture was not carrying | |
| 1488 | the claim. | |
| 1489 | ||
| 1490 | D-045 gave the view a whole sky going round once every 7 min 29 s. Against that, a | |
| 1491 | 1.5° lean is not evidence of anything, and 6,000 line segments were being stepped | |
| 1492 | every frame to fail to show it. | |
| 1493 | ||
| 1494 | **The physics stays.** `canon/motion.ts` still computes the lean and | |
| 1495 | `test/motion.test.ts` still checks it — it is a real fact about how anything falls | |
| 1496 | in here, and the waterfall is still the compass. It is only the *rendering* that is | |
| 1497 | gone, along with the toggle, the seeded raindrop field, and the six Three imports | |
| 1498 | that existed for it. The readout line it occupied now prints the pinwheel's period, | |
| 1499 | which is the interval you can actually time against the sky. | |
9c891d2e 2026-09-08 Pavel Safronov | 1500 | |
| 1501 | ## D-050 — The sun cap is a window too, and Tower never sees the sun | |
| 1502 | ||
| 1503 | **§1.4 was wrong about this cap, and the number that shows it was already in canon.** | |
| 1504 | It demanded "heavy glare management" on the grounds that the sun cap faces the sun. | |
| 1505 | It does not face the sun. It faces the **collector**, which is 244 km across at 320 | |
| 1506 | km — **41.8° wide against the sun's 0.53°, seventy-nine times its diameter**. The | |
| 1507 | collector sits between Tower and the sun by construction, so it occults it entirely, | |
| 1508 | and the umbra is 120 km across where Tower's 51 km sits in it. There is no direct | |
| 1509 | sunlight on the sun cap and there never has been. Nothing to manage. | |
| 1510 | ||
| 1511 | So this cap is glazed on the same terms as the far one, and what fills it is the | |
| 1512 | **back of the collector**: a black disc, because its front is a mirror and its back | |
| 1513 | radiates 9.6 TW of waste heat at about 245 K, which is infrared. Around the edge of | |
| 1514 | it, stars. It does not turn with them — the collector flies in formation rather than | |
| 1515 | spinning — so the sun cap window is a still black disc against a wheeling sky. | |
| 1516 | ||
| 1517 | **Nobody in Tower can see the sun.** They already could not see Earth (§1.4, the | |
| 1518 | terminator orbit puts it out the side, which is solid hull). A habitat in orbit | |
| 1519 | around a planet it has never seen, lit by a star it has never seen. | |
| 1520 | ||
| 1521 | The occultation is computed in the shader rather than modelled: the fragment casts | |
| 1522 | its own ray on to the collector's plane and asks whether it lands on the disc. That | |
| 1523 | is the same trick the far cap uses for its stars, and it means the interior does not | |
| 1524 | have to carry a 244 km mesh 320 km outside the world to draw one black circle. | |
| 1525 | ||
| 1526 | The interior's checkered cap material is gone with this. Both caps are windows now, | |
| 1527 | and nothing inside was still using it — the checker survives as the exterior's sun | |
| 1528 | cap and as both windows' mullions. | |
03607270 2026-09-08 Pavel Safronov | 1529 | |
| 1530 | ## D-051 — The range rings the world | |
| 1531 | ||
| 1532 | The mountains were "a 140 km arc, not a ring" — four named domes covering about a | |
| 1533 | third of the circumference, with the other two thirds simply unauthored. Asked to | |
| 1534 | close it, and closing it is better than the arc was: a ring of peaks is a *watershed* | |
| 1535 | that divides the whole world rather than a wall you can walk around the end of. | |
| 1536 | ||
| 1537 | **The ridge op grows optional ridged noise**, which is what turns a ridge into a | |
| 1538 | range. Without it a ring band is a smooth swell all the way round — right for the | |
| 1539 | three low walls across the waist, wrong for peaks. | |
| 1540 | ||
| 1541 | **It blends with `max` rather than adding**, and that is not a detail. Added, The | |
| 1542 | Girdle's 2,900 m would stack onto Snowcrown's 4,300 and push the massif to 6,900 — | |
| 1543 | past the raster's 4,400 m ceiling, where it would clip flat. Taking the greater of | |
| 1544 | the two leaves the named summits as the high points of their own range, which is | |
| 1545 | what they are: Snowcrown is not a separate mountain from The Girdle, it is the tall | |
| 1546 | part of it. | |
| 1547 | ||
| 1548 | Peaks reach about 3,900 m, so they cross the 3,000 m snowline and the ring reads as | |
| 1549 | snow the whole way round. The rivers still cut it, because carve runs after relief. | |
9a621732 2026-09-08 Pavel Safronov | 1550 | |
| 1551 | ## D-052 — A fourth noise octave, and colour that mottles instead of dimming | |
| 1552 | ||
| 1553 | **Height.** The base had three noise layers — 78 km, 12 km, 3.4 km — and a pixel is | |
| 1554 | 264 m. Between the finest layer and the raster's ~530 m Nyquist limit there was an | |
| 1555 | octave and a half of nothing, so the ground read as smooth swells with no grain on | |
| 1556 | them at exactly the scale you look at it from inside. A fourth layer at 1.5 km and | |
| 1557 | ±26 m fills that band. It goes in the *base*, deliberately: carve and flood run | |
| 1558 | afterwards, so rivers still cut through it and lakes keep flat surfaces rather than | |
| 1559 | inheriting a rippled floor. | |
| 1560 | ||
| 1561 | **Colour.** There was already per-pixel variation, and it was the wrong kind — one | |
| 1562 | noise field scaling all three channels together, which dims and brightens without | |
| 1563 | ever changing the colour. A second, finer field now **mottles**: it pushes the | |
| 1564 | channels apart, green moving most and blue least, so ground reads as patchy | |
| 1565 | vegetation against bare soil rather than as one paint turned up and down. Keeping | |
| 1566 | blue nearly fixed is what stops the desert drifting toward the same green as the | |
| 1567 | forest. | |
| 1568 | ||
| 1569 | **It exposed a live bug, and the fix matters more than the noise does.** The extra | |
| 1570 | relief pushed the terrain to **-521 m** against a declared floor of **-520 m**, so | |
| 1571 | every pixel down there was being silently flattened. The generator printed a warning | |
| 1572 | and carried on; nothing else noticed. That is D-024's failure mode exactly — a | |
| 1573 | clipped raster displaces terrain rather than breaking it, so nothing downstream | |
| 1574 | complains. | |
| 1575 | ||
| 1576 | Two changes: the floor moves to **-560 m**, and `npm run terrain` now **exits | |
| 1577 | non-zero** when any pixel is clamped instead of printing a line into a log nobody | |
| 1578 | reads. The unit test could never have caught this — it compares the declaration | |
| 1579 | against extremes written down when they were last true, not against the terrain that | |
| 1580 | just came out — and it now says so where it sits. | |
2bb3e0de 2026-09-08 Pavel Safronov | 1581 | |
| 1582 | ## D-053 — The day is a front, not a dimmer | |
| 1583 | ||
| 1584 | The obvious day-night cycle is a global fade between the light settings. It would | |
| 1585 | have worked and it would have thrown away the most interesting thing about how this | |
| 1586 | world is lit. | |
| 1587 | ||
| 1588 | **What the physics allows.** D-046 already established that the swarm cannot make | |
| 1589 | night, because §1.3 gives the beam's glow to the beam and the air rather than to the | |
| 1590 | diffusers — a lit beam lights the world whatever the mirrorlets are doing. So beam | |
| 1591 | power has to ramp, and when it does, the sky brightens *everywhere at once*. There | |
| 1592 | is no way to have one end of the axis get its morning before the other. | |
| 1593 | ||
| 1594 | **What the swarm can still do is choose where the light lands.** The diffusers hand | |
| 1595 | beam light to the ground, and they are individually flown with absurd control | |
| 1596 | authority (§1.3.1). So the ground can be lit progressively even though the sky | |
| 1597 | cannot, and the natural direction is the one the beam already runs: sun cap to far | |
| 1598 | cap. | |
| 1599 | ||
| 1600 | **A Tower dawn is therefore a fully lit sky over dark ground**, with a line of | |
| 1601 | daylight travelling toward you at 225 m/s. That is not a compromise between the two | |
| 1602 | options, it is what falls out of taking both constraints seriously — and it is a | |
| 1603 | better image than either. | |
| 1604 | ||
| 1605 | **It gives Tower longitude.** §1.9.4 says the world has no latitude: every point is | |
| 1606 | equidistant from the light, which is why nothing about it varies. The cycle adds the | |
| 1607 | axis back as 40 minutes of time difference between the caps. Dusk runs the same | |
| 1608 | direction, so the sun-cap end has the shorter day by a hair. | |
| 1609 | ||
| 1610 | **The panel gets a time slider before it gets a clock.** Running the cycle is the | |
| 1611 | feature, but a 24-hour day at any watchable rate means waiting for dawn, and dawn | |
| 1612 | lasts 40 world-minutes out of 1,440. Being able to scrub straight to 06:17 is what | |
| 1613 | makes the front usable at all — and the front is a pure function of the phase, so | |
| 1614 | scrubbing and ticking are the same operation. | |
| 1615 | ||
| 1616 | The readout names the distance to the line rather than just the hour, because the | |
| 1617 | distance is the part you can act on: 27 km sunward is about two minutes' wait. | |
49b0d64d 2026-09-08 Pavel Safronov | 1618 | |
| 1619 | ## D-054 — The collector gets a lit rim | |
| 1620 | ||
| 1621 | Drawing the collector as pure black was right about its face and wrong about the | |
| 1622 | object. From inside the sun cap window at night you could not see it at all — only | |
| 1623 | that a piece of the sky was missing, which reads as a rendering hole rather than as | |
| 1624 | a 244 km structure. | |
| 1625 | ||
| 1626 | **The physics gives the rim for free.** The collector is backlit by definition: its | |
| 1627 | mirror face is pointed at the sun and away from Tower. Anything holding 46,938 km² | |
| 1628 | flat has structure standing proud of the mirror plane at the edge, and at the edge | |
| 1629 | the sun strikes that structure side-on. Add the scatter every real mirror throws at | |
| 1630 | grazing incidence and the boundary is a thin sun-coloured ring. That is simply how | |
| 1631 | backlit things look, and it is the one part of the collector that can be lit. | |
| 1632 | ||
| 1633 | **What it is not is sunlight past the edge**, and the distinction matters because the | |
| 1634 | first is a property of the object and the second would break canon. Tower sits wholly | |
| 1635 | inside the umbra — §1.3.0 — so the sun's disc is occulted from every point in the | |
| 1636 | world. Nobody sees a sliver of star; they see lit metal. | |
| 1637 | ||
| 1638 | Rendered as two exponentials: a tight core at the declared 4 km of edge structure, | |
| 1639 | and a wide dim skirt for the scatter around it. One alone reads as a drawn outline | |
| 1640 | rather than as light. The first attempt was three times too bright and its skirt | |
| 1641 | seven times too wide, which washed the whole disc gold instead of tracing it. | |
| 1642 | ||
| 1643 | **It does not scale with the beam.** The sun does not dim when Tower banks its own | |
| 1644 | lamp, so the rim is outside `uExposure`, like the stars. It does fade as the eye | |
| 1645 | adapts, but from a much higher floor than a star's — this is sunlit structure, and | |
| 1646 | it stays faintly visible at noon where a star cannot. | |
17ccee99 2026-09-08 Pavel Safronov | 1647 | |
| 1648 | ## D-055 — The clock is on the collector, and it is read as an angle | |
| 1649 | ||
| 1650 | Two lit lines from the collector's hub: one at the full radius, fixed normal to the | |
| 1651 | plane of the solar system, and one stopping short of the rim at an angle θ from it, | |
| 1652 | θ running 0 to 2π over one day. | |
| 1653 | ||
| 1654 | **Why there.** Everything inside Tower co-rotates with the hull, so nothing inside | |
| 1655 | can carry a fixed reference. Everything outside appears to wheel once every 7 min | |
| 1656 | 29 s. The collector is the exception twice over: it flies in formation rather than | |
| 1657 | spinning, and it never moves in the sky — it is always dead ahead through the sun | |
| 1658 | cap window, 41.8° wide. It is the only surface in the world worth writing on. | |
| 1659 | ||
| 1660 | **Why two lines rather than a hand and a dial**, which is the part that turns out to | |
| 1661 | be forced rather than decorative. From inside, the collector's whole face appears to | |
| 1662 | rotate at Tower's spin, because the observer is turning and it is not. A single mark | |
| 1663 | against a fixed background would be useless — the background is not fixed. But the | |
| 1664 | *angle between two marks* is invariant under that rotation, so a pair of lines | |
| 1665 | survives what neither line does alone. It is a clock designed for people who cannot | |
| 1666 | stop the dial turning, and the design falls straight out of the constraint. | |
| 1667 | ||
| 1668 | The marks are read in the collector's own frame, so the shader adds `uSpin` to the | |
| 1669 | observed azimuth exactly as the star field does. Both wheel together, which is | |
| 1670 | correct: they are both outside. | |
| 1671 | ||
| 1672 | **Lit deliberately**, not found. The collector's back is unlit and 245 K; nothing on | |
| 1673 | it would be visible by accident. It is an instrument, so Tower runs it — about | |
| 1674 | 0.5 GW to read against the haze, which is one part in a hundred thousand of the | |
| 1675 | beam, less than the swarm spends holding its own station. | |
| 1676 | ||
| 1677 | **You cannot always read it.** From 270 km the sun cap is 96% airlight by day, so | |
| 1678 | the clock is washed out exactly as the far cap's stars are. It is legible at night, | |
| 1679 | or from near the sun cap. That is the same fact §1.5 has been making all along and | |
| 1680 | not a defect in the instrument. | |
| 1681 | ||
| 1682 | **Not yet in the exterior view**, which has no time of day at all — its collector is | |
| 1683 | a flat disc with no clock on it. Giving it one means giving that view the day model | |
| 1684 | first, so the two views disagree about the same object until then. | |
1fcef8e6 2026-09-08 Pavel Safronov | 1685 | |
| 1686 | ## D-056 — Flying, and the panel clock that was not being told | |
| 1687 | ||
| 1688 | **Two fixes and a mode.** | |
| 1689 | ||
| 1690 | The panel's Time slider did not move while the cycle ran. The readout was reading | |
| 1691 | `dayPhase` off the view every frame and the slider was sitting wherever it had last | |
| 1692 | been dragged, so the panel had two clocks in it disagreeing. It now follows, and is | |
| 1693 | left alone while it has focus so it cannot fight a drag in progress. | |
| 1694 | ||
| 1695 | **Flying** replaces the standing observer with a free point inside the shell. `at` | |
| 1696 | stops being the position and starts being a readout of it. | |
| 1697 | ||
| 1698 | - **Up is still toward the axis**, at every radius, because that is what up *means* | |
| 1699 | in here — the direction gravity does not point. On the axis itself the radial | |
| 1700 | direction is undefined; the fallback keeps the horizon from tumbling, and you are | |
| 1701 | weightless there anyway (§1.2), so there is no physical up to get wrong. | |
| 1702 | - **No throttle and no brake.** The mode is *flying*; the only control is where you | |
| 1703 | point, which the existing drag handlers already do. A speed slider sets the cruise | |
| 1704 | and that is all. | |
| 1705 | - **Held inside the shell.** The caps stop you at both ends and the terrain stops | |
| 1706 | you underneath — sampled from the height raster rather than the datum, so the | |
| 1707 | mountains are solid rather than something you sink through. | |
| 1708 | - **Selecting a place arrives at altitude** rather than landing you on it, since | |
| 1709 | landing would silently leave the mode you are in. | |
| 1710 | ||
| 1711 | The readout changes with the mode rather than lying: eye height means nothing once | |
| 1712 | you are off the ground, so it gives way to altitude over the terrain and height | |
| 1713 | above the datum — and the gravity beside it is computed at the real radius, which is | |
| 1714 | the interesting part. **0.91 g at 4.5 km up**, because g falls linearly to zero at | |
| 1715 | the axis (§1.2). Flight gets easier the higher you go, which is true here and true | |
| 1716 | nowhere on Earth. The beam's apparent width now measures from the flier to the axis | |
| 1717 | too, so it widens as you climb. | |
| 1718 | ||
| 1719 | **A camera that never stops cannot be photographed twice the same**, which the | |
| 1720 | screenshot tour cares about. The shot fixes where the flight *starts* rather than | |
| 1721 | trying to stop it: take off from a known 4 km, cruise at the slowest 150 m/s, and | |
| 1722 | keep the shutter short. Drift between runs comes out below the measurement — 0.00%. | |
41516065 2026-09-08 Pavel Safronov | 1723 | |
| 1724 | ## D-057 — Flight is an autopilot; input turns the head, not the aircraft | |
| 1725 | ||
| 1726 | D-056 flew the camera along the look direction: dragging steered. That is the | |
| 1727 | obvious way to build it and the wrong way to use it. Hand-flying makes you navigate | |
| 1728 | a world you came to *look* at — you end up watching your altitude and your heading | |
| 1729 | instead of the view, and every time something is worth turning to see, turning to | |
| 1730 | see it takes you somewhere else. | |
| 1731 | ||
| 1732 | So the two are separated. The autopilot flies a course; the drag handlers only turn | |
| 1733 | your head. Takeoff aims you along the course so you start out facing where you are | |
| 1734 | going, and after that the view is yours. | |
| 1735 | ||
| 1736 | **The course is a helix, 18° off the axis.** Straight down the axis would be simpler | |
| 1737 | and would show less: rolling around the world while crossing it is the only way to | |
| 1738 | see that the sky is ground. It reflects off the caps rather than stopping — flip the | |
| 1739 | along-axis component, keep the way it rolls — so the flight is genuinely permanent. | |
| 1740 | ||
| 1741 | **It is flown in the unrolled surface (θ, z), not in 3D**, and that is what makes it | |
| 1742 | safe rather than carefully guarded. The constraints live in those coordinates: the | |
| 1743 | caps are limits on z and θ simply wraps. There is no collision test anywhere in the | |
| 1744 | mode, because there is nothing it can hit. | |
| 1745 | ||
| 1746 | **Terrain-following is asymmetric on purpose**: climb at once, sink slowly. An | |
| 1747 | autopilot that lags on the way up flies into the mountain; one that drops into every | |
| 1748 | valley as fast as it climbs reads as a fault rather than as flight. | |
| 1749 | ||
| 1750 | **The eye-height slider becomes the altitude**, rather than being disabled as D-056 | |
| 1751 | had it. It is the same control over the same 1.7 m to 25 km, it is the only thing | |
| 1752 | left to fly with once the course is automatic, and disabling it would have left the | |
| 1753 | mode with no controls at all. The label switches between "Eye height" and "Altitude" | |
| 1754 | so it is not quietly lying about which it is. | |
| 1755 | ||
| 1756 | This also simplified the view rather than complicating it: `basis()` had grown a | |
| 1757 | separate branch for flight with its own radial fallback for the axis, and standing | |
| 1758 | and flying turn out to differ in exactly one term — what you are held above. The | |
| 1759 | free 3D position is gone, and with it the fallback and the ceiling clamp. |