fuzzyworld

tower / files / decisions.md

decisions.md

203.8 KB · history · blame

Tower — Decisions

Standing record of choices made and why, so they can be revisited deliberately rather than rediscovered. Newest sections append at the end.


D-001 — Cylinder geometry: r = 50 km, L = 540 km

Decided. Radius 50 km, length 540 km, giving 169,646 km² of inner surface.

Considered three shapes, all matching Wisconsin's area at 1 g:

radius length spin period character
Long & narrow 8 km 3,375 km 3.0 min ceiling, not sky; absurd aspect ratio
Balanced 50 km 540 km 7.5 min far side visible overhead through haze
Wide & short 150 km 180 km 13.0 min a drum; far cap looms as a wall

Why balanced. It is the only one where the sky reads as sky. At 8 km the roof is 16 km up and oppressive; at 150 km the far side is 300 km away and lost entirely. At 50 km the opposite land is 100 km overhead — far enough to be pale and dreamlike, near enough to be unmistakably land above you. That image is the point of the whole project, and this geometry is what produces it.

The 7.5-minute spin also sits in a good window: fast enough that star motion is obvious in a few seconds of watching, slow enough not to look like a fairground ride.


D-002 — Slats sized so land area equals Wisconsin's land area

Decided. 8 slats, 6.80 km wide, leaving 140,268 km² of land.

The brief specified a Wisconsin-sized inner surface. Wisconsin has two areas — total 169,635 km² and land 140,268 km². Sizing the slats to consume exactly the difference makes both true at once: Tower's total inner surface is Wisconsin's total area, and Tower's habitable land is Wisconsin's land area. The slats are its lakes.

This is a coincidence discovered while computing, not a constraint imposed. It costs nothing and it is too good to pass up.

Superseded by D-017: there are no slats, so Tower's land is its entire inner surface, 169,646 km² — Wisconsin's total area. The second coincidence recorded below no longer applies.

Slat count is left as a tunable constant. The runtime control is disabled and hidden as of D-033; the derivation below is unchanged and still tested. The width follows from the count:

slats each land strips between
6 9.07 km 43.3 km
8 6.80 km 32.5 km
12 4.53 km 21.7 km
24 2.27 km 10.8 km

8 chosen because 32.5 km strips read as regions — a day's travel across, room for real geography. At 24 the world becomes narrow ribbons where every point is within 5 km of a wall of stars, which is striking but claustrophobic. Revisit after seeing the map view; this is cheap to change.

"Revisit after seeing the map view" is now literal — D-014 makes the count a runtime control from 0 to 16, and 8 is the default rather than a fixed value.


D-003 — Widen the beam to 10 km; the physics now works unbroken

Decided. The beam is 10 km in circumference, carrying 54.3 TW at 5,000× concentration. No physical law is bent. This supersedes an earlier version of D-003 that accepted an étendue violation in order to keep a 1 km beam.

The mistake in the first pass. I treated the 1 km beam as fixed and concluded that Earth-like brightness was impossible without an active, physics-breaking waveguide. That was wrong, because it conflated two independent parameters:

Holding the beam at 1 km forced 313,000× and broke the second law. Widening the beam relaxes the concentration requirement quadratically and costs nothing else.

Where the number came from. Targeting 54.3 TW — Earth clear-sky noon at local midday, see design §1.3 — against candidate concentrations:

concentration beam circumference needed status
46,223× (étendue limit) 3.29 km thermodynamically impossible to exceed
10,000× 7.08 km beyond current practice
5,000× 10.01 km Odeillo solar furnace reaches ~10 MW/m² today
1,000× 22.39 km routine, but a conspicuously fat beam

5,000× chosen. It lands the beam on a round 10 km, it is demonstrated terrestrial technology rather than an extrapolation, and it sits at 11 % of the étendue limit — 9.2× of headroom in reserve. Diffuser mirrors at 99.9 % reflectivity reach thermal equilibrium at 316 °C in that beam, an ordinary radiator problem rather than an exotic one.

Cost to the imagery: none. At 1,592 m radius seen from 50 km below, the beam is 3.65° wide — about seven sun-widths. It still reads as a luminous rod running the length of the sky, which is the image the 1 km figure was protecting.

The unlocked consequence. Because Tower now has surplus capability rather than a deficit, brightness becomes a setting rather than a fixed value. The diffusers are software-controlled; illumination can run anywhere from 30 W/m² permanent dusk to 320 W/m² clear-sky noon. The "embrace the dimness" option weighed and rejected in the first pass is no longer an alternative to this decision — it is a position on its slider. The app should expose that control.

D-004 — Terminator orbit, actively precessed

Decided. Orbital plane held normal to the Earth–Sun line, precessing 360°/year.

The brief requires that one cap face the sun permanently. Any orbit whose plane contains the sun line puts Tower through Earth's shadow once per revolution, which would interrupt the beam. A terminator orbit never enters shadow.

The cost is that the plane must be reoriented ~1°/day against the gyroscopic stiffness of a spinning 540 km cylinder. This is enormous and continuous. Canon adopts it as a feature: Tower's existence is a permanent act of maintenance, not a stable configuration. That is more interesting than a free lunch.


D-005 — Vanilla TypeScript, no UI framework

Decided. Vite + TypeScript + Three.js, no React/Vue/Svelte.

The app is three canvases, a picker and a control panel, sharing one small state object. A framework would add build weight and a rendering model that fights Three.js's own loop, in exchange for conveniences this UI does not need. Revisit only if the control panel grows genuinely complex.

Canvas 2D rather than Three.js for the map: it is a flat unrolled rectangle with no projection distortion, and 2D canvas draws that natively.


D-006 — Interior fidelity: real geometry, simple ground

Decided. The shell, curvature, haze, beam, diffusers, slats and stars are all genuinely modelled and correct. The ground is procedural terrain and texture only — no scenery meshes, no settlements, no vegetation.

The illusion of standing inside Tower comes almost entirely from the sky: land curving up into blue, the lit shaft overhead, stars wheeling past every 7.5 minutes. Detailed ground scenery adds cost proportional to realism and contributes little to that illusion. Deferred, not abandoned.


D-007 — All canon derived from four constants

Decided. canon/constants.ts exposes radius, length, slat count and target land area as free parameters. Everything else — spin period, rim speed, Coriolis parameter, slat width, angular spacing, axis pressure, beam power — is computed.

The alternative, transcribing the figures in design.md as literals, would drift the moment any parameter is revisited (and D-002 and D-003 both flag likely revisits). Derivation makes the whole app answer coherently to a changed radius.


D-008 — Diffusers are a 1.2 cm swarm; the beam glows from air, not from them

Amended by D-074: individually the mirrorlets stay invisible, but by day the swarm gathers into one shoal on purpose, and that shoal is Tower's sun.

Decided. 78 billion 1.2 cm mirrorlets, and the beam's visible glow is atmospheric scattering that does not depend on them.

The question. Would a modest number of large diffusers read as a string of separate suns rather than a continuous glowing rod?

Yes. At 50 km, naked-eye resolution is 14.5 m; anything larger shows a visible disc. But shrinking below 14.5 m is not sufficient, which was the non-obvious part. A flat mirror preserves radiance, so an aimed mirrorlet shows the sun's own surface brightness over a small angle — unresolved but blinding, exactly as stars are unresolved and plainly visible. Magnitudes: 1 m → −13.6 (brighter than a full moon), 10 cm → −8.6, 1.2 cm → −4.0, which finally sits under the daylight visibility threshold. The binding constraint is flux, not angular size, and it is two orders of magnitude tighter than the resolution limit implies.

The better answer, found while checking. The beam glows regardless. Rayleigh scattering of a 6.8 MW/m² beam through 3.18 km of 5.2 kPa axis air gives 779 W/m²/sr — 26× blue sky, 25,700× below the solar disc. The continuous-glow imagery is therefore a robust property of beam plus atmosphere, not a consequence of getting the diffuser design right. Canon leans on this; the swarm sizing is then only about keeping individual glints out of the sky.

Consequences taken. 11.3 km² of mirror at 1.2 cm is 78 billion units massing 158 tonnes, spaced 3.8 m apart. Optical depth 1.41 leaves 24 % of the light reaching the far cap, which satisfies the brief's "shines to the far end" without stipulating it. The passive drift-and-recirculate mechanism first recorded here is superseded by D-009 — it does not work.

Story consequence. 78 billion independently aimable mirrors make illumination a render target rather than a schedule. Sunsets are compositing, not optics.


D-009 — Tower flies every foil itself, because nothing else works

Decided. All 78 billion foils are under continuous individual control by Tower. There is no recirculation machinery at either cap. This supersedes the passive drift mechanism in D-008.

Origin. Proposed by the author as characterisation — Tower is an intelligent being, so it controls each foil directly and moves the swarm partly for its own enjoyment. Checking the physics turned this from a flourish into a requirement.

A passive swarm is impossible. D-008 assumed the axis was effectively weightless. It is not. At the beam's edge, 1,592 m out, g = ω²s = 0.312 m/s². A 2 mg foil sinks outward at 0.26 m/s and falls clear of the beam in 1.7 hours. A drifting swarm dissolves in an afternoon. Continuous active flight is the only configuration that holds together at all.

They can easily afford it. Each foil intercepts 979 W; harvesting 0.1 % yields ~1 W on a 2 mg body — 489,600 W/kg, ~50× a jet engine. Flying up-beam at 1.5 m/s costs 15 µW against 14,400 µW harvestable from ambient world-light: a 1000× margin, without touching the beam. So the foils fly home unaided, no structure is required at the far cap, and the exterior render is simpler than D-008 implied.

The interesting part is the slack. With 1000× of margin, efficiency is not a constraint on Tower's behaviour. It can move the swarm in spirals, shoals and patterns that cost energy for no reason except preference, and nothing about the world degrades. Play is not a luxury Tower affords despite the physics — it is budget the physics hands over for free. The weather is therefore not a system Tower operates but something closer to body language.

Falling out of this: aiming is human-scale. A 1.2 cm mirror is diffraction-limited to 4.6×10⁻⁵ rad, giving a 2.3 m spot at 50 km. Tower can place a glint on one person that a bystander five metres away cannot see. Offered to the author as a story mechanic; canon records the optics either way.

And: mirrorfall. A failed foil sinks out of the beam, takes ~2 days to descend, and lands at 0.48 m/s — snowflake speed. A permanent slow glitter falls out of the sky.


D-010 — The app is a companion to a novel, not a tech demo

Decided. Tower is the setting of a novel the author is writing. The app serves that purpose first.

Two consequences, recorded because they change priorities rather than mechanics:

Not yet decided, and deliberately left open: whether the app should ever surface narrative content — place names, events, characters — or remain a pure physical model that the prose refers to from outside. That is the author's call and does not need answering to proceed.


D-011 — Writer mode: the app is an instrument, and the world file is git

Decided. Two modes over one committed world.json. Readers get a finished map; the author gets editing plus a set of measuring instruments. No backend.

Superseded in part by D-072: there is one mode now, and the world is edited in content/world.json rather than in the browser. "The world file is git" and "no backend" stand.

Origin. The author intends to use the app to write — placing things and asking questions like "can the far face be seen from the midpoint?", "where does a central embassy go?", "can a lake overhang?". That makes measurement, not rendering, the core feature. D-010 said the app serves the novel; this is what that costs.

Why those questions justify the build. Two of the three had non-obvious answers that only fell out of computation, and both became canon (§1.8): the far cap is invisible from the midpoint at 4 % transmission, and — unexpectedly — the far side of the world overhead is clearer than the ground at the same distance (78 % vs 31 % at 100 km), because a sightline across the interior rises through thin axis air. An author cannot guess these. The instrument earns its place immediately.

Static, file-based, no backend. GitLab Pages serves static files, so: edit in browser → export → commit → publish. The world lives in git beside the prose, diffable and versioned, with no service to run or pay for. Drafts sit in localStorage between exports.

Coordinates in the file are canon (θ, z), never map pixels, so the world survives any change to how the map is drawn. Consistent with D-007.

Correction (D-068): as built, writer mode has always been the default and ?reader=1 turns it off; ?writer=1 does nothing. The reasoning below holds either way.

Writer mode is a UI mode, not a security boundary. ?writer=1 is guessable and that is fine — an editor with no persistence and no backend can only alter the visitor's own copy. Recorded explicitly so it is never mistaken for access control and never load-bearing for privacy.

The real privacy risk is the repo, not the flag. Working notes must not ship to readers, but anything committed to a public repository is public whatever a "public": false field says. Hence two export files: world.json (public entries, committed) and world.private.json (everything, gitignored). Whether private notes should be version-controlled anyway — private repo, or a second one — is left to the author and does not block anything.

Sequenced early. The sightline probe lands in phase 6 but should be built first within it: it is useful the day it exists, and §1.8 is evidence the author is already asking questions only it can answer.


D-012 — Ids follow the name until pinned by hand

Decided. A place's id is re-derived from its name while it still matches it, and freezes permanently once the author edits the id field.

Retired by D-072 along with the editor. Ids are written by hand in the file now, and the reason below — a permalink is worthless if the id churns — still governs changing one.

The tension. design.md §3.2 makes ids permalink targets, which argues for never changing them. But places are created from a placeholder name ("New city"), so a never-changing id produces a world file of new-city, new-city-2, new-city-7 — unreadable in git diffs and unusable for hand-editing, both of which D-011 counts as reasons for the file-based design in the first place.

Resolution. During world-building, readability wins; after publication, stability wins; and the author is the only one who knows which phase a place is in. So the id tracks the name automatically until they say otherwise, and the id field is shown in the editor so saying otherwise is one edit. isAutoId() in src/world/schema.ts is the test, and is unit-tested directly.

Found by running it. The first implementation derived the id once at creation and never updated it, which the browser check caught immediately — every exported place was called new-city. Two further bugs surfaced the same way: renaming blanked the editor (the store re-rendered before the selection moved to the new id), and clicking an existing marker while a creation tool was active dragged the old place instead of creating a new one. None were visible in the unit tests.


D-013 — The map is bounded, and cut at a slat centre

Decided. The map shows the inner surface once, as a bordered rectangle, rather than tiling vertically. The cut falls at the centre of the last slat.

Why not tile. The circumference wraps, so tiling is the honest rendering and it makes places near the seam continuous. But it also makes the world look boundless: with no edge, the author cannot see the extent of what they are building, and repeated strip labels read as more strips rather than as repeats. For a map — a thing whose job is to show where things are relative to the whole — legible extent beats a hidden seam.

Why cut there. Any bounded map needs an edge, and the edge destroys whatever it falls on. Land strip 0 is centred on θ = 0, so the obvious cut would slice a whole strip in half across the top and bottom margins. Cutting at a slat centre instead leaves every land strip whole and splits only a band of stars, which carries no geography. This is exactly why Earth's maps are cut at the antimeridian. With zero slats there is nothing to protect, so the cut returns to θ = 0.

Consequences taken. Panning is clamped so the surface cannot be lost off-screen — without tiling there is nothing to pan back into. A sightline whose short route crosses the edge is drawn as two segments, entering and leaving opposite edges, so the drawn path always matches the measured distance rather than contradicting it.


D-014 — Slat count is a runtime control, 0 to 16

Decided. SLAT_COUNT stops being a compile-time constant. The count lives in src/canon/slats.ts, adjustable from 0 to 16, persisted per browser, with every slat-derived figure computed from it.

Why. D-002 already called this "the most tunable number in canon" and said to revisit it after seeing the map. Making it a slider makes that revisiting real rather than a code edit, and the author is choosing the shape of a world rather than configuring software.

The invariant that makes it safe. D-002 fixes the total slat width from the land-area target, and that total does not depend on how many slats it is divided into. So land area stays exactly Wisconsin's 140,268 km² at every count from 1 to 16 — the coincidence survives the whole range, and only the per-slat width moves (18.1 km at 3 slats, 3.40 km at 16).

Zero is a genuinely different world, and the app says so. With no slats the land becomes the entire inner surface, 169,646 km², and nobody in Tower can ever see a star. That breaks the D-002 coincidence, so the control states it plainly instead of silently reporting a changed number. Rounding that away would have been the easy option and the wrong one — it is exactly the kind of quiet inconsistency the documentation gate exists to catch.

Cost. Slat geometry moved from constants to functions, which rippled through geometry.ts, the map, the instruments and the tests. Worth it: D-007's principle was always that canon is derived rather than transcribed, and this is that principle extended one parameter further.


D-015 — Hydrology: shaped terrain, no sideways Coriolis, climate from the slats

Decided. Recorded as canon in design.md §1.9, and it amends §1.4.

The provable part. The floor's tangent plane is spanned by θ̂ and ẑ and the spin axis is ẑ, so surface flow gets ẑ × ẑ = 0 axially and a purely radial ẑ × θ̂ = −ŝ circumferentially. The Coriolis force has no component inside the surface at any heading. Tower's rivers do not curve, and Earth's entire rotational repertoire — hemispheres, Baer's law, gyres, cyclones — is absent. This is not a simplification; it falls out of the geometry.

Where the force goes instead. Into vertical motion. Waterfalls lean retrograde as h^1.5 (133 m over a 1 km drop, 377 m over 2 km), and rain hangs at a permanent 1.47° slant that never varies. Rising things lean the opposite way. A world where every falling thing drifts one way and every rising thing the other is a strong, cheap, always-visible signature.

The absence that matters more. Potential depends only on radius, so the floor is one equipotential and a perfect Tower has no rivers at all. Every river exists because the floor was shaped, there is no tectonic uplift, and erosion is therefore one-way — ~30 Myr to erase a 3 km range. Tower's hydrology is maintenance, which is D-004's theme arriving from a new direction.

An inconsistency found and fixed, not smoothed over. §1.4 specified a glass/diamond composite slat fill. Diamond conducts ~2,000 W/m·K; such a slat would radiate 385 W/m² to space, exceeding Tower's entire 320 W/m² mean insolation and freezing the world. Canon now specifies low-conductivity glass, which loses ~28 W/m² through 5 m. Raised explicitly per the documentation gate rather than quietly retconned.

The gift in that loss. Tower has no latitude — every point is equidistant from the light, so there is no natural climate gradient whatsoever, which is a real worldbuilding hole. The leaking slats fill it: eight cool belts running the full 540 km, parallel to the axis. Tower's climate bands run lengthwise, not around, and the slat count sets the climate as well as the geography — further support for D-014 making it a control.


D-016 — Slat orientation: axial or rings — OPEN

Not decided. Canon keeps axial strips for now. Rings are a live alternative and this records what the choice costs, so it can be made deliberately.

Why it matters more than it looks. Slat layout turns out to set three things at once — the shape of Tower's countries, its entire climate map, and what its people can see of the universe. It is not a detail of the hull.

Geometry. At equal total area: rings are 11.69 km wide with 55.8 km land bands. Each land region is then 314 × 55.8 km rather than 32.5 × 540 km — the same area at aspect 5.6:1 instead of 16.6:1, a country instead of a corridor. Ring bands are also closed loops: you can walk 314 km around your whole region and come home without crossing a window. Axial strips are ribbons with two dead ends at the caps.

Climate. Axial strips put the cool belts lengthwise, so a 540 km river runs its whole length through identical country. Rings put them crosswise, giving Tower a genuine latitude analogue — a long river crosses all eight bands like the Nile.

The sky, which is the deep one. The spin axis is the celestial pole, so stars move along the celestial equator. Orientation decides whether a window lies across that motion or along it:

Earth follows the same rule: a five-second flicker under axial slats, a slow bright traveller under rings. The sun is at the celestial pole and invisible through both.

The cost of rings. Hoop stress dominates the structure — ρv² is 785 MPa in a composite shell plus ~500 MPa of air pressure — and it runs along θ. Axial slats sit across that path, so ribs bridge the gap and the glass carries only local pressure. A ring runs along the path and is continuous in θ, so the window itself must carry ~1.2 GPa. Glass cannot; it would need hoop cables through every pane, putting bars across every view forever. Characterful, but a real price axial slats do not pay.

Consequence for the app if rings are chosen. D-013 cuts the map at a slat centre because there is an empty θ to cut at. With rings there is not — every θ is land — so the map seam would have to slice through inhabited country, or the map would need reorienting. Worth knowing before the choice is made rather than after.


D-017 — The hull is sealed; the only views out are at the caps

Decided by the author. No slats, in any orientation. The cylinder wall is opaque and unbroken for its full 540 km. Supersedes D-002's slat sizing, D-014's default, and closes D-016.

What it costs. The wheeling star field was canon's answer to "what is most alien about standing inside Tower" (§1.2) and the interior view's headline feature. It is gone. Tower also loses its only passive climate driver and its only natural source of geographic variation.

Partly recovered by D-045, and worth reading as a correction to the paragraph above rather than a reversal of the decision. The wall is still opaque and unbroken for its full 540 km — that is what D-017 decided and it stands. What came back is the pinwheel, through the far cap window, where §1.4 had always said it would be: one sky, at one end of the world, at night. It is no longer everywhere, which is the whole difference between the slats and a window.

What it buys, which is more.

The consequence the author accepted explicitly. With no latitude and no windows, nothing breaks the thermal uniformity, so there is no passive weather whatsoever. Every cloud, every rainfall and every jet stream is Tower deliberately aiming light. This is D-009 reaching its end point: the swarm was already Tower's body language, and is now its lungs. There is no autonomic layer beneath. If Tower's attention lapses, the rain stops.


D-018 — Rivers are a closed, pumped circulation, and Tower can easily afford it

Decided. Tower's rivers follow the architecture the author proposed: reservoir → shaped descent → lake → lift back to the reservoir. Source and sink are one system.

Why it is the only option. The floor is an equipotential (D-015) so there is no natural slope, and the sealed hull (D-017) leaves no natural water cycle. Water in Tower moves only because something moves it.

Why it is not a compromise. Costed against the 54.3 TW budget:

Earth-like rainfall, 1,000 mm/yr 12.2 TW 22.4 % — Earth spends the same share
Recirculating all runoff (1,792 m³/s, a Rhine) against 500 m 8.8 GW 0.016 %

Pumping the entire hydrology of the world is a rounding error — 6,000× headroom. And full Earth-intensity weather is affordable at the same fraction of budget Earth itself spends. Tower does not have to choose.

Canon uses both, because they give different rivers. A pumped river begins at a machine — walk to its source and find a wall of water leaving a structure. A rain-fed river begins at a cloud Tower made over the highlands, and looks entirely natural while being entirely deliberate. Made weather runs the world's general hydrology; pumps run the watercourses Tower wants under precise control.

Two consequences worth keeping. A closed loop has no net descent, so a river circling the world must be pumped — which is the same architecture, closed the whole way round. And since nothing evaporates to space, Tower's water inventory is conserved and exactly known, forever.


D-019 — Geography is authored as data; terrain is generated from it

Decided. content/geography.json holds the authored geography. npm run terrain (tools/terrain.mjs) reads it and emits a height map, a colour map, a corridor detail render, and a seed world.json of places.

Why data first. The author asked to work the geography out before returning to app code. A JSON source plus a generator means the map is re-derivable from one readable file: move the river by editing eighteen waypoints, not by repainting. Coordinates are canon (θ, z) throughout, consistent with D-007 and D-011.

No dependencies. Node's zlib is enough to write PNGs directly (tools/png.mjs, ~60 lines for 16-bit greyscale and 8-bit RGB), so the tool adds nothing to package.json. Terrain noise is a small value-noise/fBm module.

Three bugs the renders caught that no test would have.

Known gap, deliberately not fixed here. The seed world.json contains polyline geometry (the river, the canyon) and kinds the app does not yet know. parseWorld accepts only points and polygons, so those two places would be dropped silently on load. App work is paused by request; this is the first thing to fix when it resumes. Closed by D-022.


D-020 — The world is belts, and the desert rings it

Decided by the author. The desert and its three ridges ring the world; the capital's water becomes a sea running along the axis; both end caps get ring oceans; the range feeds four parallel rivers.

The ring desert is a barrier no Earth desert can be. 22,000 km², 13% of the surface — modest against Earth's ~33% — but on a cylinder there is no way around a band. Combined with three ringing ridges it is three concentric walls across Tower's waist.

The drainage was the hard part, and Earth answered it. Many rivers each cutting their own canyon would destroy the chokepoint. But on Earth most drainages do not cut through a rising ridge — they are defeated and diverted, and only an antecedent river keeps its course. So three of the four rivers are turned by First Ridge, run along its front gathering each other, and leave with the fourth through the one gap. The Narrows carries the entire world's runoff, ~392 m³/s. The chokepoint is strengthened by the extra rivers rather than diluted, and the hydrology is more realistic than a single river would have been.

The Deep's orientation is the decision, not its size. Long down the axis and narrow around, so it is crossed the short way — around θ, where the floor curves up and the far shore stands 2.6 km above eye level. Lengthwise it would be dead flat. It also gives Tower back a thermal flywheel: 19× the diurnal heat storage of bare ground, 5.9 days per kelvin at full power. D-017's "if Tower blinks the rain stops" becomes "the world has about a week of momentum" — a lapse now has time to become visible before it becomes fatal.

The cap seas were not chosen. Gravity on an end cap is tangent to its surface, so water cannot rest there and runs to the rim. Both ring oceans are consequences, not designs.

Skiing came out of §1.9.2 unprompted. A descending skier has radial velocity and therefore real Coriolis: a steady antispinward push on axial fall lines, and a weight change of up to 11% on circumferential ones. A retrograde run gets lighter as it accelerates; a prograde run grips harder. Every slope in Tower has a handedness, which is a sport that cannot exist on Earth.

Tool work this required: ring footprints, a flood op for troughs and bands, and a tarn op that fills from the bowl floor rather than the rim — the first version took its level from the rim mean, so water spilled downhill and every mountain lake rendered as a half-moon.


D-021 — The terrain tool reports progress, and got 13× faster doing it

Prompted by the author: npm run terrain ran for a long time saying nothing. Adding progress output meant first working out where the time went, which turned a logging task into a performance one.

86 s → 6.4 s. Three causes, all found by measuring rather than guessing:

The log is a table, not a stream. One row per relief op with its timing, then relief/colourise totals per view, then each PNG with its size, then the seed. The point is to see which feature is expensive when the file grows, not just that something is happening. --quiet collapses per-feature rows.

Worth recording as a habit: the request was for logging, and the logging is what exposed that the colour PNG cost more than the entire mountain range. Instrumenting first and optimising second found three real problems in one pass.


D-022 — The app catches up with the decisions

Implemented. Five outstanding decisions, plus two bugs found while doing it.

The schema now matches the world (closes D-019's gap). polyline geometry joins point and polygon — a polygon needs three vertices for an interior, a polyline only needs two to go somewhere — and the kind list grows from six to thirteen (mountain, river, canyon, forest, desert, tower, town), with AREA_KINDS / LINE_KINDS deciding whether a traced shape closes. Before this the seed silently dropped The Long Water and The Narrows. A test now loads static/world.json and asserts nothing is lost, so the two files cannot drift apart again unnoticed.

The rendered terrain is drawn under the map, which is what the whole tool was for. Area regions become outlines rather than fills when it is showing: the raster already says where the desert is, and a translucent rectangle over it only fights.

A desync that would have been baffling. The app cut the map at θ = 0 with no slats; content/geography.json renders against θ = π. The raster would have landed half a world out of register with its own place markers. Both now use MAP_CUT_WITHOUT_SLATS = π — opposite the capital, which is D-013's antimeridian principle applied to habitation rather than geology — and a test asserts the two files agree.

Reader and writer modes (D-011). ?writer=1 reveals the tools, editor, canon controls and import/export; readers get the map, the place list, a read-only detail card and the sightline probe. A bug worth recording: renderEditor un-hid the editor whenever a place was selected, overriding the mode — so readers were shown the edit fields including private notes. Mode is now decided inside renderEditor, not applied around it. Hiding a panel elsewhere is not the same as never rendering it.

Permalinks (D-010). #place=<id> selects and centres a place; #at=θ,z restores a sightline endpoint. This is why D-012 made ids follow names only until pinned — a permalink is worthless if the id churns.

Labels are placed by priority and skipped on collision. Settlements outrank regions; overlapping labels are dropped rather than drawn on top of each other. With 22 seeded places the mountain stations were an unreadable smear.


D-023 — The exterior view, and what it is honest about

Implemented. Phase 3: Tower from outside, with the hull, both caps, the collector, the converging beam, the spin, and picking on the hull.

Canon numbers are the code's numbers. Scene units are kilometres and the axis lies along Z with the sun cap at z = 0, so a canon point is literally (R cos θ, R sin θ, z). No conversion layer to get wrong.

Picking has to undo the spin. The hull turns, so a ray hit is in world space while the answer wanted is canon θ, which is fixed to the hull. Resolving the hit through worldToLocal handles it. Verified by clicking the same screen pixel at two spin phases 34° apart and confirming the reported θ moved by 0.588 rad against a 0.593 rad phase change, with z identical — the pixel really is landing on different ground as Tower turns beneath it.

Three departures from physics, all stated in the code. Sunlight runs parallel to the axis, so the flank is edge-on and correctly renders almost black; fill lights make it readable. And in vacuum the beam between collector and cap has nothing to scatter off, so it would be invisible; it is drawn faintly because it is the point of the view. Same licence as the map's hillshade, and worth naming rather than letting a reader mistake either for a physical claim.

Three.js loads on first use of the view. It is by far the largest dependency and starts an animation loop; a reader who only opens the map should not pay for it.

Earth is not drawn. It is ~90° off the axis and hundreds of thousands of km away; at true scale it is invisible, and at any visible scale it is a lie about distance. Given how much of this world came out of taking distances seriously, a compressed Earth would be the wrong kind of shortcut. The sunward glow is honest — the sun really is that way — and D-017's point stands: from inside, Earth is never visible at all.


D-024 — The terrain is wrapped onto the hull as an x-ray

Implemented at the author's request. The exterior view paints the rendered inner surface onto the outside of the cylinder, alongside the place markers.

It is a projection, and the code says so. From outside, Tower is structure; the texture shows what is inside. That makes it the frankest of the view's three reading aids — nobody could mistake a forest on the outer hull for a physical claim, where the fill lighting and the visible beam are subtler about it. It earns its place because the hull is opaque: this is the only view that shows the whole world at once.

The UVs are rewritten, not derived. Three's cylinder runs u around the circumference and v along the axis; the raster is the other way round, x = z and y = theta. The obvious approach — recovering the angle from each vertex position — is wrong, because the seam vertices are duplicated and atan2 gives both the same angle, smearing one column of triangles across the entire image. Remapping the original uv attribute stays linear and lets RepeatWrapping absorb the offset.

Alignment was verified, not eyeballed. The raster was sampled at each place's computed position and the colour classified: four of four mountains landed on rock, both cap seas on water, the desert on desert. Judging this by eye would have been hopeless — the whole map is bands of green and tan, and a half-world error looks much like a correct one from most angles.

Two fixes the terrain forced. Small bright dots read fine against a bare hull and vanish into forest and desert, so markers gained a dark outline shell; and they are now scaled per frame to a roughly constant screen size, because 4 km is sub-pixel at the 1,200 km the default view sits at. The terrain also needed an emissive map: sunlight runs along the axis, so most of the hull faces away from every light and the map would otherwise be legible only on a sliver.

Also fixed here: selecting a place from the list moved the panel but not the camera — only permalinks called focusOn. Both paths now do.


D-025 — An empty draft is not a draft

Fixed after the author hit it. The map showed one place where twenty-two were expected. The local draft was empty and, because any draft beat the seed, the published world had been shadowed since the first time the app was opened.

How it happened. public/world.json originally shipped as {"places": []}. Opening the app once in that window persisted an empty world to localStorage — pagehide flushes on close — and from then on load() saw a draft, took it, and never fetched the seed again. Twenty-two places were sitting in the served file, and nothing in the UI suggested anything was wrong: an empty world and a deliberately emptied world look identical.

The fix. A stored world with no places is treated as no draft, so the seed loads. There is nothing to protect in an empty world, and the opposite failure — a deliberately emptied world refilling on reload — is visible and one click to undo, where this one was silent and permanent.

Plus an explicit way back. Reload published world in the Data section discards the draft and refetches world.json, for the case where a draft has drifted rather than emptied. Verified end to end: an empty draft now recovers 22 places on reload, and reseeding after a local edit returns 23 to 22.

The general shape is worth remembering. Local-first storage that silently wins over shipped content will, sooner or later, win with nothing in it. Precedence rules between a cache and its source need a defined answer for the empty case, and a manual override for every other case.


D-026 — Two grids that did not agree, both removed

Removed at the author's request. The exterior had structural ribs every 20 km; the map had a graticule every 50 km. Neither was wrong on its own, but side by side they invited a comparison that does not hold — twenty rings against ten lines, counting nothing in common.

Both are gone. Neither was load-bearing: the map keeps its scale bar and the exterior its spin readout, and with the terrain drawn the ground itself carries the sense of distance far better than either grid did. A reference grid that has to be mentally reconciled with another reference grid is worse than none.

Worth generalising: two independent scales shown in two views of the same object must agree or one must go. It is not enough for each to be individually correct.


D-027 — A band is its own geometry, not a polygon

Fixed. Regions that ring the world — the desert, the forest, both cap seas — are now { type: 'band', zFrom, zTo } rather than four-cornered polygons.

Found while removing the graticule. With the grid lines gone, a stray hairline was left across the middle of the map. A band written as a polygon runs theta from 0 to 2pi, and both of those normalise to the same map row — so all four corners land on one line and the region collapses into a one-pixel streak. It had been hiding among the graticule lines the whole time.

Why a new type rather than a workaround. On a cylinder, "the whole circumference between two z" is a genuinely different shape from a polygon: it has no theta extent to give, because it has all of it. Tower's geography is built out of these — D-020 made the desert ring the world precisely because a band cannot be walked around — so it deserves to be a first-class shape rather than a polygon that happens to wrap. The map draws it as a full-height rectangle, hit-testing is a z range, and the editor reports it as rings the world, z 148.0 km to 218.0 km (70.0 km deep).

Tests assert the four ringing regions survive as bands through parse and public export, and that a band with unusable bounds is rejected like any other geometry.


D-028 — The beam was inverted, and cones fringe

Found because the author asked what the brown cone and the white circle were. That question is its own answer: the two most important objects in the exterior view were unlabelled and one of them was drawn backwards.

The beam pointed the wrong way. CylinderGeometry(radiusTop, radiusBottom) combined with rotateX(-pi/2) — which sends +Y to −Z — put the first radius at the collector. Writing the pair in the order the eye reads them gave a cone wide at the sun cap and narrow at the mirror: light converging on the collector instead of into the aperture, with a 122 km mouth swallowing a 50 km hull. It had been that way since the view was built, and the flat brown wash it rendered as hid it.

The brown was a second bug. A uniform additive layer at 0.085 opacity over a black sky sums to about RGB(43,39,30) — a dull brown that reads as a solid object, which is the opposite of a beam.

Cones fringe, so the beam is rays now. Brightening it exposed bright dashes scattered across the collector's rim. Isolated by bisection: removing the cone cleared the disc, removing the stars did not, and shortening the cone moved the dashes with its mouth. At the mouth the cone's shell is edge-on, so its triangles project to slivers and the additive blend stacks them. Rays from the mirror to the aperture have no silhouette to alias, need no alpha ramp to fake convergence, and read more like light than a translucent solid ever did.

Everything is captioned now. Collector, beam, sun cap, far cap, as screen-space labels. If the person who commissioned the world cannot identify an object in it, a reader has no chance.


D-029 — The view persists, which surfaced a boot-order bug

Implemented at the author's request. Map or exterior is remembered in localStorage and carried in the permalink hash, so a shared link can say look at this from outside. The hash wins; storage is the fallback for a bare URL.

And it immediately broke the exterior on load. Restoring that view un-hides the canvas and constructs the renderer in the same task, before the browser has laid the element out — so resize() measured a zero-sized box, bailed out, and the renderer kept its default 300×150 buffer stretched across a full-size canvas. Clicking into the view had always worked because layout had long since settled by then; only restoring it on boot hit the window.

Fixed with a ResizeObserver on the canvas rather than another guess about timing, which also covers the sidebar and window resizing for free.


D-030 — The camera persists, and the caps are places

Both at the author's request.

Camera. Position and target are stored per browser and restored on load, so a reload returns to the vantage you left rather than the default framing. Every field is validated before use — three finite numbers each, and a distance inside the controls' own limits — because a corrupt entry here would leave the view pointing at nothing with no obvious way back, and silently-bad persisted state has already cost this project once (D-025). Saves are debounced: OrbitControls fires change every frame of a drag.

Amended: a heading is an angle, so it validates by wrapping, not by range. The interior view persists the same way, and its saved heading was range-checked like a distance — rejected outside ±100 radians, which the panning loop reached after about sixteen turns in one direction, at which point a reload silently put the observer back facing deepward. Nothing was corrupt; the check was asking the wrong question. The heading is now wrapped into [0, 2pi) at every write, and on restore any finite value is accepted and wrapped, because an unwrapped heading from an older save is still a perfectly good direction to face.

Caps. The sun cap and far cap are now features in content/geography.json, so they appear in the place list and can be focused like anywhere else. Two things had to be special-cased, both for the same underlying reason — a cap is not a patch of ground:

A note on the debugging. Verifying camera persistence took several wrong turns because the test kept invalidating itself: location.reload() fires pagehide, which saved the live camera over the value the test had just planted. The persistence was working the whole time. Worth remembering that a pagehide save makes "set state, reload, check state" an invalid experiment.


D-031 — Sidebar: an accordion, and a label that stopped lying

Five UI corrections from the author, all of them about the panel saying the wrong thing or being in the wrong place.

"Canon" was a section heading for one slider. It read Canon → Star slats — 0, which framed an exploration control as if the whole section were canon. The section is now Star slats, its value is a Count, and canon is a chip on the summary that appears only when the count is 0 — because that is the one value canon actually asserts (D-017). A label should describe what it labels; "canon" is a property of a value here, not a category of setting.

Order follows scope. View moved to the top: it changes the whole app, not one panel. About moved to the bottom, where the least-used thing belongs.

Foldable sections. Seven panels in a fixed column pushed the useful ones off-screen. They are <details> elements now — native disclosure, keyboard and screen-reader behaviour for free. This first shipped as an accordion, one open at a time, with the Selected panel opening itself on selection; both were removed — see D-032.

Mode gating moved from a CSS class to the hidden property, since <details> already carries open/closed state and stacking a .hidden class on top of it was a second, conflicting notion of visibility.

And a claim removed. The About text said the canon was "worked out in the open", with links to design.md and decisions.md. Those documents are the author's working notes on an unpublished novel, not a public invitation, and the links went to a repo a reader may not have. Now it says what is true and no more: Tower is the setting of a novel. This map is its companion piece.


D-032 — No automatic opening or closing

Reverted at the author's request: the accordion was annoying, and nothing should open or close by itself.

The two behaviours were one mistake. Exclusive opening meant that clicking a place could collapse the panel about to describe it, so the Selected panel had to open itself to compensate. Removing the accordion removed the need for the compensation: sections the author opens stay open, so a selection simply appears in a panel that is already there. A workaround that only exists to undo another behaviour is a sign the first behaviour is wrong.

What remains is state, not behaviour. View and World start open, and Selected starts open but stays hidden until something is selected — so the first selection shows without anything having moved. After that the panels are wherever the author put them: closing the editor keeps it closed through every later selection.

The general rule this is worth remembering by: an interface that rearranges itself in response to a click costs the user their place. Prefer a sensible initial state and then hold still.


D-033 — The slat control is disabled and its UI removed

Removed at the author's request: changing the count did not fully work. It did not, and for two independent reasons, both worth writing down because this is deferred rather than abandoned.

The raster has no slats in it. terrain-color.png is generated from content/geography.json at canon's 0 slats. Raising the count drew slat bands as a vector overlay on top of terrain that shows unbroken land underneath them — two layers asserting different worlds. The corridor is authored the same way: Snowcrown, the rivers and The Narrows all assume no slats, so bands above 0 cut straight through them.

And the map's cut angle moves with the count. mapThetaOrigin() returns slatCentre(n-1) when slats exist and π when they do not (D-013, D-024). The raster is drawn against π, so every non-zero count rotates the terrain out of register with its own place markers — by 120° at 3 slats, 157° at 8, up to 137 km of displacement. This is the same desync D-024 fixed for the zero case, and the test added there only guards the zero case.

What was done. The section is gone from the sidebar and loadSlatCount() is no longer called, so a stored non-zero count from before cannot leak back in. What was kept is everything that makes the count correct: src/canon/slats.ts, the derivation, and the tests covering counts 1 to 16 — including that land stays exactly Wisconsin's 140,268 km² across the whole range, and that the map cut lands on a slat centre at every count.

What re-enabling would need. The terrain tool would have to bake slats into the raster from the same count the app uses, which means the count becomes an input to npm run terrain rather than a runtime toggle — and the geography would need re-authoring around wherever the bands fall. That is a content decision as much as a code one, which is the real reason it is parked.


D-034 — The interior view: real geometry, and haze doing the work

Phase 5, and the piece design.md §2.4 said would carry the illusion. The scope held: real geometry, simple ground. The shell is the actual cylinder, displaced by the height raster and painted with the colour raster; there is no scenery, no buildings and no vegetation.

Nothing in the scene rotates, and that is the point. The observer, the ground and the air all co-rotate, so from inside a sealed hull the spin is genuinely invisible. No stars wheel overhead because there are no stars (§1.4). What is left is src/canon/motion.ts, and it says something the design did not anticipate:

lean, antispinward
rain at terminal velocity 1.5° — fixed, whatever the cloud height
a 1 km waterfall, at the bottom 11.3°, and 133 m of drift

The waterfall figure is the velocity at the bottom, which is the angle a streak of falling water is drawn at. §1.9.2 tabulates 7.6° for the same drop, which is the tilt of the path — where it lands against how far it fell. Both are right and they differ by exactly half again in tangent (freeFallPathLeanRad alongside freeFallLeanRad, pinned in test/motion.test.ts), and noticing they were not the same number is the only reason this is written down rather than silently contradicting the canon.

Rain barely leans, and it leans by a constant angle because drag holds the fall speed constant. Only free fall keeps accelerating, so only free fall keeps leaning. Design.md §2.4 offered "slanted rain and a leaning waterfall" as a pair; they are not a pair. The waterfall is the compass. The rain is drawn, and the readout prints 1.5° beside it so the smallness reads as the finding it is rather than as a bug.

Haze is integrated per fragment, 24 midpoint samples through the rotational density profile, and it reproduces the §1.8 table from the other direction: the readout prints 78.1% straight up against 31.3% for the same 100 km along the ground. The extinction is carried per channel (Rayleigh goes as λ⁻⁴, so red survives twice as far as green and blue half as far), which is what makes distance blue rather than merely grey.

The airlight is neutral, deliberately. Tinting it sky blue is the obvious move and it is wrong: blue already scatters 2.4× harder in the per-channel extinction, so a blue source colours the haze twice and every distant thing turns mauve regardless of what it is made of — desert included, which is how the bug was spotted. With a neutral source the blue falls out of the physics and the far side stays recognisably forest and desert at the 78% canon promises.

D-035 — Standing on the ground requires the ground on the CPU

The shell is displaced in the vertex shader, which is fine until somebody has to stand on it. The first build put the camera 1.7 m above the undisplaced cylinder — underground everywhere the land is higher than 1.7 m, which is everywhere. From inside the rock the ground is invisible: its underside is a front face and the shell is drawn back-side only. The view showed sky below the horizon and nothing else, and at 366 m of eye height it looked perfect, which is what made it puzzling.

There is no way to ask the GPU where the ground is, so terrain-height.png is now read back through a canvas and sampled here. Two mismatches had to be handled:

The readback is 8-bit — browsers decode 16-bit PNGs down to 8 — so elevations quantise to 19.5 m steps. Both the shader and this lookup read the same quantised data, so they agree with each other, which is what matters for not being buried.

D-036 — A custom ShaderMaterial writes linear colour into an sRGB canvas

Three inserts its colour-space handling into its own materials and never into a custom one, and the first build silently got both halves wrong: the colour raster was sampled as if it were linear, and linear output was written straight to the canvas. Everything read dark and oversaturated, and the far side of the world came out navy where it should have been hazy green.

The fix is both halves explicitly — decode the raster with a pow(c, 2.2) and end each fragment shader with #include <colorspace_fragment>. The trap on the way: adding #include <colorspace_pars_fragment> too fails to link, because Three already puts that chunk in the prefix it builds for every material in order to define linearToOutputTexel. The duplicate definitions produced no console error at all — only INVALID_OPERATION: no valid shader program in use once per frame, and a window filled with clear colour.

D-037 — A shared #view= link was erased before it was read

Found while opening the interior view by URL. App's constructor rendered before it decided which view to open, and render() rewrites the hash from current state — so #view=interior was replaced by an empty hash a few lines before initialView() went looking for it. Every shared link that named a view had been landing on the map since D-029, and it went unnoticed because the last-used view is also persisted in localStorage, which restored the right view for whoever had visited before. The view is now decided before the first render.

D-038 — Selecting a place walks you there

Turning to face a place was the wrong default, and standing in the view for a minute makes it obvious: at ground level the haze horizon closes at 100–150 km (§1.8), so "face the Deepward Sea" from the capital points at 270 km of nothing. Selecting a place now moves the observer to it, which is what "focus" already meant in the other two views — the map centres, the exterior swings round, the interior walks.

Places with extent also get a heading: stand at the head of a river and look down it, taking the centroid of the traced points as the aim. A point has no direction of its own, so arriving is the whole answer and the heading is left alone. The separate "Stand at the selected place" button is gone, since selection now does exactly that.

D-039 — The height raster gets a declared working range

terrain-height.png was normalised 0..5,000 m against a fixed maxElevationM, and both halves of that were wrong.

The floor mattered more than the ceiling. Clamping at zero flattened everything below the datum, so every sea and lake floor was a plane — the Deepward Sea read as 0 m where it is actually −320 m. content/geography.json now declares minHeightM / maxHeightM and the raster is stretched across that range.

The range is declared, not fitted. Fitting it to the terrain would silently change what every grey level means whenever the geography is edited, which is the kind of drift D-024 came from. Instead the generator prints the range it found against the range declared, warns and clamps if the terrain has outgrown it, and test/raster.test.ts asserts the declared range still contains the terrain.

The precision win is smaller than it looks, and worth stating plainly: the terrain spans −508..4,392 m, so tightening the range to −520..4,400 m moves the step from 19.6 m to 19.3 m. The quantisation is not set by the range at all — it is set by browsers decoding a 16-bit PNG down to 8 bits, which leaves 256 levels however wide the span. The fix for that is a second byte in a second channel, which is a change to the encoder and the shader, and is not made here.

D-040 — The axial sightline names the distance it measured

The readout compared "100 km up" against "100 km on" — except the along-axis probe was clamped to the far cap, so standing 22 km from the end it measured 22 km and still called it 100. It reported 77.5% where the honest figure for 100 km is 31%, which is exactly backwards from the point the line exists to make. It now measures whichever axial direction has the most room and prints the distance it used.

D-041 — The caps get a pattern, and it is polar

Superseded by D-078 for the pattern: the caps' frames are identical triangles, at this pitch.

Half superseded by D-045: the far cap is now a window, and this pattern survives on it as the window's mullions rather than as a filled checker. Everything below still holds — the tessellation, the pitch, the mipmapping and the reason for all three are unchanged, and the sun cap is still exactly this.

The end caps were flat tinted discs. They are the only surfaces in Tower that are not landscape — 100 km of structure closing each end — and blank they read as fog with an edge. A checker fixes that, and does something more useful besides: it is the one thing in the world with a known cell size, so a glance at the far cap tells you roughly how far away it is. That is worth having in a world where §1.8 says distance is otherwise unreadable.

Polar, not square. A cap is a disc on a spinning cylinder, so its natural coordinates are radius and angle; a square grid meets the rim at every possible angle and reads as something laid over the geometry rather than part of it. Cells stay roughly square by doubling the sector count each time the radius doubles, which leaves a seam at each doubling — which is what a segmented diaphragm would have anyway. The alternative is slivers at the hub or slabs at the rim.

One texture, both views. Drawn once into a canvas and shared, so the exterior and the interior cannot drift apart. Three maps UVs across the bounding box for both CircleGeometry and RingGeometry, so the same image lands correctly on the far cap's full disc and on the sun cap's annulus.

Two things had to be walked back after looking at it:

The cap framing also moved to ~40° off the axis rather than ~17°, since there is now something on the face worth turning to see, and the caps carry an emissive copy of the pattern for the same reason the hull carries one of the terrain: sunlight runs along the axis, so a cap is edge-on to every real light and renders as a dark disc exactly when it is being looked at.

D-042 — Screenshots are taken through the UI, and diffed per shot

npm run screenshots drives the real application in a headless browser and photographs it: nine views across two viewports, plus a contact sheet of all nine.

Through the UI, on purpose. Every shot clicks the view buttons, picks places out of the list, and drags to orbit and to look up, rather than posing a renderer directly. Posing is easier and it defeats the point. A screenshot taken by using the application fails when the application fails, so a black canvas or a sidebar that has swallowed the window ends up in the picture instead of hiding behind a passing unit test. The tests already pin the canon numbers; nothing was checking that the thing still draws.

The contact sheet is for a human. overview.png exists to be looked at once, before committing — one image that answers "is it still there?". It is built by laying the shots out as an HTML page and screenshotting that, because the browser is already running and already knows how to lay images out; tools/png.mjs only encodes, and this project has no reason to gain a PNG decoder.

Diffed per shot, not per sheet. The first version measured divergence on the contact sheet, and that was the wrong unit: rewriting two of the nine panels outright moved the whole-sheet mean by 0.6%, because a mean over nine panels on a dark page dilutes everything. Per shot the same change reads in tens of percent.

The 3% threshold is measured, at both ends. Seven of the ten shots come out byte-identical run to run — the rain is the only randomised thing in Tower and is seeded on the way in, the exterior spin is switched off, and each shot gets a fresh browser context so no camera leaks into the next. The two exterior shots are the exception, drifting up to 0.98% because OrbitControls damps toward its target asymptotically and lands a hair short of the same place each time. Against that, hiding both 3D canvases scored 7–31%; a view going entirely black scores 12% to 39%. 3% is three times the worst noise and less than half the mildest real failure.

It is a smoke alarm, not a verdict: when it fires the answer is to look at the sheet. And it only catches regressions — a view that never worked scores 0% when it breaks further, which is exactly how mobile-exterior behaved during the sabotage test that calibrated this, because at the time it was already a black slot.

Two browsers, one tour. The contact sheet gets its own browser instance. In the one that has just taken the tour — nine WebGL contexts and two Three.js scenes — Page.captureScreenshot on the sheet fails outright with "Unable to capture screenshot", and the sheet is plain HTML and images, so the only thing wrong with it is its neighbours.

What it found immediately. The mobile row: the sidebar was a fixed 340 px against a 390 px viewport, so the world got about fifty pixels and both 3D views rendered into a black slot. Fixed in D-043, which the same tool then verified — the six desktop shots moved 0.00% and the three mobile ones moved 10–33%, which is exactly the shape a layout fix should have.

D-043 — On a phone the sidebar is a drawer

The layout was one grid, 1fr 340px, with no media query anywhere. On a 390 px phone that is a 340 px panel and fifty pixels of world, and nothing to hide the panel with — the application was, on a phone, a control surface for something you could not see.

A drawer, not a stack. The other obvious move is to put the panel below the world in a single column and let the page scroll. That is wrong here: the world is a position: absolute canvas filling main, all three views are drag-to-look, and touch-action: none means every gesture over the world belongs to the world. A scrolling page and a canvas that eats touches are in permanent conflict. A drawer keeps the canvas full-bleed and the gesture unambiguous.

Below 720 px, where the panel would otherwise be taking more than half the window. Above it nothing changes at all — the desktop shots come out of the screenshot tour byte-identical.

Never the full width. min(340px, 86vw) leaves a strip of world showing, which is what tells you the panel is over the map rather than a page you navigated to. That strip is live canvas, though, and in writer mode a stray tap on it drops a marker, so a scrim covers it and closes the drawer instead.

Two buttons, not one. The opener is fixed at the top right, opposite the view switcher. The closer is in the panel header, in normal flow. A single fixed toggle would have to float over the panel's own scrolling content once open, and would occlude whatever scrolled under it; this way each button is only ever over the thing it belongs to. The spin readout, which also lives at the top right, drops to top: 54px to clear the opener.

Picking a place closes it. In every view selecting a place moves you: the map centres, the exterior swings round, the interior walks there (D-038). On a phone the list is the drawer, so staying open means watching none of it happen.

The closed drawer is inert. It is only translated off-screen, so without that its fields stay in the tab order — typing into a panel nobody can see, scrolling it under the world. It is set from the same media query the CSS uses, and crossing that breakpoint (a phone turned on its side) resets to closed, which is also what clears inert on the way back to the desktop column.

One thing came along with it. The standing readout is ~65 monospace characters of aligned columns and overran 390 px at 11 px type. It shrinks to 9 px on the same breakpoint rather than wrapping — wrapping those columns destroys the thing they are for.

D-044 — Tower's wall is a kilometre thick

Canon had no figure for it. Both 3D views drew the hull, the caps and the collector as zero-thickness surfaces, which is invisible until something is seen edge-on and then reads as a paper cutout rather than a structure.

1 km, floor to outer skin — 2% of the radius. The alternatives were a 200 m technical minimum, which is defensible on shielding and services and is sub-pixel at every exterior zoom, and 5 km, which is unmistakable and a great deal of mass to justify later. A kilometre reads as built without making Tower a pipe.

It is only visible at the openings, which is worth stating because it took a false start to notice. A closed can has no visible thickness from outside — the silhouette of a 50 km cylinder does not change when you give its wall a kilometre. Depth shows only where there is a hole: the beam aperture, and now the far cap window. So the caps sit at the outer face of the wall with a 1 km collar standing between them and the inner surface, and it is the collar you see down.

The exterior camera had to be let closer. A kilometre is 1% of the cap's diameter, so at the old 120 km minimum distance the collar was two pixels and the near plane sat at 8 km — the depth was being drawn correctly and could not be got close enough to to see. 60 km and a 2 km near plane make the choice legible; without that change the constant would have been a number in a file and nothing else.

D-045 — The far cap is a window, and the checker becomes its frame

Amended by D-078: the mullions are a triangle lattice, not the polar tessellation.

§1.4 has said since the first draft that the caps are the only views out of Tower and that the sky through them turns rather than sweeps — a pinwheel about the axis with one motionless star at the middle, one revolution every 7 min 29 s. It was written and never built: §2.4 said flatly "no stars", and D-041 had meanwhile made the far cap a checkered structural plate.

Glazed whole, not windows at its foot. §1.4 put the windows at the base of a 50 km cliff. A strip at the bottom of something 540 km away is nearly nothing to look at, and the canon fact worth having in the view is the pinwheel, which needs sky. So the whole far cap is a window. The sun cap is not — it faces the sun and needs glare management rather than a view, and that asymmetry is already canon.

The checker survives as the mullions. Deleting it would have retired the only object in Tower with a known cell size, which is the only rule anyone has for judging distance (D-041, §1.8). Instead the same polar tessellation at the same pitch is drawn as the frame around transparent panes: the cells are exactly where they were, and you look through them rather than at them. The rule now reads against the sky, which is a better place for it than against a plate.

The physics fell out of the shader that was already there. A pane is black with stars in it, so the existing haze integration does the rest by itself: by day the airlight term swamps everything and the cap is a disc of blue, which is what §1.5 already said would happen to anything 540 km off; at night the airlight collapses with the beam and what is left is the sky. Nothing needed special-casing.

The stars are not scaled by the exposure, and that is the whole point. Nothing Tower does to its own beam changes how bright Vega is. What does change is the eye, so a separate gain carries that half: full at night and below, 2% at temperate. The stars are there at noon and unseeable, which is a fact about eyes and not glass.

Two projections were wrong before the third. Gnomonic (xy/z) about the axis puts the pole star at the origin and blows up as the ray goes perpendicular to the axis — which is the ordinary grazing view from the foot of the cap, not an edge case. Painting the field on the cap plane instead is stable everywhere and has no fixed angular scale: from a kilometre away the entire 60° field lands inside one star cell and the sky comes out empty, which is exactly what the first screenshot showed. Azimuthal-equidistant has neither failure. It is defined for every direction but exactly sunward, stars keep the same angular size however far off the window is — which is what being at infinity means — and the pinwheel is not animated but falls out of the projection, because azimuth plus spin phase is the rotation.

The pole star is placed rather than left to the hash. It is the fixed point the whole phenomenon is read against, and a sky that happened not to roll one there would be missing the only part of it anybody in Tower would have named.

Settled by D-048: the surviving quarter of the beam does not go through the window at all. It arrives collimated in a 3.2 km spot — a thousandth of the cap — and that spot is a mirror, not glass.

D-046 — Night is the beam banked, not the swarm aimed away

Still true under D-074, where night begins when the sun goes out at the far cap.

The obvious way to make night in a habitat lit by a steerable mirror swarm is to aim the swarm off. It does not work here, and §1.3's own numbers say so.

The beam glows by itself: §1.3 gives the glow to the beam and the air rather than to the diffusers, and says outright it "would be there if the beam were empty". 779 W/m²/sr over a strip 3.65° wide running horizon to horizon is about 0.2 sr, and with the mean cosine of a strip through the zenith that is ~100 W/m² on the floor — half of temperate. A lit beam is a light source no matter where the mirrorlets are pointed, so a dark world requires a dark beam. There is a test for this figure in test/canon.test.ts, because the conclusion rests entirely on it.

So night is the beam banked to a thousandth of full, 0.32 W/m²: a rod you can still find overhead and a world you can just make out under it. And dark is the beam shut down — no airlight, no lit ground, no rod, and the only thing left in the world is the sky through the far cap (D-045). The two settings are genuinely different states rather than two points on a dimmer, which is what made both worth having.

The beam's brightness scales with the exposure already, so it banks and goes out on its own; neither setting needed the beam shader touched.

D-047 — The exterior hull reads two ways, and they are different claims

Amended by D-076, D-077 and D-081: Rock, Piping, and the two together are on trial beside True and X-ray.

The exterior had a "Show terrain" checkbox that wrapped the raster onto the hull. It was doing something stronger than a checkbox admits: from outside Tower is structure, so terrain on the outside is not what Tower looks like — it is a diagram of what is inside, drawn on the outside because the wall is opaque. Useful, and not a view. The two readings are different claims about the same object, so the control is now a two-way switch that names both:

The wall is drawn as two meshes now, not one double-sided one. Its faces show different things: the outer skin changes with the mode and the inner surface is always the land, and a single DoubleSide material has no way to say so. The inner mesh is emissive for the same reason the x-ray always was — it is lit by a beam this scene does not model, and unlit it reads as a hole rather than a world.

This is what made the far cap window worth having from outside. D-045 glazed it for the sake of the interior, where the payoff is the pinwheel. From outside the payoff is different and better: the panes are clear, so in true mode you look in through the glass and the world is really there, mountains and desert and lakes receding 540 km down the tube behind a grid of mullions. It is the only line of sight into Tower that is not a projection, and it exists only because the wall got a window and the hull got two faces in the same week.

The dark backing disc the first version of D-045 put behind the glazing is gone. It was there to stop the panes showing the inside of the tube — which turns out to be exactly the thing worth showing.

D-048 — The far cap's bullseye is a mirror

Decided by the author, from four options: mirror it, let it out, absorb it behind fins, or grade the swarm so it never arrives.

The shape of the load is the argument. §1.3.1 leaves 24% of the beam unscattered after 540 km, and it survives collimated — so this was never 13 TW spread over a 7,854 km² cap. It is 1.7 MW/m² in a 7.96 km² spot, about 1,200 suns, and an absorber there sits at 2,330 K: survivable for tungsten, and for no structure. But the spot is a thousandth of the cap's area, which means the whole problem lives on a 3.2 km bullseye and 99.9% of the far cap needs nothing done to it at all. That is what makes D-045's window and this coexist.

A mirror is the cheap thermal answer, not the expensive one. At 99.9% it absorbs 1,665 W/m² — about 1.2 Earth solar constants, ordinary sunlight. The thing that would have glowed white is merely warm. The absorber-with-fins option needed the entire cap radiating at 400 K and 13 TW moved 50 km sideways from the spot to get there; it was strictly worse than every alternative.

It also recovers the energy, and fixes a number that was already wrong. The return trip makes the optical column 1,080 km rather than 540, so utilisation is 1 - e^(-2τ) = 94% against 76%. That matters more than it looks, because canon had a contradiction sitting in it: §1.3 sets full brightness at 54.3 TW ÷ 169,646 km² = 320 W/m², which assumes the whole beam reaches the inner surface, while §1.3.1's optical depth says only 76% does — implying 243 W/m². Both were printed, and a test pinned 320. With the cavity closed at both ends (the mirror at one, the collector at the other, returning what still escapes) essentially all 54.3 TW is eventually absorbed, and 320 is right as it stands. No test moved.

What it does not do is make weather. Aiming light at a region to warm it stays a separate lever, and §1.9.4 already says that is how every climate in Tower is made. The mirror is about not wasting the beam; the Dry Reach is about where the heat is wanted. Keeping them separate means neither constrains the other.

A geometric trap, recorded because I fell into it. The mirror sits on the axis, so it looks like it must occult the pinwheel's motionless centre — I wrote that into canon before checking. It does not. The pinwheel turns about the direction parallel to the axis, and that direction lands on the window at the observer's own radius, not at the cap's middle. From the floor the spin axis is horizontal, so the centre of Tower's only moving sky is a point on the horizon, dead ahead at eye level — while the mirror is ~10° above it from 270 km. The two coincide only for an observer standing on the axis, and nobody can stand there.

D-049 — Rain is not drawn any more

Rain existed in the interior view for one reason: with the hull sealed it was the only moving thing in the sky, and therefore the only evidence Tower turns. §1.9.2 then undercut it — drag pins a drop's speed, so rain leans a fixed 1.5° whatever the cloud, against a waterfall's 11°. The readout printed the angle to stop the smallness reading as a bug, which is an admission that the picture was not carrying the claim.

D-045 gave the view a whole sky going round once every 7 min 29 s. Against that, a 1.5° lean is not evidence of anything, and 6,000 line segments were being stepped every frame to fail to show it.

The physics stays. canon/motion.ts still computes the lean and test/motion.test.ts still checks it — it is a real fact about how anything falls in here, and the waterfall is still the compass. It is only the rendering that is gone, along with the toggle, the seeded raindrop field, and the six Three imports that existed for it. The readout line it occupied now prints the pinwheel's period, which is the interval you can actually time against the sky.

D-050 — The sun cap is a window too, and Tower never sees the sun

§1.4 was wrong about this cap, and the number that shows it was already in canon. It demanded "heavy glare management" on the grounds that the sun cap faces the sun. It does not face the sun. It faces the collector, which is 244 km across at 320 km — 41.8° wide against the sun's 0.53°, seventy-nine times its diameter. The collector sits between Tower and the sun by construction, so it occults it entirely, and the umbra is 120 km across where Tower's 51 km sits in it. There is no direct sunlight on the sun cap and there never has been. Nothing to manage.

So this cap is glazed on the same terms as the far one, and what fills it is the back of the collector: a black disc, because its front is a mirror and its back radiates 9.6 TW of waste heat at about 245 K, which is infrared. Around the edge of it, stars. It does not turn with them — the collector flies in formation rather than spinning — so the sun cap window is a still black disc against a wheeling sky.

Nobody in Tower can see the sun. They already could not see Earth (§1.4, the terminator orbit puts it out the side, which is solid hull). A habitat in orbit around a planet it has never seen, lit by a star it has never seen.

The occultation is computed in the shader rather than modelled: the fragment casts its own ray on to the collector's plane and asks whether it lands on the disc. That is the same trick the far cap uses for its stars, and it means the interior does not have to carry a 244 km mesh 320 km outside the world to draw one black circle.

The interior's checkered cap material is gone with this. Both caps are windows now, and nothing inside was still using it — the checker survives as the exterior's sun cap and as both windows' mullions.

D-051 — The range rings the world

The mountains were "a 140 km arc, not a ring" — four named domes covering about a third of the circumference, with the other two thirds simply unauthored. Asked to close it, and closing it is better than the arc was: a ring of peaks is a watershed that divides the whole world rather than a wall you can walk around the end of.

The ridge op grows optional ridged noise, which is what turns a ridge into a range. Without it a ring band is a smooth swell all the way round — right for the three low walls across the waist, wrong for peaks.

It blends with max rather than adding, and that is not a detail. Added, The Girdle's 2,900 m would stack onto Snowcrown's 4,300 and push the massif to 6,900 — past the raster's 4,400 m ceiling, where it would clip flat. Taking the greater of the two leaves the named summits as the high points of their own range, which is what they are: Snowcrown is not a separate mountain from The Girdle, it is the tall part of it.

Peaks reach about 3,900 m, so they cross the 3,000 m snowline and the ring reads as snow the whole way round. The rivers still cut it, because carve runs after relief.

D-052 — A fourth noise octave, and colour that mottles instead of dimming

Height. The base had three noise layers — 78 km, 12 km, 3.4 km — and a pixel is 264 m. Between the finest layer and the raster's ~530 m Nyquist limit there was an octave and a half of nothing, so the ground read as smooth swells with no grain on them at exactly the scale you look at it from inside. A fourth layer at 1.5 km and ±26 m fills that band. It goes in the base, deliberately: carve and flood run afterwards, so rivers still cut through it and lakes keep flat surfaces rather than inheriting a rippled floor.

Colour. There was already per-pixel variation, and it was the wrong kind — one noise field scaling all three channels together, which dims and brightens without ever changing the colour. A second, finer field now mottles: it pushes the channels apart, green moving most and blue least, so ground reads as patchy vegetation against bare soil rather than as one paint turned up and down. Keeping blue nearly fixed is what stops the desert drifting toward the same green as the forest.

It exposed a live bug, and the fix matters more than the noise does. The extra relief pushed the terrain to -521 m against a declared floor of -520 m, so every pixel down there was being silently flattened. The generator printed a warning and carried on; nothing else noticed. That is D-024's failure mode exactly — a clipped raster displaces terrain rather than breaking it, so nothing downstream complains.

Two changes: the floor moves to -560 m, and npm run terrain now exits non-zero when any pixel is clamped instead of printing a line into a log nobody reads. The unit test could never have caught this — it compares the declaration against extremes written down when they were last true, not against the terrain that just came out — and it now says so where it sits.

D-053 — The day is a front, not a dimmer

Superseded by D-074: the day is a sun that crosses the world, not a front.

The obvious day-night cycle is a global fade between the light settings. It would have worked and it would have thrown away the most interesting thing about how this world is lit.

What the physics allows. D-046 already established that the swarm cannot make night, because §1.3 gives the beam's glow to the beam and the air rather than to the diffusers — a lit beam lights the world whatever the mirrorlets are doing. So beam power has to ramp, and when it does, the sky brightens everywhere at once. There is no way to have one end of the axis get its morning before the other.

What the swarm can still do is choose where the light lands. The diffusers hand beam light to the ground, and they are individually flown with absurd control authority (§1.3.1). So the ground can be lit progressively even though the sky cannot, and the natural direction is the one the beam already runs: sun cap to far cap.

A Tower dawn is therefore a fully lit sky over dark ground, with a line of daylight travelling toward you at 225 m/s. That is not a compromise between the two options, it is what falls out of taking both constraints seriously — and it is a better image than either.

It gives Tower longitude. §1.9.4 says the world has no latitude: every point is equidistant from the light, which is why nothing about it varies. The cycle adds the axis back as 40 minutes of time difference between the caps. Dusk runs the same direction, so the sun-cap end has the shorter day by a hair.

The panel gets a time slider before it gets a clock. Running the cycle is the feature, but a 24-hour day at any watchable rate means waiting for dawn, and dawn lasts 40 world-minutes out of 1,440. Being able to scrub straight to 06:17 is what makes the front usable at all — and the front is a pure function of the phase, so scrubbing and ticking are the same operation.

The readout names the distance to the line rather than just the hour, because the distance is the part you can act on: 27 km sunward is about two minutes' wait.

D-054 — The collector gets a lit rim

Drawing the collector as pure black was right about its face and wrong about the object. From inside the sun cap window at night you could not see it at all — only that a piece of the sky was missing, which reads as a rendering hole rather than as a 244 km structure.

The physics gives the rim for free. The collector is backlit by definition: its mirror face is pointed at the sun and away from Tower. Anything holding 46,938 km² flat has structure standing proud of the mirror plane at the edge, and at the edge the sun strikes that structure side-on. Add the scatter every real mirror throws at grazing incidence and the boundary is a thin sun-coloured ring. That is simply how backlit things look, and it is the one part of the collector that can be lit.

What it is not is sunlight past the edge, and the distinction matters because the first is a property of the object and the second would break canon. Tower sits wholly inside the umbra — §1.3.0 — so the sun's disc is occulted from every point in the world. Nobody sees a sliver of star; they see lit metal.

Rendered as two exponentials: a tight core at the declared 4 km of edge structure, and a wide dim skirt for the scatter around it. One alone reads as a drawn outline rather than as light. The first attempt was three times too bright and its skirt seven times too wide, which washed the whole disc gold instead of tracing it.

It does not scale with the beam. The sun does not dim when Tower banks its own lamp, so the rim is outside uExposure, like the stars. It does fade as the eye adapts, but from a much higher floor than a star's — this is sunlit structure, and it stays faintly visible at noon where a star cannot.

D-055 — The collector looks the same from outside as it does from inside

The exterior view drew the collector as a near-white disc with a pale blue glow, and the sun cap window drew it as black with a lit rim (D-054). Both cannot be right, and the window is the one that follows from the physics: the collector's mirror face is pointed at the sun and away from Tower, so what the world sees is the back of it, a 46,938 km² radiator running at 245 K. That is infrared. In visible light it is black.

So the exterior draws two faces rather than one. The disc is two meshes sharing a plane, one front-facing and one back-facing, and the camera's own position picks which one it gets — culling does the choosing, so they never fight over the same depth.

The rim is what makes it an object. The 4 km of sunlit edge structure from D-054 is drawn here as a ring of geometry rather than as two exponentials in a shader, because out here it is being seen rather than looked through — and it is placed just outboard of the mirror, not overlapping its edge, so the two surfaces never share a depth either. It reads from both sides, which is right: it is structure, not a reflection.

The cost is that the largest object in the scene is now a dark one. The label, the beam converging on it and the lit rim carry it — and being hard to see is the point, since Tower has never seen this thing lit.

D-056 — Stars are drawn a pixel wide, so the sky stops flickering

Superseded by D-075: the stars are now the real catalogue; the pixel-wide gaussian stays.

The star field was a spike: brightness (1 - d/r)^4 over a radius of about 0.11°, which on a retina panel at the default field is a core roughly one pixel across. The sky is sampled once per pixel, at the pixel's centre. So what got drawn was that spike read at one arbitrary point along it, and half a pixel of head movement moved the reading from the peak to a third of it. Every star did that independently, every frame, and the whole sky boiled whenever anyone looked around.

That is wrong twice over. Vacuum has no air to twinkle through — the sun cap window looks at deep space and the far cap at the anti-sun sky, and neither has an atmosphere in front of it. And the interior view's whole claim is that nothing in Tower moves except the pinwheel: a sky that seethes contradicts the one thing §1.4 promises about it.

The star becomes a gaussian one pixel wide. Sigma is tied to the pixel footprint, so a star is one pixel on any display and at any zoom, and a curve that smooth barely changes between neighbouring samples. This is not a compromise with the physics; it is closer to it. No optic renders a point source as a point — it renders it as a blob the width of its own resolution, and here the resolution is the pixel.

The footprint is measured from the view direction rather than from the projected star coordinates, because those carry the branch cut of an atan and a derivative across that seam is a line of garbage down the middle of the sky.

Measured, facing sunward from The Capital at night on a 2x display, turning one pixel at a time over sixteen frames: the light in star-like points varied by 8.1% of its mean before and 5.0% after, and the count of them by 5.2% before and 3.9% after. About a quarter more stars are visible at any moment — not because the field got denser, but because a star no longer has to land near a pixel centre to be seen at its true brightness. The variation that remains is mostly stars genuinely entering and leaving the frame as the view turns, which is not flicker; per star, the worst case went from a factor of three between neighbouring samples to about 16%.

D-057 — The height raster carries no mipmaps, because the vertex stage cannot choose one

Firefox on Apple Silicon rendered the interior wrongly, intermittently: the terrain lost its colour to a flat blue-grey wash and its relief to a coarse grid of creases, and it came back on a hard reload. It followed where the view was pointed rather than where the observer stood — changing eye height did not clear it — which is what made it read as a bug about rotation rather than about a texture.

The cause is a texture fetch in a vertex shader that never named its mip level. elevation() in the ground shader reads the height raster once per cell corner, in the vertex stage. A vertex shader has no derivatives, so there is nothing for an implicit texture2D to compute a level of detail from — the level is whatever the driver decides, and the driver is entitled to decide anything. The raster was loaded with Three's defaults, which means generated mipmaps, and with the card's maximum anisotropy on top: a sampler with eleven levels and an ambiguous rule for picking one. On a driver that picks badly the shell is displaced by a downsampled height field, which flattens the terrain into blocks and shows the block seams as creases, and the colour raster read at the same wrong scale averages into the flat wash. Every symptom follows, including the reload: a fresh context re-uploads and re-mips the textures.

The ambiguity is removed at the sampler rather than at the fetch. The obvious guard is to name the level in the shader, and it is not available here — the vertex-only texture2DLod does not survive Three's WebGL2 shader prefix, and asking for it fails the program link outright, which trades an intermittent bug for a permanent black world. So the height raster is built with generateMipmaps off, LinearFilter at both ends and anisotropy 1: one level, nothing to choose between.

This costs nothing. The height raster is read at a fixed rate — one texel per 0.7 km cell corner — that has no relationship to how the cell lands on screen, so mipmaps and anisotropy were never doing anything for it but occupying a third more texture memory. The colour raster keeps both, and should: it is read in the fragment shader, at every grazing angle the inside of a cylinder can offer, which is exactly what anisotropic filtering is for.

Honest about the evidence. The failure was not reproduced on demand — four hundred scripted pan steps in Firefox on the same machine rendered correctly throughout, with no WebGL warnings and no context loss. The diagnosis is fitted to the artefact: the reported view was reproduced exactly from its saved state, and the difference between what it should show and what was photographed is specifically the two rasters, not the camera, the depth buffer or the geometry. If it recurs, the next thing to drop is the anisotropy on the colour raster, which would move the suspicion from the vertex stage to the sampler state as a whole.

D-058 — The interior keeps a flight recorder

D-057 was diagnosed from a photograph. That worked, and it should not have had to: the fault was intermittent, it was reproduced by the author and never once by the tooling, and every question about it — was the geometry there, was the camera sane, were the textures still live — had to be answered by inference from a screenshot rather than by reading the number. So the interior now records what it is doing.

A ring of the last nine hundred frames, about a quarter of a minute: camera, heading, field, where the observer stands, draw calls, triangle count, and every fifteenth frame a readback of a 64-pixel block from the middle of the frame. The readback has to happen immediately after render, while the drawing buffer is still the current target; a first attempt read it from a 2D canvas a moment later and got nothing but zeroes, which looked for a while like a finding of its own.

towerDump() on the console prints the lot and copies it to the clipboard. It is present in the built site, not only in dev, because this fault has never once appeared while anybody was looking for it. It is two properties on window that do nothing until they are called.

The line that actually answers the question draws each surface alone. A flat, colourless frame can be the ground shader losing its raster, or the ground missing altogether with a cap or the bare clear colour behind it — indistinguishable on screen, entirely different faults. Rendering the ground, the two caps, the bore and the beam one at a time and measuring each separates them in a single dump. The clear colour reads 19 in chroma, so 19 means that surface drew nothing here; the ground reads about 30 when it is carrying land. Verified by taking the colour raster away from the ground shader mid-flight: ground fell from 31.3 to 19.5 while the far cap stayed at 31.3, which is exactly the signature to look for.

There is deliberately no automatic alarm, and that is a result rather than a gap. The first version warned when the middle of the frame lost its colour, on the theory that the fault paints a neutral wash. It does not. It paints the haze tint, and the haze tint is itself a blue-grey measuring 19 — while a legitimate view of the far side through 200 km of air measures about 20, against about 30 for land underfoot. Any threshold that caught the fault would have fired every time anyone looked at the horizon. The recorder therefore records and the human decides; a quarter-minute of history is long enough that noticing the fault, reaching for the console and typing still catches the frames on both sides of the change.

D-059 — Inside, the finger count is the mode

Amended by D-083: the pinch no longer zooms and the wheel moves rather than zooms; the field of view is fixed at 60°.

Until now the interior could not be moved through at all. Dragging looked around, the wheel zoomed, and the only way to change where you stood was to select a place and be walked there (D-038). That is fine for the places that exist and useless for the ground between them, and on a phone it left the whole view as something to be turned on the spot.

The gesture had to carry the mode, because a switch would have to live on the world. The alternatives were considered and dropped. A Look/Move toggle in the corner is the CAD convention and it is honest, but it is a second pill bar over a canvas that is already carrying the view switcher, the standing readout and the place captions, and on a phone that is the scarcest surface in the application (D-043). A virtual joystick is the mobile-FPS convention and reads as a game rather than a map. So: one pointer looks, two move. The count is the mode, nothing is drawn, and the gesture is the one every map on the device already uses.

The cost is discoverability, and it is paid in the panel. A gesture with no chrome is a gesture nobody finds, so the interior hint now leads with it in bold rather than mentioning it in passing. This is the known trade and it was taken deliberately.

A mouse has only ever one pointer, so shift-drag or the middle button stands in for the second finger. Without that the feature would exist only on touchscreens, which is not what was wrong.

Moving means grabbing the ground, not steering. The ray through the pointer is met with the ground plane before the move and after it, and the observer is shifted by the difference — so the patch of land under the finger stays under the finger. The point of doing it this way is that no speed has to be chosen: the distance covered falls out of the eye height and the field of view, and the same drag is a few metres of pavement at 1.7 m and a hundred kilometres of plain from 10 km up. Tower is 540 km deep and 314 km around, and no fixed rate is right at both ends of that.

Two caps keep it from flinging. A ray at the horizon meets the ground at infinity and one above it never does, so the grab distance is capped at 1,200 eye heights and at 60 km outright. Both numbers were measured rather than guessed, by dragging the running view a screen at a time at five eye heights: a screen of level drag carries 1.9 km at a standing 1.7 m, and 56 km from 100 m up, where the ceiling has taken over. The ceiling sits above the honest grab distance from the highest the eye goes — 25 km up, tilted well down, meets the ground about 60 km out — so it only ever bites near the skyline and never lags the finger on a view aimed at the ground. A first pass capped at 150 eye heights and a screen of drag moved four metres, which is how the numbers came to be measured.

The tilt is the gearing, and it was not arranged. Looking level grabs the far ground and covers kilometres; looking down grabs what is underfoot and covers metres — 26 m to the screen at standing height, tilted 23° down. That falls out of grabbing the ground rather than being a control, which is the argument for the whole approach: no speed had to be chosen and none can be got wrong.

The grab plane is flat, deliberately. Meeting the ray with the terrain instead would make the grab point jump as the ray crossed a ridge half a screen away, and throw the observer with it. The plane through the observer's feet keeps the drag smooth, and the height they actually stand at is resolved afterwards, as it always was.

Both readings of the pointer set are taken over the same set. The centroid is sampled before the moving pointer is folded in and after, never across a finger arriving or leaving — otherwise the second finger touching down would jerk the world by half the distance between the two.

D-060 — The exterior looks in at the interior's own world

Implemented at the author's request, three changes at once.

The place markers are gone from the exterior. They were coloured spheres on the hull, and they had become a crutch the view no longer needs: the sidebar lists the places, the map shows them, and selecting one still swings the exterior camera round to it (D-038). With the markers went their outline shells and their per-frame scaling. A cap is still framed from 40° off its axis, which now also means looking in through its window.

Looking in from outside now shows the interior view's world, not a sketch of it. Before this the exterior had its own inside: an emissive terrain texture on the back of the hull, no haze, and a checkered sun cap that was opaque. The interior had glazed that cap long ago (D-050), so the two views disagreed about the one surface you could see from both. The fix is not to copy the interior's look but to use it: the ground, both windows, the bore and the beam move out of interior.ts into interior-model.ts, and both views add the same meshes with the same shaders.

Two things had to change for the shaders to be looked at from outside.

The exterior turns its hull and the air turns with it, so the shaders work in the tower's frame: uFrame carries world into tower coordinates and the exterior updates it each frame after the spin. Inside, nothing turns and it is the identity.

The blue light on the collector was the fill lights, reflected. D-055 made the collector's sun face polished metal under the scene's lights, expecting a single sun glint. But the scene also carries two fill lights that exist only to make the hull readable (§2.4), and a mirror reflects them as faithfully as it reflects the sun: at any angle lined up with the bluish rim fill, a blue-white sweep crossed the disc. Reproduced by aiming the camera along that light's reflection. A standard material cannot be told to ignore some lights, so the face is now a small shader that knows exactly one: the same GGX lobe Three drew for the sun at the same roughness, over the same dark base, and nothing for the fills.

The consequence is worth writing down, because it surprised the author: the bright band that had been read as the sun's reflection was the fill light too, white at its core where it saturated. The sun is on the axis behind the collector, so its real glint only appears when looking nearly straight down that axis from sunward.

The exterior's stars are the interior's. The old field was 3,500 points 26 km wide at 18,000 km — about a pixel each, and on a retina display simply absent. The sky is now a sphere around the scene painted with the windows' own star field (D-056), sampled by direction from the camera so the stars are at infinity and hold still while it orbits. The pinwheel phase is zero, since the camera is not riding the hull, and the gain is full, since there is no daylight out here to adapt to. The same pole star sits on the deepward axis in both views.

Showing the whole sky exposed that the field was never uniform. It was laid out in an azimuthal-equidistant projection about the axis, which spends area as θ dθ dφ where the sky has sin θ dθ dφ: stars per square degree grew as θ / sin θ, correct at the deepward pole, half again by the equator and without bound sunward, where they also shrank into grey dust. The windows had only ever shown narrow parts of it. The author saw it at once from outside — sparse bright stars one way, a haze of faint ones the other. Stars are now scattered in 3D instead: a cubic lattice of cells each rolls a star, and only stars inside a one-cell shell around the eye count, which holds the same number of cells per steradian in every direction. The pinwheel is kept by turning the direction about z before the lookup, and the density matches what the old field had at its pole, so the far cap window looks as it did.

D-061 — The exterior opens on the true hull, and remembers the choice

Implemented at the author's request. X-ray was the default because, before D-060, the true hull showed little: a bare cylinder, a checkered cap, and land only through the far window. Now both caps are windows onto the hazed interior, so the true hull is the better first look, and it is what Tower actually looks like.

The choice is remembered in localStorage under tower.exteriorHull, alongside the active view (D-025) and the camera. It is restored into the Hull control at startup, before the exterior is built, because the exterior reads its reading from that control when it is first constructed. Switching to another view and back never rebuilds the exterior, so the reading survives that as well. An unreadable or unknown stored value falls back to true. The screenshot script selects a reading explicitly for each shot, in a fresh browser context, so it is unaffected.


D-062 — The world generates itself, live, from features

Decided by the author (2026-09-16), after two rounds of answers below. Full design in world-generation.md; Phase 1 is D-063.

The terrain stops being two baked images. content/geography.json and world.json merge into one world file of typed features — mountains, lakes, rivers, seas, desert, forest, city, roads and paths — and the browser generates the world from it in web workers, on load and on every edit, streaming detail in around the observer. Trees, buildings, water surfaces with waves and flow, and ships on the cap seas become real geometry.

What it reverses. D-006 chose "real geometry, simple ground" and ruled out scenery, settlements and vegetation; this adds all three. D-019's build step, its PNG rasters and its no-dependencies rule go, along with D-021, D-035, D-039 and D-057, which exist only because of the rasters. design.md §2.4's "water has no surface" and §4's "procedural scenery, settlements" out-of-scope line are reversed. These are listed in world-generation.md §11. The author agreed to all of it by asking for the build to begin.

Author's answers, 2026-09-16 (world-generation.md §12.1). Nothing inside Tower is a remnant: every landform, including the desert's dry washes and shorelines, is there because Tower chose to quote something it saw on Earth or elsewhere. Wind is Tower's published weather plan, which it revises when it likes — storms are scheduled, and cancelled if someone foolish sails into one. Settlements are generated to fill the world. Trees are real size, with a tall forest of 90–115 m giants as a landmark. A 3–8 s compile behind a Terraria-style status dialog is acceptable. Phones get the same world at full density, with shorter draw distances. Second round: days count from 0; the seed lives in the world file, so the world is identical for every reader; generated settlements are named for their size; the tall forest is placed at random near the capital once and then fixed; the status dialog lines are approved.


D-063 — Phase 1: the ground is generated, and the rivers had to be engineered

Built. world-generation.md phase 1: world file v2 and the one-off port of v1's geography; the world compile and the elevation function (src/terrain/), in a pool of web workers; the ground as a quadtree of tiles; the status dialog; keyboard flight; and the map and the x-ray hull drawn from a picture rendered live from the features. tools/terrain.mjs, its PNG encoder and noise, the three terrain PNGs, raster.ts and content/geography.json are gone. Two runtime dependencies came in, simplex-noise and comlink; the other three the plan adopts wait for the phases that need them.

One geometry field for every kind. The plan sketched kind-specific fields — area, shore, course. As built, every feature has geometry and its parameters sit beside it, so the map, the list, the editor and the permalinks needed no per-kind code. Parameters arrive with the phase whose generator reads them.

The rivers cross a lowland they cannot get out of, and the profile had to be chosen. v1 authored a 60 m lowland between the Girdle and the Dry Reach and a 800–1,100 m plateau beyond it, and painted its rivers across at about 1,000 m. Generated honestly, a river cannot do that. Three profiles were tried against the real ground:

Profile Result
clamped to the ground every river falls to 120 m in the lowland and cuts a 900 m canyon through 100 km of desert
graded curve, source to mouth a 1,300 m embankment across the lowland
least earthwork (isotonic regression, ≥ 2 m/km) The Narrows cut 300–500 m deep, as §1.11 says; up to 880 m of embankment under each river

The third is built. It is the only one of the three that is a rule rather than a guess — Tower moves as little ground as it can — and it reproduces the canyon canon already described. The embankments are the price, and they are real in the render. Whether the world should change instead (raise the lowland, or let it hold a lake) is recorded as an open question in design.md §1.11: that is a decision about Tower, not about the code.

Tiles were being measured against the tallest mountain. A fixed 4.5 km allowance for relief made every tile within 5 km of the eye look near enough to split, all the way to level 10 — 11,000 tiles on screen. Each tile is now bounded by the relief it actually has, and the split distance was measured down from 2.4 to 1.8 tile-widths: 1.9 M triangles and about 200 draw calls, settled in 1.4 s at the Capital.

A coarse tile cannot show an 80 m river. One vertex of a 500 m tile falling in a channel painted a 500 m blue diamond, and the far side of the world wore a dotted line down every river. Water now weighs by how much of the vertex's spacing it covers.

The x-ray hull had been mirrored end to end since D-024. Three's cylinder puts v = 1 at the far cap after the rotation, and the UV rewrite flipped it, so the Girdle's snow was drawn at the far cap. The committed v1 screenshot shows it. Fixed with the rest, since the picture on the hull changed anyway.

Readers pay a compile on every visit, and it is short. About 120 ms to compile and 1.2 s to draw the map, so the status dialog rarely appears on the author's laptop; it will on slower machines, and it will once erosion arrives in phase 2. A production build reaches a fully detailed interior 4.3 s after a cold load.


D-064 — The Uplands, a hull a kilometre thick, and rivers that prefer to cut

The author's call on D-063's open question: raise the lowland. A region can now carry an uplift, and The Uplands (provisional name) lift the ring of ground between the Girdle and the Dry Reach — z 105 to 150 km — to about 1,000 m. A raised region lifts the ground to a height rather than by one: it takes the higher of itself and what is already there. Added, it stacked onto the plateau's rising edge and moved the river's problem ten kilometres deepward instead of solving it; taken as a maximum, it meets the plateau and stops.

Measured before choosing, as each river's worst embankment:

worst fill The Narrows cut
v1's lowland 895–980 m ~670 m
Uplands at 1,000 m, least-squares profile 240–350 m ~390 m
Uplands at 1,000 m, 0.2-quantile profile 93–199 m ~540 m

The profile now prefers cutting to filling. Least squares treats a metre of embankment like a metre of cutting, and so balances the two; but a cut through high ground reads as a valley and a 300 m bank across a plain reads as a dam. Fitting the 0.2-quantile instead keeps the water below four-fifths of the ground in any reach. The Narrows comes out a little past design.md's "up to 480 m" at about 540 m, which is recorded rather than tuned away.

The hull is a kilometre thick all the way round. D-044 gave Tower a 1 km wall and drew it only at the caps, reasoning that a closed can shows no thickness from outside. That held while the ground never went far below the floor. Generated sea floors reach 330 m down and the tiles hang skirts below that, and with the outer skin at the floor's own radius they came through the hull as a lattice of lines (the author's screenshot). The skin now sits a kilometre out, rings close the wall at both ends, and nothing generated may go deeper than 950 m below the datum.

Two smaller faults found on the way.


D-065 — Mountains are summits, ridges and valleys water cut

Built. world-generation.md phase 2, in src/terrain/mountains.ts; the full account of what changed from the plan is §16 there. In short:

Named summits are the top of their ground, and that took four steps, because every earlier one broke the one before: scale the surroundings down smoothly if anything is higher, pin the summit, trim the few cells pinning lifted past a gentle cone, pin again. A flat cap instead left a grey disc with a rim around every summit; a single pin left neighbours 9 m above Snowcrown.

A summit's elevation is above the datum. v1 added a massif's height on top of the ground under it, so Snowcrown's 4,300 m stood at 4,447 m. Heights everywhere else in the world are above the datum, so the land under a summit is now subtracted before it is pinned, and the checks that it is the top compare total heights.

Tarns keep a shelf. Eroded honestly, the ranges ran valleys through three of v1's four tarns and dropped them to 500 m. A cirque is exactly ground the rivers did not cut, so an authored bowl lake now keeps its range at the uncut height around it. Sisters Tarn is at 2,173 m; design.md puts the ski station beside it at 2,100 m.

Regions raise ground after the mountains, not before. Lifted first, The Uplands' 1,000 m stacked under the Girdle's foot and put Sisters Tarn at 3,665 m.

One compile at a time. Dragging a summit updates the world on every pointer move, and each would have started a four-second compile. The service now runs one, keeps only the latest edit made during it, and runs that next.

Cost: a whole-world compile of about 4 s in a browser worker, most of it the Girdle. Loading to a drawn map is about 4 s; the status dialog shows for it.


D-066 — Water is geometry, and waterfalls lean

Built. world-generation.md phase 3; §17 there has the detail.

Every lake, sea and river is a surface at its own level, meshed per tile, with the ground beneath it drawn as bed and the depth test finding the shore. Waves are the wind's: fetch-limited JONSWAP seas, capped at fully developed, which is what a ring sea always is. Rivers run at Manning's speed and whiten on their rapids.

Waterfalls were not added; they were found. The river profile of D-064 pools reaches and steps between them, and a step is a fall. Merged where the steps come in a run, they give the Long Water a 680 m fall off the range with a gorge below, a 170 m fall into The Deep, and Eastwall Water a 440 m fall. The sheets drift antispinward as they drop, by the free-fall drift canon already computed: §1.9.2 said the waterfall would be the compass, and it now is one.

Three faults the falls surfaced, each a straight cliff where there should be none: the step applied across a whole valley's width (fixed by shaping walls from a smoothly ramped wall level), across a whole embankment's (the embankment uses the ramp too), and a second cliff kilometres out where a steep gorge wall's straight term fell below the valley's curved one (the gorge shares the curvature). Tile skirts now reach past each tile's steepest step, or a gorge wall's cracks showed as dark panels.

The beam went fuzzy over the seas — the author saw it. Water blends and is drawn after the ground; the beam was drawn before the water, so distant seas blended over it. The beam is always in front of water, and is now drawn last.

A river met its lake badly — the author saw that too. v1's Long Water starts at the centre of Hollow Tarn, so its channel was cut across the lake floor and drawn as a stripe across the tarn. Rivers are now trimmed to the water's edge, found by sampling the water rather than the lake's outline, which a tarn's water does not reach.

The screenshot tour was running the author's laptop hot, and deserved it: fifteen shots, four at a time, each in a fresh browser context compiling the whole world with a full pool of six workers. The compiled world is now cached in IndexedDB — which returning readers get too, as §5.4 planned — keyed so a new build never reads an old world. The tour compiles once, hands every shot the warm cache, caps each page at two workers, and reports any shot that compiled anyway. It takes about 100 s and averages about 3.4 cores.

A regression this caused, fixed 2026-09-27. Giving water render order 1 put it after the cap windows, which kept the default 0. From outside, a window is transparent and in front of the seas: drawn first, it wrote its depth, and the water behind it failed the depth test — so looking in at the sun cap showed the Sunward Sea's bed, brown, instead of the sea. See-through windows now draw at 2.5, after the water and the falls and before the beam, and the exterior's rays share that order so they still sort against the glass. D-074 moved them to 4, after the beam and the sun, so both show from outside too.


D-067 — Tower's weather is a published plan, and ships read it

Built. world-generation.md phase 4; §18 there has the detail.

The plan keeps both halves. An event records when Tower published it; a revision records when Tower changed its mind, and why. The world then has two readings of the same plan: what Tower does (the event as revised so far) and what people were told (the event as published). The waves follow the first. The ships follow the second, so a sailor warned of a storm stays in harbour even after Tower quietly calls it off — and a storm nobody was told about catches the fleet at sea. This is the author's answer to D-062 question 2 made mechanical.

Weather and ships are functions of the clock, not simulations. The interior's clock gained a day number, counted from 0 (D-062). Wind, sea state, whitecaps and every ship's position are pure functions of the world file and the hour, so a link to a moment shows the same sea to everyone, and scrubbing the clock is the same as waiting.

The sea state left the compile. Phase 3 baked waves into the compiled world; with weather that changes hourly that would recompile the world for every gust. Water vertices now name their body and the shader looks the waves up. Wind and ship counts no longer change the terrain signature.

A float32 precision fault was fixed on the way. The water shader's around-the-ring coordinate was only good to about 6 m, and whitecaps drew as streaks radiating from the eye. Water now measures from a reference near the eye that steps in whole noise periods, with each wave's phase at the reference added from float64. River ripples were suffering from the same fault unnoticed.

Simplifications, each recorded: no tacking, no harbours drawn, ships only on ring seas, flat sails. The default plan is 28 days long and repeats: storms and gales on both cap seas and The Deep, a flat calm, and one storm Tower holds down to a gale because a fishing boat ignored the plan.


D-068 — A scenic tour, and screenshots taken from it

Built, between phases 4 and 5 at the author's request. world-generation.md §19 has the detail.

Particular things want a way to be found. With a 678 m waterfall, a fleet and scheduled storms in the world, standing still and dragging is a poor way to see them, and a screenshot placed by hand at coordinates goes stale the moment the world is edited. The interior view now flies a tour of nine scenes, thirty seconds each — helicopter arcs, flights along rivers and over The Deep, chase cameras on ships — each worked out from the compiled world rather than written down, so it follows edits. The screenshot tour photographs those scenes, replacing four hand-placed shots, and a test flies all of them.

Ships can be caught out. A ship chased through bad weather needs a ship at sea in bad weather, and D-067's harbour rule emptied the sea the moment any gale was published, even one published as it began. Ships now go in only for weather announced at least three hours ahead — the time they are given to get in — and ride out anything announced later. The default plan gains one such squall on the Sunward Sea, on day 11. Tower keeping the fleet honest about its warnings is also a better story than a fleet that vanishes on cue.

A documentation fault, corrected. design.md §3.1 and world-generation.md §10 said readers were the default and ?writer=1 enabled editing. The app has always done the opposite — writer by default, ?reader=1 to preview — and the README said so. The docs now match the code; D-011's reasoning is unchanged.


D-069 — The desert is quoted, not remembered

Built. world-generation.md phase 5; §20 there has the detail.

Tower laid out the whole grammar of a desert, dry channels included. The Dry Reach now has tiered plateaus and mesas over sand, dune fields whose crests run across the wind Tower sends, rocky hills, a network of dry washes, salt pans in its low ground, and the shoreline benches of lakes that never stood. None of it is history — §0.1 — and the washes are routed by the same drainage the ranges erode with precisely because a channel network that ignores drainage is the one thing that would look wrong.

Two findings worth keeping. A flat desert has no drainage: water fans out from every edge and nothing gathers, so Tower crowns the Reach 150 m in its middle and the washes run off it. And widening a one-cell channel by blurring destroys it — dilate first, blur after.

Terracing belongs to the tiles, not to the compile. The grid stores how far up the plateau the ground stands; the tile turns that into flat tops and steep risers at its own detail. The other way round, every cliff band is one grid cell wide, which is a ramp.

Tower does not build rock across water it has already routed. Mesas dropped across The Narrows turned the world's whole drainage into a kilometre-deep slot. Rivers carry a 6.5 km corridor clear of plateau rock, which reads as the sand-and-gravel valley a river makes through a mesa field. The consequence recorded: the Long Water's canyon through the lower Dry Reach now runs about 780 m deep where the old test expected under 700, because the rim above it is the desert's rock as often as the ridge's.

Materials gained salt, and rock is now claimed rather than inferred. Bare rock came only from mountain relief before; it now comes from the desert's plateaus and wash floors and from any steep desert face, since sand does not stand at 60°. The ground shader lays strata by elevation — a level in Tower is a constant radius — so the bands line up across separate mesas, which is what makes a mesa field read as one formation.


D-070 — The Capital is a footprint, and it reaches the water

Built alongside phase 5, at the author's suggestion.

The Capital was a dot 11 km inland of the lake it was said to stand on. design.md §1.11 described it as sitting on The Deep's antispinward shore; the generated water starts 10.75 km spinward of its marker, on ground falling from 300 m to the water's 120 m. The fix the author chose is the better one: make the city big enough to be both things at once.

So a settlement is now a point with an extent — a size and a radius — rather than a marker alone. The author places where a town stands and how far it reaches; Tower lays the streets and buildings out inside that footprint (phase 7). The map draws the extent as a ring around the marker, and each size carries the usual radius a new settlement starts from.

The Capital is 20 km across, centred on the axial midpoint of the world — the only centre a cylinder has — which is just wide enough that its spinward edge is the waterfront. It holds the centre of the world and a shore on The Deep, and nothing had to move to make that true but 2 km of marker.

A canon figure corrected on the way: §1.11 said the far shore was "32 km away and 2.6 km above eye level". Thirty-two kilometres around the cylinder would stand 9.9 km above the eye; 2.6 km is the figure for the 18 km of water The Deep actually fills, which is what the crossing is. The outline is 154 × 32 km; a flooded basin's water is narrower than its outline, as it should be.


D-071 — Three billion trees, and only the near ones exist

Built. world-generation.md phase 6; §21 there has the detail.

A forest is a rule, not a list. Greatwood holds about three billion trees at 450 stems per hectare. What exists at any moment is the few thousand near the eye, and they are found by a stratified rule — a fixed grid about one tree apart, each cell hashing its own coordinates — so every tile, every view and every visitor works out the same trees without knowing about any other. Walk a kilometre and come back and every trunk is where it was.

Four ranges, and the handovers are the hard part. Meshes to about 210 m, cards to 560 m, clumps — one card for every 30 m cell of wood — to 2.8 km, and under all of it from 620 m the canopy skin: the tile's own ground vertices lifted by the canopy's height, which sinks back onto the ground as the eye comes close. The plan had three ranges ending at 760 m; the author saw the result at once — the wood ended a few hundred metres away and its edge walked along with him. Clumps are what made the reach affordable: a card per tree is a million of them at two kilometres, a clump every thirty metres is twenty-six thousand.

The skin was a black ceiling from underneath, because a solid surface is what it is. Real canopy is about a third gaps, and those gaps are the only reason you can see any distance through a forest. From below it now drops half its fragments on a ground-fixed pattern and lights the rest as light that has come through leaves.

Two numbers the plan had wrong, found by looking: a meadow's edge cannot be a fixed ±0.02 of noise (the same lesson the desert taught about thresholds), and a giant's crown cannot be 0.34 of its height — at that width a grove at 55 stems/ha closes over into unbroken black. Redwoods carry about 0.18, and 30 stems per hectare puts 18 m between trunks.

A city is not a wood. The Capital sits in the middle of Greatwood's belt, so once the forest was trees it was standing in one. Settlements now clear the forest inside their footprint — which is also the first thing the Capital's new extent (D-070) has been used for.

The Tall Wood was placed once and written down. Drawn from the world's seed under the author's rules — inside Greatwood, 15–50 km from the Capital, off the water and the rivers — it landed 17 km from the city and is now a feature in content/world.json like any other. Greatwood gives up its own ground wherever a smaller forest stands inside it.


D-072 — The world is changed in the repository, not in the browser

Decided by the author, 2026-09-26. Built.

The author writes alongside the app, not with it. The writer mode of D-011 assumed the map would be the place the world was built: draw a lake, name it, export, commit. In practice the author wants to build it by describing changes to Claude, who edits content/world.json directly — the Tall Wood (D-071) was placed that way. The editor had become a second, stale path to the same file, and a hazardous one: a browser's local draft was preferred over the committed world, so a draft from last week could quietly shadow everything added since.

So the app only reads. Removed: writer and reader modes (?reader=1 now does nothing), the Add tools, the feature editor and every per-kind control, dragging markers and summits, weather scheduling and revision, the localStorage draft and its autosave, backup and static exports, import, and "Reload published world". Everyone sees what readers always saw — the three views, the list, a detail card, the published weather board and the sightline probe — and the editor's read-only readouts moved onto the detail card: a range's summits, a lake's or sea's waves now, a forest's tree count and spacing, and what a desert's landforms came to.

content/world.json is the one authoritative copy. static/world.json is gone: the public file is made from content/world.json by a Vite plugin — served fresh by the dev server, emitted into the build — with notes emptied and unpublished features left out, exactly as toPublicWorld always did. Two committed copies had to be kept in step by hand and a test; one copy and a derived file cannot disagree. Saving the file with the dev server running reloads the page, so a change can be looked at as soon as it is made.

What stays true from D-011: no backend; the world lives in git beside the prose, diffable and versioned; notes and "public": false keep things out of the app but not out of the public repository.

A browser that used the old writer mode still holds its draft under tower.world.v2 in localStorage. The app no longer reads or writes it, and does not delete it either, so anything in it that never reached the repository can still be recovered by hand.


D-073 — Every branch is on Pages, and the app can switch between them

Decided by the author, 2026-09-27. Built.

The author wants to look at a branch in the browser without running a dev server — main and forest side by side. GitLab does this natively with parallel deployments, but they are Premium-only and this project is on the Free plan. So the default branch's pipeline builds every branch into one site (tools/build-pages.sh): the default branch at the root, each other branch in a folder named for it. A push to any other branch triggers a default-branch pipeline, so its preview follows it within minutes. That goes through the pipeline trigger API with a trigger token kept in the masked CI/CD variable PAGES_TRIGGER_TOKEN: a trigger: job cannot start a pipeline in its own project, which is how the first attempt failed (invalid_bridge_trigger). The token acts with the author's permissions and cannot be limited to protected branches, since previews come from unprotected ones; acceptable while the author is the only one who pushes. A branch that fails to build is left out with a warning rather than failing the deploy, so a broken experiment can never take the published map down.

The app has a branch picker. Every folder of the site carries a branches.json listing what was built, with links relative to that folder; the panel header shows a picker when there is more than one branch, and switching keeps the hash, so the other branch opens on the same view and place. Without the list — on a dev server — there is no picker. A branch built from code older than the picker has none either, but it is still listed in the others.

One origin, shared storage. The branches are paths on one site, so localStorage and the IndexedDB world cache are shared. That is harmless — the cache is keyed by the world and the build — but it means a remembered view or camera follows you between branches.


D-074 — Tower has a sun, and keeps Sydney's days

Amended by D-079: the rod no longer stops at the sun; it runs on to the far cap. Amended by D-089: Tower keeps Pisa's days now, not Sydney's.

Decided by the author, 2026-09-27. Built. Supersedes D-053; amends D-008.

A sun at one point on the beam, crossing the world once a day. The swarm gathers into one shoal on the axis and throws the beam's light out sideways there: a single blazing point, Tower's sun. It lights at the sun cap at sunrise, moves at a steady speed down the whole axis, and goes out at the far cap at sunset. Night follows, with the beam banked to a thousandth as D-046 has it, and the next morning the sun lights at the sun cap again. This replaces D-053's dawn front, and it turns D-008's aim — no mirrorlet may read as a sun — into its exception: Tower wants exactly one.

Sydney's days. Tower keeps an Earth city's clock and its sunrises and sunsets, and the author chose Sydney. The clock is Sydney's local time, daylight saving included; day 0 is 23 September 2026, the southern spring equinox, so the calendar starts on a 12-hour day and lengthens toward December. Sunrise and sunset come from NOAA's solar position equations, within a few minutes of the published tables (test/sky.test.ts). The city and the date of day 0 are a calendar in content/world.json, so both can be changed.

How it lights the world. Tower aims the shoal's mirrorlets so the sun is as bright seen from anywhere as from beneath it, so the ground is lit by how squarely it faces the sun, as under Earth's — not by the inverse square of a lamp 50 km up, which would leave most of the world in twilight all day. Each place has its own noon when the sun passes overhead; the sun never sets, since from one cap the sun at the other is still 5° up. The shoal takes the whole beam where it gathers, so the rod glows from the sun cap to the sun and is dark past it, and the far cap's mirror is lit only by the banked beam at night.

Consistency, inside and out, was the author's main requirement. So there is one of everything: one world clock (src/world/clock.ts), one sky computed from it (src/world/sky.ts), and one setSky that lights the meshes both 3D views share. The sun is one of those meshes, lit by the same haze integral as everything else, and every lit surface goes through one shader function, direct. The exterior used to render the inside at a fixed noon; it now follows the clock like the interior does, the windows draw after the beam and the sun so both show through the glass, and the rays from the collector dim when the beam is banked. The map marks the sun and shades the world by the same light curve.

What changed in the app. The Time controls — day, time, run the clock, its rate, and how bright Tower runs its day — moved out of the interior's options into a Time section that works in every view. The Light menu lost "Night", which the clock now brings; "Dark" still shuts the beam down. Scenic tour scenes with no reason to choose an hour are shown in the morning where they are, the sun 60 km sunward and about 40° up.

The caps have more than one name. The sun cap; the far cap, also the star cap. The author may use any of them, and more are added as they come into use.

The clock opens at the time in Sydney now (author, 2026-09-27). It starts from Sydney's current date and time, counted from day 0, and runs forward at the speed it was last run at; the speed and the Light are remembered, the time is not. ?clock=<hours since day 0> holds it at a moment, stopped, which is what the screenshot tour uses to stay reproducible.

D-075 — The stars are Earth's

Asked for (author, 2026-09-27): the sky was "sparse and random"; use the accurate night sky from Earth instead.

What it is. The Yale Bright Star Catalogue, 5th revised edition (Hoffleit & Warren 1991, CDS V/50): 9,096 stars to about magnitude 6.5, which is everything the naked eye sees from the darkest sites. tools/bright-stars.mjs writes it to src/data/bright-stars.json — position (J2000), V magnitude, B−V colour — and the app loads it as a separate chunk the first time a 3D view opens. The procedural field of D-056 is gone; both views still share one STAR_GLSL, so the sky out of the far cap and the sky round the exterior are one sky.

Where it points. Tower's axis points at the sun (§1.6), so the sky it sees is set by the sun's ecliptic longitude, from the clock's real instant (Sky.at). The far cap looks at the antisolar point; it walks the ecliptic a degree a day, so the pinwheel's centre is in Pisces in late September and near Orion's feet at the December solstice. Roll about the axis is the spin inside; outside nothing fixes it, so ecliptic north is up (scene +Y), the one choice square to the axis every day of the year. Precession since J2000 is applied. src/world/celestial.ts holds the maths and test/celestial.test.ts pins it: the sun where the almanac has it at the 2026 equinox and solstice, the frame a rotation and not a mirror, and Orion beyond the far cap in December.

How it draws. A pixel cannot look at 9,000 stars. They are baked into cells on the six faces of a cube (256 a side, each face bordered by its neighbours' stars), and a pixel reads the nine cells round its own. Each star is still the pixel-wide gaussian of D-056, which stays still rather than flickering; brightness goes as flux^0.8, and the brightest grow a little wider. Colour follows B−V, kept pale, since starlight is too faint for much colour vision.

The motionless star is not canon (author, 2026-09-27). §1.4 promises "one motionless star at the middle" of the pinwheel. The real sky has no star at the antisolar point, and it moves a degree a day, so no real star could hold the place. The placed star at the centre stays for now and is on the backlog (design.md §5) to revisit; removing it is one line.

D-076 — Trying other outsides: rock

Asked for (author, 2026-09-27): the outside is very uniform; try different approaches, starting with "a rocky surface, almost like someone tried to make it look like an asteroid".

An experiment, not canon. It is a third reading in the Hull switch — True, Rock, X-ray — so it can be compared with the bare skin and kept, changed or dropped. Nothing in §1 changes. A shell kilometres thick round a 540 km cylinder would be over 10¹⁸ kg of rock on a spinning hull, which canon would have to answer for if it stays.

The shape. A shell laid over the metal skin, never cut into it: at least 0.2 km proud of the wall's outer face everywhere, so nothing inside can show through, and the windows are left clear, with rock heaped round their rims. Lumps a few kilometres high and 1,400 craters from 0.8 to 16 km across, a power-law population with raised rims, in the geometry (src/views/rock-hull.ts, ~0.7 km between vertices); grit, pits and colour mottling below that in the shader, bump-mapped from noise in the hull's own frame and faded out as a pixel grows wider than the detail. Seeded, so it is the same rock on every load. Clicks still land on the smooth skin, hidden, so a picked point is the canon point beneath the rock. The rock is lighter than a real carbonaceous asteroid's charcoal, under the same licence as the fill lights.

D-077 — Trying other outsides: piping

Asked for (author, 2026-09-27): "something industrial like the Alien movies, with panels and pipes and exhaust vents, only on a huge scale." The second experiment after D-076's rock, and like it a Hull reading labelled as an experiment, not canon.

What is on the skin (src/views/pipe-hull.ts): - Plating, in the shader of the hull cylinder itself: 40 panels round by 8 km along, some split in two or four, each its own shade, some ribbed, with grime streaked along the axis. The seams are bump-mapped grooves that widen with the pixel, so they fade with distance instead of aliasing. 40 round, so the plating closes on itself exactly. - Bands every 30–42 km: a collar 2.6 km wide and 1.1 km proud, with a hoop pipe either side. They divide the 540 km into stations, as a ship's frames do. - Pipe runs: fourteen lanes of two to five pipes, 0.6–2.2 km across, running from band to band on brackets, and skipping some stations so runs start and stop. - Plant: 170 clusters of blocks, the odd one a tower up to 6 km, kept off the pipe lanes and the bands. - Vents: 64 housings, each with a grille and a plume that fire at random, on separate clocks (author: "the plumes are constant, let's make them fire at random", and the same for the lights). A grille is lit 1–7 s at a time and an ember between; a plume shoots out for 1.5–4 s, then is gone for 2–12. There is no air to hold a plume out here; it is the look asked for, under the same licence as the beam's rays. - Thrusters ("larger fires, light them less often"): six pods at each end, in 12 km kept clear for them, each with four bells — out, spinward, antispinward, and along the axis away from the world. They are what §1.6's never-ending reorientation would need. A pod burns one bell at a time for 2–5 s, then waits 25–80 s. The flame is 28–42 km long and fans out as it goes, since exhaust in vacuum has nothing to hold it together; the spread grows as the square of the distance, so a sideways flame stays clear of the plating it runs along. Two point lights follow the two brightest burns and light the hull round them — one per bell would recompile every material for 48 lights, and more than two at once is rare.

Dust (author: "it's too shiny, let's add a nice layer of dust"). Every piping material — plating, bands, pipes, blocks, pods, bells — goes through dusty(), which lays a dust colour over the surface and makes it fully matte and non-metallic where it lies. Bare metal under the fill lights read as chrome. Dust is thickest on faces turned outward, the way everything on a spun hull is thrown, thinner on the sides and nearly absent underneath, and broken into drifts by noise. It is worked out in the hull's frame from the vertex, so instanced parts get it too.

Timing is on the CPU. A small scheduler, Bursts, holds each emitter's on/off clock and a level that rises over an attack and falls over a release, so nothing snaps on or off. The shaders only read that level per instance. The lights need to know which bells are burning, and keeping one schedule in JavaScript is simpler than mirroring a hash in GLSL. It is tested in test/pipe-hull.test.ts, including a long pause such as a hidden tab.

What it must not do. Nothing is inside the skin (test/pipe-hull.test.ts checks every vertex), and the bands are open drums with ring faces, never closed cylinders, whose discs would stand inside Tower and be seen through the windows. The plating is two-sided so that, from inside the tube, it hides the far side's parts wherever the ground has not been generated yet. All the parts are instanced: five draw calls for a few thousand pieces. Seeded, so it is the same plant on every load, and built only when first chosen.

D-078 — The cap windows are framed in identical triangles

Asked for (author, 2026-09-27): "the endcaps have circular window patterns. please change these to be identical triangle pieces."

Both caps' mullions are now a lattice of identical equilateral triangles: three families of straight members at 60° to each other, spaced one triangle height apart, with a vertex on the axis where six triangles meet. The rings and spokes of the polar tessellation (D-041, D-045), including the heavy ring at 25 km, are gone. The side is 8.3 km — the old cells' pitch, six across the radius — so the frame keeps its job as the one object in the world with a known size (D-041), and it is coarse enough to survive the near edge-on views from outside (D-030).

A disc cannot be tiled by whole triangles, so the rim cuts the outermost ones; the heavier rim mullion closes them. The far cap's mirror sits on the centre vertex, and the sun cap's aperture on the same point of its own lattice. test/cap-pattern.test.ts pins the lattice: three directions 60° apart, every line a whole triangle height from the axis. The polar checker drawing, which nothing had used since both caps became windows, was removed with it.

D-079 — The beam runs from cap to cap, and the sun moves along it

Asked for (author, 2026-09-28): "can we keep a permanent light beam in the sky (from cap to cap) and the large sun still moving from cap to cap? … i'd like the beam extend all the way, not just from sun cap to the sun."

Under D-074 the sun's shoal took the whole beam where it gathered, so by day the rod glowed from the sun cap to the sun and was dark past it, and the far cap's mirror was lit only at night. Now the shoal takes part of the beam and the rest runs on: the rod is one line of light from cap to cap at every hour, the sun still crosses the world along it, and the rod always ends on the lit mirror at the centre of the far cap (D-048).

How much gets past. BEAM_PAST_SUN = 0.7 in SUN_GLSL: past the sun the rod is 70% of itself, so the sun still reads as where the beam is being taken, and the mirror by day is lit by that same share. One constant for both, so the rod and the mirror it ends on agree. Night is unchanged: the banked beam runs the whole length evenly.

What did not change, and should be settled. The light on the ground is still worked out as if the sun took the whole beam (direct), so the world is exactly as bright as before. Strictly, a sun taking 30% of the beam would light it less. That is on the backlog (design.md §5): pick the split, then the light budget follows from it. The map, which does not draw the rod, is untouched.

D-080 — A dead terrain worker no longer freezes the world

Reported (author, 2026-09-28): now and then, over several days, the page froze on "Teaching Eastwall Water which way is down", with no clear console errors.

What it was. That label is the compile's last step, not a slow one: the whole compile takes about 5 s, and that step 3 ms. After it, the page sent the compiled world to every worker in the pool and waited for all of them, with no time limit. A worker that never started never answered, so the wait never ended — and a comlink call to a dead worker fails silently. The only trace was Firefox's warning "Attempting to create a Worker from an empty source", which the author's screenshots showed a dozen times: Firefox had been handed an empty script for a worker and started it with nothing to run. Why it sometimes gets an empty script was not pinned down (twenty clean loads and eighteen with files changing mid-load all worked); a page that made one worker silent on purpose reproduced the freeze exactly, on the same label, with the same warning and nothing else.

What changed (src/terrain/service.ts). Before a world is handed out, every worker must answer a ping within 15 s. One that does not is terminated and replaced once; if the replacement is silent too it is dropped, and the pool carries on smaller. Handing over the world has a 30 s deadline per worker on the same terms. Each of these says so with a console.error naming the worker, and a worker's own error event is logged too, so the next time this happens it is visible. Only an empty pool is fatal, and it says so.

Checked in Firefox with the same sabotage: one silent worker now costs about 15 s and a console error; a silent worker with a silent replacement costs about 30 s and loads with five; a normal load is unchanged.

D-081 — Trying other outsides: rock and piping together

Asked for (author, 2026-09-28): "i like the outside rock and pipes views, do you think we can combine them? have rock show through, and have pipes overlaying in other spots." Offered three readings: rock heaped on the plant as shielding, the plant built on top of the rock, or Tower half dug out of an asteroid. Chosen by the author: shielding — "it seems like a reasonable approach to creating Tower". A fifth Hull reading, Rock and piping, an experiment like D-076 and D-077, not canon.

Why the two together, and not just either. The rock alone has nothing in it to give scale — a crater could be sixteen metres or sixteen kilometres. The plating alone is regular, which was D-076's complaint in the first place. Each fixes the other: a pipe run that goes into a bank of rock and comes out ten kilometres on says how big both are.

One cover field decides where the rock is (rockCover in src/views/rock-hull.ts): seeded noise over the hull, 0..1, rock wherever it is at least 0.5. Two scales, 80 km and 22 km, so it lies in banks and drifts tens of kilometres across with bare ground between. It first covered about 43% of the hull; the author asked for "a slightly higher concentration", and it is now about 60%. - The rock is D-076's shell scaled by the cover, rising from 0.2 km to full height over a narrow band of it so each bank ends in a scarp. It is 1.6 times D-076's height here (author: "the rock feels like it can be taller, at least another 50%") — banks with bare plating beside them need the height to read as heaps. The Rock reading on its own is unchanged. A first try that ramped up slowly left the edges a thin feathered skin that read as smoke, not rock. The cut is made per pixel from the cover carried on each vertex, broken up by noise finer than the mesh, so the shore of each bank is ragged rather than the mesh's 0.7 km grid. - The plant — blocks and vents — goes only on bare ground, a little inside the edge. Each plant room is centred on bare ground, so they crowd into the gaps rather than thinning out. The bands and pipe runs are laid as before and go under the rock: that is what makes the rock read as heaped on rather than the plant as built round it. - The thruster pods are dug clear, 14 km round each, with a sloped edge: a sideways flame runs along the hull, and would otherwise burn into the rock.

Built separately from the other two. The plant crowds differently and the rock is dug out, so neither half is the same as the dress on its own; the combined one is its own pair of meshes, built the first time it is chosen. With nothing under rock, the piping builder makes the same draws from its seed as before, so the Piping reading is unchanged (tested).

Canon. Nothing in §1 changes. The shielding reading gives D-076's rock a reason, and rock over three fifths of the hull at 1.6 times the height is about the same mass as D-076's shell — still over 10¹⁸ kg, so still a question for §1.2 if this stays (design.md §5 Backlog). Real habitat studies do bury the hull under regolith for shielding, but metres of it, not kilometres.

The plating's moiré (author, 2026-09-28: "the repetitive pattern in the pipes creates moiree lines — circle patterns"). Seen from a glancing angle near the far cap on a high-density screen: rings of interference on the plating. The ribbed panels were the cause. Their ribs are rings round the hull 0.9 km apart, and they faded out only once a pixel was most of a rib wide — so they were still drawn at full strength while one to three pixels apart, which is exactly the band where a regular pattern beats against the pixel grid. They were also faded by the pixel's overall size, when what matters for rings is how fast z runs across a pixel, which at a glancing angle is several times more. The ribs now fade by that rate, and are gone before they are three pixels apart. The same reasoning is applied to the two other regular patterns in D-077's plating: the panel seams fade out once a panel is a few pixels across, and the hard-edged grime stripes are averaged away as they near a pixel's width. Close up, nothing changes. This is D-077's plating, so the Piping reading gets the fix too.

D-082 — Eye height is a gauge in the view, marked with things of known height

Asked for (author, 2026-09-28): "please make the bottom-left panel collapsible", and "move the eye height selector out of the right panel, and put it instead as a vertical height, and indicate comparable heights when we reach those heights: average adult, average home, eiffel tower, airplane cruising altitude, etc."

The gauge (src/ui/eye-gauge.ts) runs up the right-hand side of the interior view, from 1.7 m at the bottom to 45 km at the top (48.4 km, the beam, since D-084), on the slider's own logarithmic scale — so a person and a weather balloon both get room. Eleven marks, all real-world, so none needs explaining: an adult (1.7 m), a two-storey house (8 m), a ten-storey block (30 m), the Statue of Liberty (93 m), the Eiffel Tower (330 m), the Burj Khalifa (828 m), Mount Fuji (3.8 km), Everest (8.8 km), an airliner cruising (11 km), Concorde cruising (18 km) and a weather balloon (35 km). The gaps between the author's examples were filled so no stretch of the scale is empty. Marks brighten as you pass them and turn accent-coloured when you are level with one — within a tenth of its height. The readout by the thumb gives the height, the local gravity (which the old slider's label carried), and the comparison: "≈ the Eiffel Tower" when level, otherwise a multiple of the highest mark passed, "2.0× the Eiffel Tower".

A widget of its own, not a rotated <input type=range>: the marks must sit exactly level with the thumb, and a native range insets its thumb by an amount each browser picks. It is still a slider to the keyboard (arrows, Page Up/Down, Home/End) and to screen readers (role=slider, with the comparison as its value text). On a short view, a mark too close to the one below it keeps its tick and drops its name — Everest and the airliner are the pair that collide first. On a phone the names go altogether, and the readout by the thumb says which mark you are near. It follows the view as the slider did: flying with E and Q moves it, except while it is being dragged.

The standing readout folds to its title, "Where you stand", with a click on it; the rest of it stays click-through, as every overlay is. The fold is remembered. On a phone it starts folded, since open it covers a third of the view, and it now stops short of the gauge.

D-083 — No zoom inside; the trackpad swipe moves

Asked for (author, 2026-09-28): "the current interior docs state that two fingers move, but i'm seeing that two fingers changes the zoom. can the zoom be hard-coded internally, and can we get the motion working instead?"

Why it zoomed. D-059 made two touches move the observer and pinch the field of view, and the wheel zoom. On a Mac trackpad, a two-finger swipe is not two touches: the browser delivers it as the wheel. So the gesture the panel described went to the wheel handler and zoomed, and the move D-059 built was reachable only on a touchscreen or with shift-drag.

The field of view is fixed at 60° (FIELD_DEG in src/views/interior.ts). Pinch no longer zooms, the wheel no longer zooms, a saved field is no longer restored, and the readout's field line is gone, since it could only ever say 60°. The scenic tour still frames its own shots at its own fields; stopping it puts 60° back. A trackpad pinch arrives as the wheel with ctrl held, and is swallowed rather than zooming the page.

The wheel moves. Not as a grab of the ground under the cursor, which was the first try: it matched the touch drag exactly, but a trackpad's cursor sits wherever it was left. Measured from standing, a 300 px swipe moved 2 m with the cursor near the bottom of the screen and 99 m just under the horizon; and above the horizon every ray is held to the same capped reach, so a swipe there barely moves at all. The travel depended on where the cursor happened to be, which is no way to walk. So a pixel of swipe is a fixed fraction of a second of keyboard flight, which already scales with height: 1/25 s, so a screen's swipe walks about 70 m from standing and some 40 km from a kilometre up, wherever the cursor is and however the head is tilted. It walks along the heading, in the same direction as the touch drag — the ground follows the fingers, so swiping down goes ahead, as the page does under natural scrolling. No shift boost, because browsers turn shift-wheel sideways.

Captions pass the wheel on. They take the pointer so they can be clicked, and one sits on the skyline more often than not; a swipe over one did nothing until the caption layer was given the same wheel handler.

Touch is unchanged apart from the pinch: two fingers still grab the ground and drag it. Their centroid is what moves, so a pinch — fingers apart about a still centre — now does nothing at all.

D-084 — The top of the eye-height gauge is the beam

Asked for (author, 2026-09-28): "add 'sun beam' or whatever the technical name is to the chart, since that's the top point we can go to." Canon's name is the beam (§1.3): the column of concentrated sunlight on the axis, 3.18 km across, glowing where it runs through the air.

It was not the top, so it is now. The ceiling was 45 km above the ground, 3.4 km short of the beam's edge, which is 48.4 km above the datum (the 50 km radius less the beam's 1.59 km). The ceiling is now that edge: EYE_MAX_M = RADIUS_M - BEAM_RADIUS_M in src/ui/scales.ts, derived from canon rather than typed in, and the gauge's last mark, "the beam", sits at the top of the scale. The interior stops the eye 300 m short of the edge over whatever ground is underfoot, so on high ground the ceiling comes lower and the observer can never be inside the beam. From there it is 80° wide overhead and looking up is white — a brilliantly sunlit cloud, 300 m off, as §1.3 describes it.

The gauge's gravity now counts the ground, as the standing readout's always did; the old slider's label measured from the ground up and read 0.044 g at the top where the readout said 0.038 g.

D-085 — The scenic tour flies the world's flyers, inside and out

Asked for (author, 2026-09-28): automatic flight, to show Tower off inside and out — a flight plan put together at random from components, a camera that points at interesting things as it goes, about fifteen seconds before changing from one flyer to another, and every point of view belonging to something in the world: paragliders, planes, ships, spacecraft, trains. Planned before it was built, and the plan was agreed with these answers:

Built. world-generation.md §19 has the detail.

A leg is a craft on a path, and a camera on the craft. Twenty-six inside — a helicopter round the Long Water's fall and a paraglider across its face, ships in fair weather and in a squall, a paraglider on a summit's flank, a glider in a thermal over the Dry Reach, light aircraft down rivers and through The Narrows, a seaplane across The Deep, an airliner round the Girdle, a giant airship climbing out of the Capital and another circling the sun, a night flight toward the far cap, trains on both railways (D-086) — and nine outside, all spacecraft: skimming the hull, circling it against its spin, docking on it, looking in through the far cap, crossing the sun cap, over the collector's rim, down the beam, pulling away, and Tower, the collector and the sun in one frame. The camera rides in the craft, or behind it on the far side from what it is looking at, so the craft and its subject share the frame.

The camera is pointed by a director, not by hand. Everything worth a look — summits, falls, lakes, the Capital, the Tall Wood, the sun, both caps, ships — is scored from the craft by how big it looks, how clearly it can be seen through the air (the same integral of the density profile the renderer hazes with), whether the ground hides it, and whether a pilot could look that way. The leg's own subject wins most arguments; anything looked at for long starts to bore it; turns are eased over three seconds and never swing faster than 0.7 rad/s. So the tour does not stare at haze, and the far side of the world gets looked at, because §1.8 says it is the clearest thing in the sky.

The deal is a deck. Every leg is flown once before any is flown twice, no two in a row are the same kind of craft, two legs about the same subject are kept apart where they can be, and a new deck never opens on the craft the last one closed on.

Some legs are time-lapses, and say so. A giant airship takes minutes to climb ten kilometres and circles the sun at 28 m/s, so those legs run the world's clock ×20 to ×150, and the caption says "time ×150". Everything else is real time.

What the numbers say, the captions say. Moving round the world changes what you weigh (canon/motion.ts apparentGravity): the Girdle's airliner, flying antispinward at 230 m/s nine kilometres up, weighs about a third of what it would at rest, and a train on the Uplands Ring is 31% heavier one way round and 27% lighter the other. Every circumferential journey in Tower has a handedness, as every slope does for a skier (§1.11).

Outside, the hull turns at the leg's rate — real time — whatever the Speed slider says, so a craft skimming it sees 700 m/s of hull go by and not a blur. A leg flies in one of two frames: space, where the hull wheels past, or the hull's own, where it holds still and the stars wheel, which is what docking looks like. The near plane follows the nearest thing, so a craft 400 m off is not clipped.

D-086 — Two railways: the Axial Line and the Uplands Ring

Asked for (author, 2026-09-28): trains in the tour, with the lines "the ones you find reasonable". There were no railways in the world, so there is now a railway kind in world.json — a line, with a line speed — and two of them, chosen and laid by Claude:

The track keeps its own grade, over the ground. terrain/rails.ts lays the deck at the highest ground within a kilometre either side along the line, eased over 900 m, so it is never below the ground and a train never runs through a hill. Tower does not cut the ground for it. The cost of that is honesty about viaducts: the deck stands 10–20 m up on piers over most of both lines, and up to 260 m where the Ring crosses a gorge. The steepest grade is 7%, where the Axial Line comes down off the plateau into Greatwood; all but 23 km of it is under 3%, and the Ring never passes 3.7%. A kilometre window was measured against 400 m and 1.5 km: shorter made grades of 17–28%, longer made the whole line a viaduct.

The map draws them as lines in their own colour, and the interior draws the deck, its girders and a pier every 100 m wherever it stands clear of the ground.

D-087 — Where spacecraft dock, and where aircraft may not go

Canon, from the author (2026-09-28), answering questions the tour raised:

Consequences worked out with it, not canon until the author says so: the giant airships live near the axis because only there do they have to. At 44 km up the air is about a twentieth of the floor's density and weighs about a tenth, and what a gasbag lifts per cubic metre depends on the density of the air it displaces, not on gravity — so an airship there needs some twenty times the volume for the same load. Hence giants, 420 m long, and slow.

D-088 — Tower has a moon

Asked for (author, 2026-09-28): "a 'moon' will travel from night cap to the sun cap over the course of the night. The brightness of the moon will be based on the moon phases, so this will give Tower a moon of sorts." The night cap is the far cap: night starts there, when the sun goes out at it (the cap names are in §1.4).

Built. design.md §1.3.5 has the canon.

What it is. A second gathering of the swarm on the banked beam, by night, the size of the sun. It lights at the far cap at dusk — SUN_RAMP_H, the same quarter hour the sun takes to go out — runs at a steady pace to the sun cap, and goes out there at sunrise as the sun lights. The sun crosses the world one way by day and the moon the other way by night, so there is always one of them on the beam except at new moon. With the beam shut down (the Dark setting) there is no moon either.

Its phase is Earth's Moon's, worked out for the moment on the clock (world/celestial.ts moonPhase, Meeus's low-precision phase angle, checked against the almanac's new, full and quarter moons). Day 0's night, 23 September 2026, has an 89% waxing moon; it is full on the 26th in Pisa (the 27th in Sydney, which Tower kept when this was written) and new on 11 October. The disc is drawn with its terminator, lit toward the far cap while it waxes and toward the sun cap while it wanes — where the sun set and where it will rise, as on Earth.

How bright, and why that. The brightness had to be chosen, because the honest one would be invisible: Earth's full moon lights the ground at about 0.25 lux, and Tower's banked night is already some 150 times that. So Tower's moon is sized to its own night: at full it throws as much light on the floor as the whole banked rod (MOON_AT_FULL_OF_NIGHT = 1), and a moonlit night is up to 0.64 W/m² against a moonless 0.32. It is lit from where the moon is, not from the whole rod, so it has a direction: slopes facing it are brighter, and seas carry a path of light toward it.

Everywhere the sun is, the moon is. The panel's time section says where it is and how lit, the interior's readout gives its height and distance as it does the sun's, the map draws a pale line where it is overhead, the exterior sees it in through the windows, and the scenic tour's director will look at it by night. The tour has one new leg for it: a seaplane crossing The Deep by moonlight, on the first night of the month with the moon at least 60% lit.

A canon question it raises, not answered here: whether the moon takes its light from the beam as the sun does — the rod dimmer past it — or is fed by Tower lifting the banked beam, as written above. The second is what is drawn.

D-089 — Tower keeps Pisa's days

Amends D-074.

Decided by the author, 2026-09-28: "for reasons, Tower has actually decided to set its internal 'time' to match that of Pisa, Italy." Built.

What changed. The calendar in content/world.json is Pisa's: Europe/Rome, 43.7228° N, 10.4017° E. Tower's clock is Pisa's local time, Italian daylight saving included (CEST, +2, until 25 October 2026; CET, +1, after), the app opens at the time in Pisa now, and the sun rises and sets with Pisa's sunrises and sunsets. The fallback calendar in src/world/schema.ts, used when the world file's is missing or unusable, is Pisa's too.

The clock and the days stay together. D-074 makes Tower keep one city's clock and its sunrises and sunsets; changing the city changes both. So Tower's seasons turned over: day 0 — still 23 September 2026 — is now the northern autumn equinox, and the days shorten toward December instead of lengthening. The day runs 12 h 11 min at the equinox (the sun at 44 km/h), 8 h 55 min at midwinter (61 km/h) and 15 h 27 min at midsummer (35 km/h), against Sydney's 9 h 54 min and 14 h 24 min.

Tests. Sydney's published sunrise tables still check the solar maths, on a Sydney calendar of their own; Pisa's day lengths, daylight saving and clock have tests of their own.

One knock-on in the tour. The ship leg starts in the morning over its sea, and the morning moved; at the new hour the director looked up at the sun, which put the chase camera below the ship, and the sea pushed it back up beside the ship, looking past it. A ship's chase camera now looks no more than 20° up (maxPitch 0.35 in shipLeg).

D-090 — The far end is cold: ice at the far cap, a frosted window, and Tower sets the hull's heat loss

Amends §1.9.4, which said nothing breaks Tower's thermal uniformity.

Worked out with the author, 2026-09-28, starting from a question: if Tower sent no light to the sea by the far cap, would it freeze? Canon only; nothing drawn yet.

What the question found. Two things canon had missed.

Decided by the author:

  1. Tower controls the hull's heat loss, region by region, up to about 5 TW — a tenth of the beam. "Not infinite, but some control." Light makes the weather; heat rejection is how Tower keeps the books.
  2. The far end is kept dim and freezes in bands, sunward to cap: open water z 496–512 km, pack ice 512–525, fast ice frozen to the cap 525–540, then the window.
  3. The rod stays as D-079 has it, running past the sun to the cap. So the fast ice gets dusk by day, tens of W/m², not darkness — "no big deal if we don't have complete darkness". Stars by day, which a truly dark end would have given, are given up with it.
  4. The sun still goes to the cap (D-074), so it goes out over the ice each evening.
  5. The far cap is single-glazed and frosts. Tower clears it one pane per turn of the world, 7 min 29 s, in a random order that is reshuffled every loop but always the same for a given loop. 282 panes (216 whole, 66 cut by the rim) make a loop of 35.1 hours.

Consequences worked out, now in §1.9.6:

Numbers that are rough: the window wind (5–15 m/s) and the frost rate (millimetres a day, to a factor of ten). The frost rate decides how many panes are clear at once; one in ten is a guess.

The sun cap is double-glazed (author, 2026-09-29). Single glazing there would have done all the same things — radiated, frosted, and sent a cold wind onto the Sunward Sea. Double, its inner pane stays near the air's temperature, so the Sunward Sea stays temperate and the far end is the world's only cold place.

D-091 — Drawing the far end: sea ice, the dim, and the frosted panes

Builds D-090. Asked for by the author, 2026-09-29: "please commit and implement". Also recorded here: the author chose double glazing for the sun cap (D-090), so the Sunward Sea has no ice and needs nothing drawn.

The ice is in the world file. A sea may carry ice: { edgeM, fastM } — the ice edge and where the fast ice begins, as z — and the Deepward Sea does: 512 and 525 km. The fast ice runs on from fastM to the end of the sea beyond it, whichever end that is. It is part of the terrain signature, so changing it rebuilds the tiles, as it must: every water sample carries its ice cover (terrain/ice.ts, field.ts seaIce).

The dim. Tower's aimed light — the sun's and the moon's — falls from the open water's shore (the sea's end away from the ice) to 0.35 at the ice edge, 0.1 at the fast ice, and nothing on it. The rod is never aimed, so by night nothing changes but the moon, and by day the fast ice keeps the rod's glow, taken as a twentieth of the sun's light (ROD_GLOW_BY_DAY — tens of W/m² against about a thousand). Each lit surface is multiplied by its share put through the same 0.35 power the exposure gives the light dial: an eye standing in the dim has adapted to it, and at a straight twentieth the fast ice drew as a black hole. So it reads as dusk, a third as bright as elsewhere. The stars go by the light where the eye is, so from the fast ice the brightest of them show at dusk.

The frost (world/frost.ts). The window's 282 panes are named by lattice cell and half, the same triangles the mullions draw. Tower clears one per turn of the world (7 min 29 s) in an order shuffled by the world's seed and the loop's number, reshuffled every 35.1-hour loop; a cleared pane frosts over again as 1 − e^(−age / 10 h). Ten hours is chosen to leave about one pane in ten nearly clear, §1.9.6's guess — the frost rate is rough to a factor of ten, and this is the knob for it. The pane being cleared thaws across its turn. The clock writes the frost into a 32 × 32 table the far cap's shader reads, so both views, inside and out, show the same panes clear at the same moment. Frost is feathered rather than a grey sheet, and lit by the light at the cap, which is the fast ice's dusk.

Left open (design.md §5): the sun is drawn as bright from the fast ice as from anywhere, though Tower aims none of its light there.

Tests (test/far-end.test.ts): the bands and their cover, a sea with its ice the other way round, ships clear of the ice, the dim by day and by night, 282 panes (216 whole) found from points inside them, a loop of 35.1 hours, every pane cleared once a loop in a reshuffled order, a pane thawing across its turn, no jump between loops, and about one pane in ten clear.

D-092 — A dead terrain worker is replaced at once, not after 15 s

Amends D-080.

Reported (author, 2026-09-29): "every so often, the page freezes on the loading screen and there are errors in the console. a reload fixes it, or just waiting." The console showed Firefox's "Attempting to create a Worker from an empty source" many times, and three workers failing with an error event.

What it was. D-080 made a silent worker survivable but slow: it was given 15 s to answer its ping before being replaced, so a load with a dead worker sat on "Reading the plans" for 15 s or more — the "just waiting". Workers die two ways, and both were waiting out that deadline:

Why Firefox is handed an empty or broken script is still not pinned down. It was not reproduced by fifteen plain reloads, nor by changing files mid-load. The likeliest cause is the dev server answering while it rebuilds after an edit: the author reported it while files were being edited under an open page. So this is a cure, not a prevention.

What changed (src/terrain/service.ts):

Checked in Firefox, with the worker's script replaced by Playwright for the first few requests:

Sabotage Before After
none 7.4 s 7.4 s
three scripts that throw 22.8 s 7.7 s — no delay
three empty scripts 22.3 s 11.1 s
all six empty, twice over — 38.6 s, and all six start on the third try

D-093 — Ice mountains at the far cap: the Cap Range

Asked for (author, 2026-09-29): "the ice at the far cap should slope up, since the snow/ice would build up closer to the window. i'm imagining ice mountains that go a bit up the window, even."

Why ice alone was not enough. The ice is thickest against the window — snow off the window wind and the meltwater of every cleared pane (D-090) come down at the cap's foot — so it should rise toward the glass. But grounded ice spreads under its own weight until the stress at its bed is what the bed will take, which caps it at the plastic profile of an ice sheet, H² = H0² + (2τ/ρg)·x. With τ = 150 kPa, the top of what ice sheets show, it has to fill 420 m of sea before any of it stands out, and over 8 km it reached about 280 m above the water: built and drawn, it read from the pack as a pale strip under 1° high at the foot of a 50 km window. Three ways on were offered — keep that, raise the sea bed (500–700 m, still a kerb), or a rock range under the ice. Chosen by the author: the range.

What it is. The Cap Range (provisional name): rock Tower raised at the foot of the far cap for the ice to stand on, as it raised the Uplands (D-064). It is part of the Deepward Sea's ice in the world file — ice.range: { fromM: 524000, heightM: 2400 } — and a label feature, a region band from 528.5 km to the cap, carries its name.

Tests (test/far-end.test.ts): the rock from bed to height, steepening; the ice's margin at the waterline and its mantle thickening to 350 m; and on the compiled world, all round the ring, dry ground 1.5–4 km high at the cap that varies, fast ice at 526 km, open water at 505. test/terrain.test.ts counts the new feature and probes the Deepward Sea short of it.

D-094 — The tour shows how long a leg has left: its caption's border fills in

Asked for (author, 2026-09-29): "for the tour, it would be nice to have an indicator of 'we've been here 11 second, 3 more until transition', but preferrably in some intuitive way, like the border of the tour text filling out or collapsing or something."

What it is (views/tour-player.ts, style.css .tour-ring). The caption's border fills in clockwise from its top-left corner as the leg runs, in the accent colour over the caption's own faint border, and closes as the leg starts to fade to the next. The next leg's caption starts empty. No numbers: a clock read at a glance, the way a progress ring is. It is drawn as an SVG rectangle over the caption's border, its dash measured in pixels of the border's own length, which follows the caption's size as its text changes. Both tours, inside and out, have it.

D-095 — The Fields: farmland where each crop is lit on its own day

Asked for (author, 2026-09-30): "please add farm fields to the model. these should be realistic fields, in their perfect bloom, so colorful. and this will be a new thing, but these fields will receive 100% sunlight, even at night."

The conflict, raised before building. Tower's night is made by banking the beam, because the rod's glow cannot be aimed away (§1.3.2). Fields lit at full sun all night need the beam up, and the whole world gets the rod's glow with it: 250 km² of fields would add about 0.5 W/m² everywhere, all night; 1,000 km², about 2 W/m², a permanent twilight. The author chose 250 km², at z 430–496 km (the unauthored belt by the far end) — and, asked how the flowers stay in bloom, changed the brief: "flowers get darkness, and other things (corn, potatoes, tomatoes, etc.) will have appropriate timings. i thought 100% daylight would be helpful for plants, but if it's not, adjust as appropriate, since Tower can provide whatever lighting."

What it is.

The night it costs. At the equinox about 91% of the fields are still lit at sunset — 220 GW, 0.4% of the beam — adding about 0.4 W/m² of rod glow everywhere, falling to 0.2 at two hours, 0.1 at four, and nothing at six. This is canon but not drawn: the world's night light in the app does not yet include it (design.md §5).

Tests (test/farms.test.ts): the ring closes on whole cells; the same field at the same place and across the seam; tracks between cells; every crop planted, flowers most; a long-day crop lit into the night then dark, a short-day crop dimmed before sunset; six hours of dark for every crop; and on the compiled world, farmland inside The Fields, cleared of trees, and none outside it.

D-096 — The farms spread round the world, and the world holds still while it is made

Amends D-095.

Asked for (author, 2026-09-30): "the fields are concentrated in a single region. let's split them up and rearrange them around the world." And, separately: "don't run the world while the building process is going on. the 'sun' moves while the world is still building, and that seems weird. have everything be paused until the build process is all finished."

Nine farms instead of one. The 250 km² D-095 chose for the night stays the total; it is now nine farms of about 28 km² each, rounded rectangles 6.2 km along the axis and 4.8 km around, each its own farmland feature with a provisional name:

Farm θ z Where
Ringside Fields 1.7 129 km the Uplands, beside the Uplands Ring
Far Ringside Fields 4.3 131 km the Uplands, the other side of the world
Greatwood Clearing 2.6 240 km Greatwood, near its sunward edge
Capital Fields 5.4 262 km Greatwood, 44 km antispinward of the Capital
Deepwood Fields 3.8 320 km Greatwood, opposite The Deep
Greatwood Fields 2.0 380 km Greatwood's deepward half
Railhead Fields 0.0 470 km where the Axial Line ends
Far Fields 3.4 455 km before the far end, opposite the railhead
Last Fields 5.0 470 km the nearest to the Deepward Sea and the ice

Each site was checked on the compiled world for open ground — no water, desert, bare rock or range — before it was placed. The Uplands are the roughest: part of each Uplands farm falls on broken ground and is left unploughed, as the farmland's own rules leave it. The ones in Greatwood clear their ground of trees, so they read as clearings.

The world holds still while it is made. The clock (world/clock.ts) gains held: it keeps whether it is running — the panel still says so — but no time passes. The app holds it from the moment it starts until the status dialog has gone, or, for a compile quick enough never to show the dialog, until the compile is done; and again whenever the world is recompiled. Both views read it too: while it is held their frame time is zero, so the hull does not spin, the water does not move, the sky through the far cap does not turn and a tour does not advance.

Tests: nine farms sharing 240–260 km², spread more than 4 rad round the ring and 300 km down the axis, each farmland at its heart and cleared of trees.

D-097 — Farms grow over the land, and their fields are irregular

Amends D-095 and D-096.

Asked for (author, 2026-09-30): "the current rectangular regions (both the general 9 and the farms inside them) are very orderly, can we do some sort of natural growing pattern that results in more random-looking patterns?"

A farm grows (terrain/farms.ts growFarm, compiled last, over the land as it is). A farm in the world file is now a point and an area — the nine of D-096 keep their places, each areaKm2: 27.8. At compile time it grows from the point over a 200 m lattice of patches, always taking next the patch on its edge with the lowest key: half how far it is from the seed (over the radius a disc of that area would have), half luck, by hash. It never takes ground that is wet, sand, rock, a range (over 150 m of relief), ice, or steeper than 12%, so it goes round a river or a hill instead of over it, until it has its area. Near-and-lucky is what makes it a blob with lobes and bays — a test holds it to at least 1.3 times the edge of a disc the same size, all in one piece. A field belongs to the farm if the patch under the point it was laid out from does, so the farm's edge runs along its fields' edges and no field is cut in half; the ground shader sharpens the vertex-interpolated weight so the edge does not smear. A farm may still be drawn as a polygon or band, which is its outline as before.

The fields are irregular (fieldAt, and its GLSL twin). In place of D-095's grid:

Tests (test/farms.test.ts): no seam; fields of many sizes (the largest fifth at least twice the smallest fifth); tracks along the blocks and only there; every crop, flowers most; a grown farm taking its area in one piece, ragged, and going round ground that does not fit; and in the world, nine farms grown to 240–260 km² between them, farmland and no trees about each seed.

D-098 — On a phone, the height readout fades when the height is still

Asked for (author, 2026-10-01): "the height label takes up a lot of space on mobile, can the big rectangle box fade away or something?"

What it does (ui/eye-gauge.ts wake, style.css). Below 720 px the eye-height readout — the boxed height, gravity and comparison beside the gauge's thumb — fades out over 0.6 s once the height has been still for two seconds, and comes back at once when the height changes by more than half a percent (flying, a tour, the gauge) or the gauge is touched; it stays while the gauge is held. The view re-telling the same height every frame does not wake it. The thumb and the ticks stay, so the height is still read off the scale. Wider screens are unchanged: there the box is not in the way. Screen readers are unaffected — the gauge's value text is always current.

D-099 — Photographs: a view saved as numbers, rendered again on demand

Asked for (author, 2026-10-03): "i've been finding interesting photo opportunities, like lining things up at a particular time, etc. and i've started to use these photos as illustrations, after messing with them in lightroom. let's come up with a way for me to choose the parameters, then render the latest version of that image." And, of the choices then put to the author: save from the app with a key and a button that show up only locally; 4K; no labels at all; the renders in a photos/ folder that git ignores; the exterior first, then the two caps from the Capital.

The list (content/photos.json, src/world/photos.ts). A photograph is a view, a moment and a frame: the world clock in hours, the Light, the frame's aspect, and then for the interior where the observer stands and looks (theta, z, eyeM, yaw, pitch, fovDeg) and how far the far cap's sky has wheeled (spin); for the exterior the camera and its target in kilometres, the hull's turn (phase), the field of view and the hull reading. The exterior has no zoom, so a saved shot's field is always its own 42°; the number is there to be typed, to frame a shot tighter than the view can. Its id is its file name. parsePhoto checks every field and says which is wrong, because the list is typed by hand as often as it is saved, and the id is held to letters, digits and hyphens because it becomes a path.

Saving (ui/app.ts savePhoto, vite.config.ts). P, or Save this shot under View, takes the open 3D view, asks for a name, and posts it to the dev server, which writes the list; a name already there is re-framed after a confirm, keeping its place and its note. This is the one place the browser causes a file to be written, and D-072 stands: it is not the world, it is not in the build (the row is hidden and the key does nothing unless import.meta.env.DEV), and the endpoint answers only the machine the server runs on, since npm run dev is open to the network. A shot is refused while a scenic tour runs: the tour's flyers and camera are its own and would not come back.

Going back to one (author, the same day: "it would be helpful to be able to navigate back to existing points, like replaying. show a list of current ones and allow loading one?"). Saved shots, under the button, lists the file as the dev server has it now, read again each time the list is opened. Choosing one opens its view and poses it (poseShot, not held), with the clock stopped at its hour and, outside, the spin switched off, since otherwise the framing is gone before it is seen; the view carries on from there, and the shot's name is offered when the next one is saved, so adjusting and re-framing is two keys.

Rendering (tools/photos.mjs, npm run photos). Each shot is opened with its clock in the address, posed through window.towerPhotos.show — which sets the Light and the hull and calls the view's holdShot — and photographed with everything but the canvas taken away: panel, readouts, gauge, view buttons, labels. The window is the shot's aspect, 1920 CSS px on its long edge at two device pixels, so the picture is 3840 px and the stars and lines keep the weight they had on the author's screen. --long-edge changes the size, --only picks shots. A held view stands still — the hull does not turn, the far cap's sky does not wheel, the water is at the held scenes' moment, the plant does not vent — and an interior entry rendered twice gave the same bytes. Posed directly, not through the UI: the screenshot tour (D-042) is a test of the application and stays as it is; this is a camera. The two share tools/browser.mjs, and the tour hides the save button, since it runs on a dev server and photographs what readers get.

Not done. A shot taken during a tour.

The list as it starts. The far cap and the sun cap from 300 m over the Capital, at 08:45 (author: "i like the cap photos, but please change them to be at around 8:30-9am"). And tower-from-the-far-end: the author's blog illustration, whose settings were never saved, worked back from the JPEG. Its stars matched a render's at 1.28 times the size, so it is a crop — hence the 33.31° field; the camera was fitted by least squares to the far cap's hub and outline, the collector's rim and the sun, to about 3 px; the stars put it on day 5, and the lit rod seen through the far cap puts the sun 525 km along, 18:47. The cap's frame repeats every sixth of a turn and the rock could not be matched through the processing, so phase is 0 by default rather than by measurement.

Tests (test/photos.test.ts): both kinds of shot read back as written; a bad field named; ids that could leave the folder refused; every entry in the list valid and uniquely named.

Source
1# Tower — Decisions
2
3Standing record of choices made and why, so they can be revisited deliberately
4rather 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
12Considered 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
21roof is 16 km up and oppressive; at 150 km the far side is 300 km away and lost
22entirely. At 50 km the opposite land is 100 km overhead — far enough to be pale
23and dreamlike, near enough to be unmistakably *land above you*. That image is the
24point of the whole project, and this geometry is what produces it.
25
26The 7.5-minute spin also sits in a good window: fast enough that star motion is
27obvious in a few seconds of watching, slow enough not to look like a fairground
28ride.
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
36The brief specified a Wisconsin-sized inner surface. Wisconsin has two areas —
37total 169,635 km² and land 140,268 km². Sizing the slats to consume exactly the
38difference makes both true at once: **Tower's total inner surface is Wisconsin's
39total area, and Tower's habitable land is Wisconsin's land area. The slats are its
40lakes.**
41
42This is a coincidence discovered while computing, not a constraint imposed. It
43costs 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
46surface, 169,646 km² — Wisconsin's total area. The second coincidence recorded
47below no longer applies.*
48
49**Slat count is left as a tunable constant.** *The runtime control is disabled and
50hidden as of D-033; the derivation below is unchanged and still tested.* The width
51follows from the count:
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
608 chosen because 32.5 km strips read as *regions* — a day's travel across, room
61for real geography. At 24 the world becomes narrow ribbons where every point is
62within 5 km of a wall of stars, which is striking but claustrophobic. Revisit
63after 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
66runtime 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×
73concentration. No physical law is bent. *This supersedes an earlier version of
74D-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
77that Earth-like brightness was impossible without an active, physics-breaking
78waveguide. 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
86Holding the beam at 1 km forced 313,000× and broke the second law. Widening the
87beam 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
90midday, 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*
100terrestrial 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 %
102reflectivity reach thermal equilibrium at 316 °C in that beam, an ordinary
103radiator 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
1063.65° wide — about seven sun-widths. It still reads as a luminous rod running the
107length 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
110than a deficit, brightness becomes a **setting** rather than a fixed value. The
111diffusers are software-controlled; illumination can run anywhere from 30 W/m²
112permanent dusk to 320 W/m² clear-sky noon. The "embrace the dimness" option
113weighed and rejected in the first pass is no longer an alternative to this
114decision — 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
120The brief requires that one cap face the sun permanently. Any orbit whose plane
121contains the sun line puts Tower through Earth's shadow once per revolution, which
122would interrupt the beam. A terminator orbit never enters shadow.
123
124The cost is that the plane must be reoriented ~1°/day against the gyroscopic
125stiffness of a spinning 540 km cylinder. This is enormous and continuous. Canon
126adopts it as a feature: Tower's existence is a permanent act of maintenance, not a
127stable 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
135The app is three canvases, a picker and a control panel, sharing one small state
136object. A framework would add build weight and a rendering model that fights
137Three.js's own loop, in exchange for conveniences this UI does not need. Revisit
138only if the control panel grows genuinely complex.
139
140Canvas 2D rather than Three.js for the map: it is a flat unrolled rectangle with no
141projection 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
148genuinely modelled and correct. The ground is procedural terrain and texture only —
149no scenery meshes, no settlements, no vegetation.
150
151The illusion of standing inside Tower comes almost entirely from the sky: land
152curving up into blue, the lit shaft overhead, stars wheeling past every 7.5
153minutes. Detailed ground scenery adds cost proportional to realism and contributes
154little 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
161land area as free parameters. Everything else — spin period, rim speed, Coriolis
162parameter, slat width, angular spacing, axis pressure, beam power — is computed.
163
164The alternative, transcribing the figures in `design.md` as literals, would drift
165the moment any parameter is revisited (and D-002 and D-003 both flag likely
166revisits). 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*Amended by D-074: individually the mirrorlets stay invisible, but by day the swarm
173gathers into one shoal on purpose, and that shoal is Tower's sun.*
174
175**Decided.** 78 billion 1.2 cm mirrorlets, and the beam's visible glow is
176atmospheric scattering that does not depend on them.
177
178**The question.** Would a modest number of large diffusers read as a string of
179separate suns rather than a continuous glowing rod?
180
181**Yes.** At 50 km, naked-eye resolution is 14.5 m; anything larger shows a visible
182disc. But shrinking below 14.5 m is not sufficient, which was the non-obvious part.
183A flat mirror preserves radiance, so an aimed mirrorlet shows the *sun's own
184surface brightness* over a small angle — unresolved but blinding, exactly as stars
185are unresolved and plainly visible. Magnitudes: 1 m → −13.6 (brighter than a full
186moon), 10 cm → −8.6, **1.2 cm → −4.0**, which finally sits under the daylight
187visibility threshold. The binding constraint is flux, not angular size, and it is
188two orders of magnitude tighter than the resolution limit implies.
189
190**The better answer, found while checking.** The beam glows regardless. Rayleigh
191scattering of a 6.8 MW/m² beam through 3.18 km of 5.2 kPa axis air gives 779
192W/m²/sr — 26× blue sky, 25,700× below the solar disc. The continuous-glow imagery
193is therefore a robust property of beam plus atmosphere, not a consequence of
194getting the diffuser design right. Canon leans on this; the swarm sizing is then
195only about keeping individual glints out of the sky.
196
197**Consequences taken.** 11.3 km² of mirror at 1.2 cm is 78 billion units massing
198158 tonnes, spaced 3.8 m apart. Optical depth 1.41 leaves 24 % of the light
199reaching the far cap, which satisfies the brief's "shines to the far end" without
200stipulating it. *The passive drift-and-recirculate mechanism first recorded here is
201superseded by D-009 — it does not work.*
202
203**Story consequence.** 78 billion independently aimable mirrors make illumination a
204render target rather than a schedule. Sunsets are compositing, not optics.
205
206---
207
208## D-009 — Tower flies every foil itself, because nothing else works
209
210**Decided.** All 78 billion foils are under continuous individual control by Tower.
211There is no recirculation machinery at either cap. *This supersedes the passive
212drift mechanism in D-008.*
213
214**Origin.** Proposed by the author as characterisation — Tower is an intelligent
215being, so it controls each foil directly and moves the swarm partly for its own
216enjoyment. Checking the physics turned this from a flourish into a requirement.
217
218**A passive swarm is impossible.** D-008 assumed the axis was effectively
219weightless. It is not. At the beam's edge, 1,592 m out, g = ω²s = **0.312 m/s²**. A
2202 mg foil sinks outward at 0.26 m/s and **falls clear of the beam in 1.7 hours**. A
221drifting swarm dissolves in an afternoon. Continuous active flight is the only
222configuration that holds together at all.
223
224**They can easily afford it.** Each foil intercepts 979 W; harvesting 0.1 % yields
225~1 W on a 2 mg body — 489,600 W/kg, ~50× a jet engine. Flying up-beam at 1.5 m/s
226costs 15 µW against 14,400 µW harvestable from ambient world-light: **a 1000×
227margin, without touching the beam.** So the foils fly home unaided, no structure is
228required at the far cap, and the exterior render is simpler than D-008 implied.
229
230**The interesting part is the slack.** With 1000× of margin, efficiency is not a
231constraint on Tower's behaviour. It can move the swarm in spirals, shoals and
232patterns that cost energy for no reason except preference, and nothing about the
233world degrades. Play is not a luxury Tower affords despite the physics — it is
234budget the physics hands over for free. The weather is therefore not a system
235Tower operates but something closer to body language.
236
237**Falling out of this: aiming is human-scale.** A 1.2 cm mirror is
238diffraction-limited to 4.6×10⁻⁵ rad, giving a **2.3 m spot** at 50 km. Tower can
239place a glint on one person that a bystander five metres away cannot see. Offered
240to the author as a story mechanic; canon records the optics either way.
241
242**And: mirrorfall.** A failed foil sinks out of the beam, takes ~2 days to descend,
243and lands at 0.48 m/s — snowflake speed. A permanent slow glitter falls out of the
244sky.
245
246---
247
248## D-010 — The app is a companion to a novel, not a tech demo
249
250**Decided.** Tower is the setting of a novel the author is writing. The app serves
251that purpose first.
252
253Two consequences, recorded because they change priorities rather than mechanics:
254
255- **Coherence outranks spectacle.** These documents are series-bible material and
256 the prose is downstream of them. A number that looks better but contradicts
257 something is worse than a duller number that holds. This is already how the
258 physics has been handled; it is now the stated rule.
259- **Shareable permalinks to a point move into scope** (phase 4, previously
260 deferred). "Stand here and look at this" is the entire value of a companion
261 piece, and a link is how a reader receives it. Named locations from the novel
262 should be markable on the map for the same reason.
263
264Not yet decided, and deliberately left open: whether the app should ever surface
265narrative content — place names, events, characters — or remain a pure physical
266model that the prose refers to from outside. That is the author's call and does not
267need answering to proceed.
268
269---
270
271## D-011 — Writer mode: the app is an instrument, and the world file is git
272
273**Decided.** Two modes over one committed `world.json`. Readers get a finished map;
274the author gets editing plus a set of measuring instruments. No backend.
275
276*Superseded in part by D-072: there is one mode now, and the world is edited in
277`content/world.json` rather than in the browser. "The world file is git" and "no
278backend" stand.*
279
280**Origin.** The author intends to use the app to *write* — placing things and asking
281questions like "can the far face be seen from the midpoint?", "where does a central
282embassy go?", "can a lake overhang?". That makes measurement, not rendering, the
283core feature. D-010 said the app serves the novel; this is what that costs.
284
285**Why those questions justify the build.** Two of the three had non-obvious answers
286that only fell out of computation, and both became canon (§1.8): the far cap is
287invisible from the midpoint at 4 % transmission, and — unexpectedly — the far side
288of the world overhead is *clearer* than the ground at the same distance (78 % vs
28931 % at 100 km), because a sightline across the interior rises through thin axis
290air. An author cannot guess these. The instrument earns its place immediately.
291
292**Static, file-based, no backend.** GitLab Pages serves static files, so: edit in
293browser → export → commit → publish. The world lives in git beside the prose,
294diffable and versioned, with no service to run or pay for. Drafts sit in
295`localStorage` between exports.
296
297**Coordinates in the file are canon (θ, z), never map pixels,** so the world
298survives any change to how the map is drawn. Consistent with D-007.
299
300*Correction (D-068): as built, writer mode has always been the default and `?reader=1`
301turns it off; `?writer=1` does nothing. The reasoning below holds either way.*
302
303**Writer mode is a UI mode, not a security boundary.** `?writer=1` is guessable and
304that is fine — an editor with no persistence and no backend can only alter the
305visitor's own copy. Recorded explicitly so it is never mistaken for access control
306and never load-bearing for privacy.
307
308**The real privacy risk is the repo, not the flag.** Working notes must not ship to
309readers, but anything committed to a public repository is public whatever a
310`"public": false` field says. Hence two export files: `world.json` (public entries,
311committed) and `world.private.json` (everything, gitignored). Whether private notes
312should be version-controlled anyway — private repo, or a second one — is left to
313the author and does not block anything.
314
315**Sequenced early.** The sightline probe lands in phase 6 but should be built first
316within it: it is useful the day it exists, and §1.8 is evidence the author is
317already asking questions only it can answer.
318
319---
320
321## D-012 — Ids follow the name until pinned by hand
322
323**Decided.** A place's id is re-derived from its name while it still matches it,
324and freezes permanently once the author edits the id field.
325
326*Retired by D-072 along with the editor. Ids are written by hand in the file now, and
327the reason below — a permalink is worthless if the id churns — still governs changing
328one.*
329
330**The tension.** design.md §3.2 makes ids permalink targets, which argues for
331never changing them. But places are created from a placeholder name ("New city"),
332so a never-changing id produces a world file of `new-city`, `new-city-2`,
333`new-city-7` — unreadable in git diffs and unusable for hand-editing, both of which
334D-011 counts as reasons for the file-based design in the first place.
335
336**Resolution.** During world-building, readability wins; after publication,
337stability wins; and the author is the only one who knows which phase a place is in.
338So the id tracks the name automatically until they say otherwise, and the id field
339is shown in the editor so saying otherwise is one edit. `isAutoId()` in
340`src/world/schema.ts` is the test, and is unit-tested directly.
341
342**Found by running it.** The first implementation derived the id once at creation
343and never updated it, which the browser check caught immediately — every exported
344place was called `new-city`. Two further bugs surfaced the same way: renaming
345blanked the editor (the store re-rendered before the selection moved to the new id),
346and clicking an existing marker while a creation tool was active dragged the old
347place instead of creating a new one. None were visible in the unit tests.
348
349---
350
351## D-013 — The map is bounded, and cut at a slat centre
352
353**Decided.** The map shows the inner surface once, as a bordered rectangle, rather
354than tiling vertically. The cut falls at the centre of the last slat.
355
356**Why not tile.** The circumference wraps, so tiling is the honest rendering and it
357makes places near the seam continuous. But it also makes the world look boundless:
358with no edge, the author cannot see the extent of what they are building, and
359repeated strip labels read as more strips rather than as repeats. For a map — a
360thing whose job is to show where things are relative to the whole — legible extent
361beats a hidden seam.
362
363**Why cut there.** Any bounded map needs an edge, and the edge destroys whatever it
364falls on. Land strip 0 is centred on θ = 0, so the obvious cut would slice a whole
365strip in half across the top and bottom margins. Cutting at a slat centre instead
366leaves every land strip whole and splits only a band of stars, which carries no
367geography. This is exactly why Earth's maps are cut at the antimeridian. With zero
368slats there is nothing to protect, so the cut returns to θ = 0.
369
370**Consequences taken.** Panning is clamped so the surface cannot be lost off-screen
371— without tiling there is nothing to pan back into. A sightline whose short route
372crosses the edge is drawn as two segments, entering and leaving opposite edges, so
373the drawn path always matches the measured distance rather than contradicting it.
374
375---
376
377## D-014 — Slat count is a runtime control, 0 to 16
378
379**Decided.** `SLAT_COUNT` stops being a compile-time constant. The count lives in
380`src/canon/slats.ts`, adjustable from 0 to 16, persisted per browser, with every
381slat-derived figure computed from it.
382
383**Why.** D-002 already called this "the most tunable number in canon" and said to
384revisit it after seeing the map. Making it a slider makes that revisiting real
385rather than a code edit, and the author is choosing the shape of a world rather
386than configuring software.
387
388**The invariant that makes it safe.** D-002 fixes the *total* slat width from the
389land-area target, and that total does not depend on how many slats it is divided
390into. So **land area stays exactly Wisconsin's 140,268 km² at every count from 1 to
39116** — the coincidence survives the whole range, and only the per-slat width moves
392(18.1 km at 3 slats, 3.40 km at 16).
393
394**Zero is a genuinely different world, and the app says so.** With no slats the
395land becomes the entire inner surface, 169,646 km², and nobody in Tower can ever
396see a star. That breaks the D-002 coincidence, so the control states it plainly
397instead of silently reporting a changed number. Rounding that away would have been
398the easy option and the wrong one — it is exactly the kind of quiet inconsistency
399the documentation gate exists to catch.
400
401**Cost.** Slat geometry moved from constants to functions, which rippled through
402`geometry.ts`, the map, the instruments and the tests. Worth it: D-007's principle
403was always that canon is derived rather than transcribed, and this is that
404principle extended one parameter further.
405
406---
407
408## D-015 — Hydrology: shaped terrain, no sideways Coriolis, climate from the slats
409
410**Decided.** Recorded as canon in design.md §1.9, and it amends §1.4.
411
412**The provable part.** The floor's tangent plane is spanned by θ̂ and ẑ and the spin
413axis is ẑ, so surface flow gets `ẑ × ẑ = 0` axially and a purely radial `ẑ × θ̂ = −ŝ`
414circumferentially. The Coriolis force has **no component inside the surface** at any
415heading. Tower's rivers do not curve, and Earth's entire rotational repertoire —
416hemispheres, Baer's law, gyres, cyclones — is absent. This is not a simplification;
417it falls out of the geometry.
418
419**Where the force goes instead.** Into vertical motion. Waterfalls lean retrograde
420as h^1.5 (133 m over a 1 km drop, 377 m over 2 km), and rain hangs at a permanent
4211.47° slant that never varies. Rising things lean the opposite way. A world where
422every falling thing drifts one way and every rising thing the other is a strong,
423cheap, always-visible signature.
424
425**The absence that matters more.** Potential depends only on radius, so the floor is
426one equipotential and a perfect Tower has *no rivers at all*. Every river exists
427because the floor was shaped, there is no tectonic uplift, and erosion is therefore
428one-way — ~30 Myr to erase a 3 km range. Tower's hydrology is maintenance, which is
429D-004's theme arriving from a new direction.
430
431**An inconsistency found and fixed, not smoothed over.** §1.4 specified a
432glass/**diamond** composite slat fill. Diamond conducts ~2,000 W/m·K; such a slat
433would radiate 385 W/m² to space, exceeding Tower's entire 320 W/m² mean insolation
434and freezing the world. Canon now specifies low-conductivity glass, which loses
435~28 W/m² through 5 m. Raised explicitly per the documentation gate rather than
436quietly retconned.
437
438**The gift in that loss.** Tower has no latitude — every point is equidistant from
439the light, so there is no natural climate gradient whatsoever, which is a real
440worldbuilding hole. The leaking slats fill it: eight cool belts running the full
441540 km, parallel to the axis. **Tower's climate bands run lengthwise, not around**,
442and the slat count sets the climate as well as the geography — further support for
443D-014 making it a control.
444
445---
446
447## D-016 — Slat orientation: axial or rings — OPEN
448
449**Not decided.** Canon keeps axial strips for now. Rings are a live alternative and
450this records what the choice costs, so it can be made deliberately.
451
452**Why it matters more than it looks.** Slat layout turns out to set three things at
453once — the shape of Tower's countries, its entire climate map, and what its people
454can see of the universe. It is not a detail of the hull.
455
456**Geometry.** At equal total area: rings are 11.69 km wide with 55.8 km land bands.
457Each land region is then 314 × 55.8 km rather than 32.5 × 540 km — the same area at
458aspect 5.6:1 instead of 16.6:1, a country instead of a corridor. Ring bands are also
459**closed loops**: you can walk 314 km around your whole region and come home without
460crossing a window. Axial strips are ribbons with two dead ends at the caps.
461
462**Climate.** Axial strips put the cool belts lengthwise, so a 540 km river runs its
463whole length through identical country. Rings put them crosswise, giving Tower a
464genuine latitude analogue — a long river crosses all eight bands like the Nile.
465
466**The sky, which is the deep one.** The spin axis is the celestial pole, so stars
467move along the celestial equator. Orientation decides whether a window lies across
468that motion or along it:
469
470- *Axial*: 3.90° across the motion — each star crosses in **4.9 s**, but the strip
471 spans pole to pole so essentially the whole sky passes through, strobing.
472- *Rings*: 6.70° along the motion — a star rides the band horizon to horizon over
473 **3.7 minutes**, but only the 5.8 % of sky within 3.35° of the celestial equator
474 is *ever* visible. The rest is never seen at all.
475
476Earth follows the same rule: a five-second flicker under axial slats, a slow bright
477traveller under rings. The sun is at the celestial pole and invisible through both.
478
479**The cost of rings.** Hoop stress dominates the structure — ρv² is 785 MPa in a
480composite shell plus ~500 MPa of air pressure — and it runs along θ. Axial slats sit
481across that path, so ribs bridge the gap and the glass carries only local pressure.
482A ring runs along the path and is continuous in θ, so **the window itself must carry
483~1.2 GPa**. Glass cannot; it would need hoop cables through every pane, putting bars
484across every view forever. Characterful, but a real price axial slats do not pay.
485
486**Consequence for the app if rings are chosen.** D-013 cuts the map at a slat centre
487because there is an empty θ to cut at. With rings there is not — every θ is land —
488so the map seam would have to slice through inhabited country, or the map would need
489reorienting. Worth knowing before the choice is made rather than after.
490
491---
492
493## D-017 — The hull is sealed; the only views out are at the caps
494
495**Decided by the author.** No slats, in any orientation. The cylinder wall is
496opaque and unbroken for its full 540 km. Supersedes D-002's slat sizing, D-014's
497default, and closes D-016.
498
499**What it costs.** The wheeling star field was canon's answer to "what is most alien
500about standing inside Tower" (§1.2) and the interior view's headline feature. It is
501gone. Tower also loses its only passive climate driver and its only natural source
502of geographic variation.
503
504*Partly recovered by D-045, and worth reading as a correction to the paragraph
505above rather than a reversal of the decision. The wall is still opaque and unbroken
506for its full 540 km — that is what D-017 decided and it stands. What came back is
507the pinwheel, through the far cap window, where §1.4 had always said it would be:
508one sky, at one end of the world, at night. It is no longer everywhere, which is
509the whole difference between the slats and a window.*
510
511**What it buys, which is more.**
512
513- **One unbroken surface.** 169,646 km² of continuous land — Wisconsin's *total*
514 area, the coincidence in §1.1 now standing alone and cleaner. Nothing divides the
515 world into strips; you can walk anywhere, and a river can circle the whole 314 km
516 circumference and close on itself.
517- **The spin inverts.** With no windows and a co-rotating far side, *nothing in the
518 sky moves at all*. But rain still hangs at a permanent 1.47° slant and a 1 km
519 waterfall still lands 133 m off its cliff. **The sky is still and the water is
520 not** — a stranger signature than wheeling stars, and one that shows up in every
521 puddle rather than only when you look up.
522- **The caps become the only view, and it is a good one.** A cap window looks along
523 the spin axis — the celestial pole — so stars do not sweep but *rotate about a
524 fixed centre*, one turn per 7.5 minutes, one motionless star at the middle. The
525 caps are also walls rather than floors: gravity there is tangent to their surface,
526 so each is a 50 km cliff at the end of the world with its windows at the base.
527- **Nobody can ever see Earth.** The terminator orbit (D-004) puts Earth ~90° off
528 the axis, out the side, which is now solid hull. A habitat in Earth orbit whose
529 people have never seen the planet they orbit.
530
531**The consequence the author accepted explicitly.** With no latitude and no windows,
532nothing breaks the thermal uniformity, so there is no passive weather whatsoever.
533Every cloud, every rainfall and every jet stream is Tower deliberately aiming light.
534This is D-009 reaching its end point: the swarm was already Tower's body language,
535and is now its lungs. There is no autonomic layer beneath. **If Tower's attention
536lapses, the rain stops.**
537
538---
539
540## D-018 — Rivers are a closed, pumped circulation, and Tower can easily afford it
541
542**Decided.** Tower's rivers follow the architecture the author proposed: reservoir →
543shaped descent → lake → lift back to the reservoir. Source and sink are one system.
544
545**Why it is the only option.** The floor is an equipotential (D-015) so there is no
546natural slope, and the sealed hull (D-017) leaves no natural water cycle. Water in
547Tower moves only because something moves it.
548
549**Why it is not a compromise.** Costed against the 54.3 TW budget:
550
551| | | |
552|---|---|---|
553| Earth-like rainfall, 1,000 mm/yr | 12.2 TW | **22.4 %** — Earth spends the same share |
554| Recirculating all runoff (1,792 m³/s, a Rhine) against 500 m | 8.8 GW | **0.016 %** |
555
556Pumping the entire hydrology of the world is a rounding error — 6,000× headroom.
557And full Earth-intensity weather is affordable at the same fraction of budget Earth
558itself spends. Tower does not have to choose.
559
560**Canon uses both, because they give different rivers.** A pumped river *begins at a
561machine* — walk to its source and find a wall of water leaving a structure. A
562rain-fed river begins at a cloud Tower made over the highlands, and looks entirely
563natural while being entirely deliberate. Made weather runs the world's general
564hydrology; pumps run the watercourses Tower wants under precise control.
565
566**Two consequences worth keeping.** A closed loop has no net descent, so a river
567circling the world must be pumped — which is the same architecture, closed the whole
568way round. And since nothing evaporates to space, **Tower's water inventory is
569conserved and exactly known, forever.**
570
571---
572
573## D-019 — Geography is authored as data; terrain is generated from it
574
575**Decided.** `content/geography.json` holds the authored geography. `npm run terrain`
576(tools/terrain.mjs) reads it and emits a height map, a colour map, a corridor detail
577render, and a seed `world.json` of places.
578
579**Why data first.** The author asked to work the geography out before returning to
580app code. A JSON source plus a generator means the map is re-derivable from one
581readable file: move the river by editing eighteen waypoints, not by repainting.
582Coordinates are canon (θ, z) throughout, consistent with D-007 and D-011.
583
584**No dependencies.** Node's `zlib` is enough to write PNGs directly (tools/png.mjs,
585~60 lines for 16-bit greyscale and 8-bit RGB), so the tool adds nothing to
586`package.json`. Terrain noise is a small value-noise/fBm module.
587
588**Three bugs the renders caught that no test would have.**
589
590- *A float hash.* The noise hash multiplied large integers in floating point,
591 silently losing precision and streaking the whole map with axis-aligned lines.
592 Fixed with `Math.imul` throughout.
593- *A fractional wrap period.* The circumference is not a round multiple of any
594 useful feature size, so the noise lattice did not close on itself and tore at the
595 seam. Sample coordinates on the θ axis are now snapped to a whole number of
596 cells; the resulting few-percent anisotropy is invisible.
597- *A discontinuous river.* Valley geometry switched between reaches abruptly,
598 stepping the valley from 5.4 km wide to 18.2 km in one pixel column and drawing a
599 visible wall across the map. Reach parameters now blend along the river. A
600 `--probe` flag prints elevation jumps along a row, which is how it was found.
601
602**Known gap, deliberately not fixed here.** The seed `world.json` contains
603`polyline` geometry (the river, the canyon) and kinds the app does not yet know.
604`parseWorld` accepts only points and polygons, so those two places would be dropped
605silently on load. App work is paused by request; this is the first thing to fix when
606it resumes. *Closed by D-022.*
607
608---
609
610## D-020 — The world is belts, and the desert rings it
611
612**Decided by the author.** The desert and its three ridges ring the world; the
613capital's water becomes a sea running along the axis; both end caps get ring oceans;
614the range feeds four parallel rivers.
615
616**The ring desert is a barrier no Earth desert can be.** 22,000 km², 13% of the
617surface — modest against Earth's ~33% — but on a cylinder there is no way *around* a
618band. Combined with three ringing ridges it is three concentric walls across Tower's
619waist.
620
621**The drainage was the hard part, and Earth answered it.** Many rivers each cutting
622their own canyon would destroy the chokepoint. But on Earth most drainages do not
623cut through a rising ridge — they are defeated and diverted, and only an antecedent
624river keeps its course. So three of the four rivers are turned by First Ridge, run
625along its front gathering each other, and leave with the fourth through the one gap.
626**The Narrows carries the entire world's runoff, ~392 m³/s.** The chokepoint is
627strengthened by the extra rivers rather than diluted, and the hydrology is more
628realistic than a single river would have been.
629
630**The Deep's orientation is the decision, not its size.** Long down the axis and
631narrow around, so it is crossed the *short* way — around θ, where the floor curves
632up and the far shore stands 2.6 km above eye level. Lengthwise it would be dead
633flat. It also gives Tower back a thermal flywheel: 19× the diurnal heat storage of
634bare ground, 5.9 days per kelvin at full power. D-017's "if Tower blinks the rain
635stops" becomes "the world has about a week of momentum" — a lapse now has time to
636become visible before it becomes fatal.
637
638**The cap seas were not chosen.** Gravity on an end cap is tangent to its surface,
639so water cannot rest there and runs to the rim. Both ring oceans are consequences,
640not designs.
641
642**Skiing came out of §1.9.2 unprompted.** A descending skier has radial velocity and
643therefore real Coriolis: a steady antispinward push on axial fall lines, and a
644weight change of up to 11% on circumferential ones. A retrograde run gets lighter as
645it accelerates; a prograde run grips harder. Every slope in Tower has a handedness,
646which is a sport that cannot exist on Earth.
647
648**Tool work this required:** `ring` footprints, a `flood` op for troughs and bands,
649and a `tarn` op that fills from the bowl floor rather than the rim — the first
650version took its level from the rim mean, so water spilled downhill and every
651mountain lake rendered as a half-moon.
652
653---
654
655## D-021 — The terrain tool reports progress, and got 13× faster doing it
656
657**Prompted by the author:** `npm run terrain` ran for a long time saying nothing.
658Adding progress output meant first working out where the time went, which turned a
659logging task into a performance one.
660
661**86 s → 6.4 s.** Three causes, all found by measuring rather than guessing:
662
663- **No spatial index on the river carve.** `opCarve` tested all 592 smoothed river
664 segments against all 2.4 million pixels — ~2.9 billion distance tests across both
665 renders. Segments are now binned into a 4 km grid and each pixel tests only its
666 own cell. *86 s → 17.8 s.*
667- **Deflate level 9 on the colour maps.** Hillshade noise compresses poorly, so
668 level 9 spent 4.2 s on one map for a few percent of size. RGB now encodes at
669 level 6: 8× faster for 10% larger files, which is the right trade for a tool run
670 on every edit. The 16-bit height map stays at level 9 — smooth terrain under the
671 Up filter is nearly all zero deltas, and it costs 0.1 s. *17.8 s → 8 s.*
672- **Domain warps computed before range checks.** Several ops evaluated two noise
673 functions per pixel *then* discovered the pixel was nowhere near the footprint.
674 A conservative unwarped-z rejection now runs first. *8 s → 6.4 s.*
675
676**The log is a table, not a stream.** One row per relief op with its timing, then
677relief/colourise totals per view, then each PNG with its size, then the seed. The
678point is to see which feature is expensive when the file grows, not just that
679something is happening. `--quiet` collapses per-feature rows.
680
681**Worth recording as a habit:** the request was for logging, and the logging is what
682exposed that the colour PNG cost more than the entire mountain range. Instrumenting
683first and optimising second found three real problems in one pass.
684
685---
686
687## D-022 — The app catches up with the decisions
688
689**Implemented.** Five outstanding decisions, plus two bugs found while doing it.
690
691**The schema now matches the world** (closes D-019's gap). `polyline` geometry
692joins point and polygon — a polygon needs three vertices for an interior, a
693polyline only needs two to go somewhere — and the kind list grows from six to
694thirteen (`mountain`, `river`, `canyon`, `forest`, `desert`, `tower`, `town`), with
695`AREA_KINDS` / `LINE_KINDS` deciding whether a traced shape closes. Before this the
696seed silently dropped The Long Water and The Narrows. A test now loads
697`static/world.json` and asserts nothing is lost, so the two files cannot drift apart
698again unnoticed.
699
700**The rendered terrain is drawn under the map**, which is what the whole tool was
701for. Area regions become outlines rather than fills when it is showing: the raster
702already says where the desert is, and a translucent rectangle over it only fights.
703
704**A desync that would have been baffling.** The app cut the map at θ = 0 with no
705slats; `content/geography.json` renders against θ = π. The raster would have landed
706**half a world out of register** with its own place markers. Both now use
707`MAP_CUT_WITHOUT_SLATS = π` — opposite the capital, which is D-013's antimeridian
708principle applied to habitation rather than geology — and a test asserts the two
709files agree.
710
711**Reader and writer modes** (D-011). `?writer=1` reveals the tools, editor, canon
712controls and import/export; readers get the map, the place list, a read-only detail
713card and the sightline probe. **A bug worth recording:** `renderEditor` un-hid the
714editor whenever a place was selected, overriding the mode — so readers were shown
715the edit fields *including private notes*. Mode is now decided inside
716`renderEditor`, not applied around it. Hiding a panel elsewhere is not the same as
717never rendering it.
718
719**Permalinks** (D-010). `#place=<id>` selects and centres a place; `#at=θ,z` restores
720a sightline endpoint. This is why D-012 made ids follow names only until pinned —
721a permalink is worthless if the id churns.
722
723**Labels are placed by priority and skipped on collision.** Settlements outrank
724regions; overlapping labels are dropped rather than drawn on top of each other. With
72522 seeded places the mountain stations were an unreadable smear.
726
727---
728
729## D-023 — The exterior view, and what it is honest about
730
731**Implemented.** Phase 3: Tower from outside, with the hull, both caps, the
732collector, the converging beam, the spin, and picking on the hull.
733
734**Canon numbers are the code's numbers.** Scene units are kilometres and the axis
735lies along Z with the sun cap at z = 0, so a canon point is literally
736`(R cos θ, R sin θ, z)`. No conversion layer to get wrong.
737
738**Picking has to undo the spin.** The hull turns, so a ray hit is in world space
739while the answer wanted is canon θ, which is fixed to the hull. Resolving the hit
740through `worldToLocal` handles it. Verified by clicking the same screen pixel at two
741spin phases 34° apart and confirming the reported θ moved by 0.588 rad against a
7420.593 rad phase change, with z identical — the pixel really is landing on different
743ground as Tower turns beneath it.
744
745**Three departures from physics, all stated in the code.** Sunlight runs parallel to
746the axis, so the flank is edge-on and correctly renders almost black; fill lights
747make it readable. And in vacuum the beam between collector and cap has nothing to
748scatter off, so it would be invisible; it is drawn faintly because it is the point
749of the view. Same licence as the map's hillshade, and worth naming rather than
750letting a reader mistake either for a physical claim.
751
752**Three.js loads on first use of the view.** It is by far the largest dependency and
753starts an animation loop; a reader who only opens the map should not pay for it.
754
755**Earth is not drawn.** It is ~90° off the axis and hundreds of thousands of km
756away; at true scale it is invisible, and at any visible scale it is a lie about
757distance. Given how much of this world came out of taking distances seriously, a
758compressed Earth would be the wrong kind of shortcut. The sunward glow is honest —
759the sun really is that way — and D-017's point stands: from inside, Earth is never
760visible at all.
761
762---
763
764## D-024 — The terrain is wrapped onto the hull as an x-ray
765
766**Implemented at the author's request.** The exterior view paints the rendered inner
767surface onto the outside of the cylinder, alongside the place markers.
768
769**It is a projection, and the code says so.** From outside, Tower is structure; the
770texture shows what is *inside*. That makes it the frankest of the view's three
771reading aids — nobody could mistake a forest on the outer hull for a physical claim,
772where the fill lighting and the visible beam are subtler about it. It earns its
773place because the hull is opaque: this is the only view that shows the whole world
774at once.
775
776**The UVs are rewritten, not derived.** Three's cylinder runs u around the
777circumference and v along the axis; the raster is the other way round, x = z and
778y = theta. The obvious approach — recovering the angle from each vertex position —
779is wrong, because the seam vertices are duplicated and `atan2` gives both the same
780angle, smearing one column of triangles across the entire image. Remapping the
781original uv attribute stays linear and lets RepeatWrapping absorb the offset.
782
783**Alignment was verified, not eyeballed.** The raster was sampled at each place's
784computed position and the colour classified: four of four mountains landed on rock,
785both cap seas on water, the desert on desert. Judging this by eye would have been
786hopeless — the whole map is bands of green and tan, and a half-world error looks
787much like a correct one from most angles.
788
789**Two fixes the terrain forced.** Small bright dots read fine against a bare hull
790and vanish into forest and desert, so markers gained a dark outline shell; and they
791are now scaled per frame to a roughly constant screen size, because 4 km is
792sub-pixel at the 1,200 km the default view sits at. The terrain also needed an
793emissive map: sunlight runs along the axis, so most of the hull faces away from
794every light and the map would otherwise be legible only on a sliver.
795
796**Also fixed here:** selecting a place from the list moved the panel but not the
797camera — only permalinks called `focusOn`. Both paths now do.
798
799---
800
801## D-025 — An empty draft is not a draft
802
803**Fixed after the author hit it.** The map showed one place where twenty-two were
804expected. The local draft was empty and, because any draft beat the seed, the
805published world had been shadowed since the first time the app was opened.
806
807**How it happened.** `public/world.json` originally shipped as `{"places": []}`.
808Opening the app once in that window persisted an empty world to `localStorage` —
809`pagehide` flushes on close — and from then on `load()` saw a draft, took it, and
810never fetched the seed again. Twenty-two places were sitting in the served file, and
811nothing in the UI suggested anything was wrong: an empty world and a deliberately
812emptied world look identical.
813
814**The fix.** A stored world with no places is treated as no draft, so the seed
815loads. There is nothing to protect in an empty world, and the opposite failure — a
816deliberately emptied world refilling on reload — is visible and one click to undo,
817where this one was silent and permanent.
818
819**Plus an explicit way back.** *Reload published world* in the Data section discards
820the draft and refetches `world.json`, for the case where a draft has drifted rather
821than emptied. Verified end to end: an empty draft now recovers 22 places on reload,
822and reseeding after a local edit returns 23 to 22.
823
824**The general shape is worth remembering.** Local-first storage that silently wins
825over shipped content will, sooner or later, win with nothing in it. Precedence rules
826between a cache and its source need a defined answer for the empty case, and a
827manual override for every other case.
828
829---
830
831## D-026 — Two grids that did not agree, both removed
832
833**Removed at the author's request.** The exterior had structural ribs every 20 km;
834the map had a graticule every 50 km. Neither was wrong on its own, but side by side
835they invited a comparison that does not hold — twenty rings against ten lines,
836counting nothing in common.
837
838Both are gone. Neither was load-bearing: the map keeps its scale bar and the
839exterior its spin readout, and with the terrain drawn the ground itself carries the
840sense of distance far better than either grid did. A reference grid that has to be
841mentally reconciled with another reference grid is worse than none.
842
843**Worth generalising:** two independent scales shown in two views of the same object
844must agree or one must go. It is not enough for each to be individually correct.
845
846---
847
848## D-027 — A band is its own geometry, not a polygon
849
850**Fixed.** Regions that ring the world — the desert, the forest, both cap seas — are
851now `{ type: 'band', zFrom, zTo }` rather than four-cornered polygons.
852
853**Found while removing the graticule.** With the grid lines gone, a stray hairline
854was left across the middle of the map. A band written as a polygon runs theta from
8550 to 2pi, and both of those normalise to *the same map row* — so all four corners
856land on one line and the region collapses into a one-pixel streak. It had been
857hiding among the graticule lines the whole time.
858
859**Why a new type rather than a workaround.** On a cylinder, "the whole circumference
860between two z" is a genuinely different shape from a polygon: it has no theta
861extent to give, because it has all of it. Tower's geography is built out of these —
862D-020 made the desert ring the world precisely because a band cannot be walked
863around — so it deserves to be a first-class shape rather than a polygon that happens
864to wrap. The map draws it as a full-height rectangle, hit-testing is a z range, and
865the editor reports it as *rings the world, z 148.0 km to 218.0 km (70.0 km deep)*.
866
867Tests assert the four ringing regions survive as bands through parse and public
868export, and that a band with unusable bounds is rejected like any other geometry.
869
870---
871
872## D-028 — The beam was inverted, and cones fringe
873
874**Found because the author asked what the brown cone and the white circle were.**
875That question is its own answer: the two most important objects in the exterior view
876were unlabelled and one of them was drawn backwards.
877
878**The beam pointed the wrong way.** `CylinderGeometry(radiusTop, radiusBottom)`
879combined with `rotateX(-pi/2)` — which sends +Y to −Z — put the *first* radius at
880the collector. Writing the pair in the order the eye reads them gave a cone wide at
881the sun cap and narrow at the mirror: light converging **on** the collector instead
882of into the aperture, with a 122 km mouth swallowing a 50 km hull. It had been that
883way since the view was built, and the flat brown wash it rendered as hid it.
884
885**The brown was a second bug.** A uniform additive layer at 0.085 opacity over a
886black sky sums to about RGB(43,39,30) — a dull brown that reads as a solid object,
887which is the opposite of a beam.
888
889**Cones fringe, so the beam is rays now.** Brightening it exposed bright dashes
890scattered across the collector's rim. Isolated by bisection: removing the cone
891cleared the disc, removing the stars did not, and shortening the cone moved the
892dashes with its mouth. At the mouth the cone's shell is edge-on, so its triangles
893project to slivers and the additive blend stacks them. Rays from the mirror to the
894aperture have no silhouette to alias, need no alpha ramp to fake convergence, and
895read more like light than a translucent solid ever did.
896
897**Everything is captioned now.** Collector, beam, sun cap, far cap, as screen-space
898labels. If the person who commissioned the world cannot identify an object in it, a
899reader has no chance.
900
901---
902
903## D-029 — The view persists, which surfaced a boot-order bug
904
905**Implemented at the author's request.** Map or exterior is remembered in
906`localStorage` and carried in the permalink hash, so a shared link can say *look at
907this from outside*. The hash wins; storage is the fallback for a bare URL.
908
909**And it immediately broke the exterior on load.** Restoring that view un-hides the
910canvas and constructs the renderer in the same task, before the browser has laid the
911element out — so `resize()` measured a zero-sized box, bailed out, and the renderer
912kept its default 300×150 buffer stretched across a full-size canvas. Clicking into
913the view had always worked because layout had long since settled by then; only
914restoring it on boot hit the window.
915
916Fixed with a `ResizeObserver` on the canvas rather than another guess about timing,
917which also covers the sidebar and window resizing for free.
918
919---
920
921## D-030 — The camera persists, and the caps are places
922
923**Both at the author's request.**
924
925**Camera.** Position and target are stored per browser and restored on load, so a
926reload returns to the vantage you left rather than the default framing. Every field
927is validated before use — three finite numbers each, and a distance inside the
928controls' own limits — because a corrupt entry here would leave the view pointing at
929nothing with no obvious way back, and silently-bad persisted state has already cost
930this project once (D-025). Saves are debounced: OrbitControls fires `change` every
931frame of a drag.
932
933**Amended: a heading is an angle, so it validates by wrapping, not by range.** The
934interior view persists the same way, and its saved heading was range-checked like a
935distance — rejected outside ±100 radians, which the panning loop reached after about
936sixteen turns in one direction, at which point a reload silently put the observer back
937facing deepward. Nothing was corrupt; the check was asking the wrong question. The
938heading is now wrapped into [0, 2pi) at every write, and on restore any finite value is
939accepted and wrapped, because an unwrapped heading from an older save is still a
940perfectly good direction to face.
941
942**Caps.** The sun cap and far cap are now features in `content/geography.json`, so
943they appear in the place list and can be focused like anywhere else. Two things had
944to be special-cased, both for the same underlying reason — *a cap is not a patch of
945ground*:
946
947- **Its marker belongs on the axis.** Placed on the hull at theta = 0 like every
948 other marker, it sat on the cap's rim and rode round the edge as Tower turned.
949 A cap's location is its centre.
950- **Its framing is different.** The usual from-the-side shot shows an end cap edge-on
951 as a line. Standing off along the axis instead is worse — it looks down the barrel
952 and foreshortens all 540 km behind an unlit disc. What works is the default view's
953 oblique angle slid onto the cap, and a light from deepward so the far cap's outer
954 face is not a black hole exactly when you focus on it.
955
956**A note on the debugging.** Verifying camera persistence took several wrong turns
957because the test kept invalidating itself: `location.reload()` fires `pagehide`,
958which saved the live camera over the value the test had just planted. The
959persistence was working the whole time. Worth remembering that a `pagehide` save
960makes "set state, reload, check state" an invalid experiment.
961
962---
963
964## D-031 — Sidebar: an accordion, and a label that stopped lying
965
966**Five UI corrections from the author, all of them about the panel saying the wrong
967thing or being in the wrong place.**
968
969**"Canon" was a section heading for one slider.** It read *Canon → Star slats — 0*,
970which framed an exploration control as if the whole section were canon. The section
971is now **Star slats**, its value is a **Count**, and *canon* is a chip on the
972summary that appears only when the count is 0 — because that is the one value canon
973actually asserts (D-017). A label should describe what it labels; "canon" is a
974property of a value here, not a category of setting.
975
976**Order follows scope.** *View* moved to the top: it changes the whole app, not one
977panel. *About* moved to the bottom, where the least-used thing belongs.
978
979**Foldable sections.** Seven panels in a fixed column pushed the useful ones
980off-screen. They are `<details>` elements now — native disclosure, keyboard and
981screen-reader behaviour for free. *This first shipped as an accordion, one open at a
982time, with the Selected panel opening itself on selection; both were removed — see
983D-032.*
984
985**Mode gating moved from a CSS class to the `hidden` property**, since `<details>`
986already carries open/closed state and stacking a `.hidden` class on top of it was a
987second, conflicting notion of visibility.
988
989**And a claim removed.** The About text said the canon was "worked out in the open",
990with links to design.md and decisions.md. Those documents are the author's working
991notes on an unpublished novel, not a public invitation, and the links went to a repo
992a reader may not have. Now it says what is true and no more: *Tower is the setting
993of a novel. This map is its companion piece.*
994
995---
996
997## D-032 — No automatic opening or closing
998
999**Reverted at the author's request:** the accordion was annoying, and nothing should
1000open or close by itself.
1001
1002**The two behaviours were one mistake.** Exclusive opening meant that clicking a
1003place could collapse the panel about to describe it, so the Selected panel had to
1004open itself to compensate. Removing the accordion removed the need for the
1005compensation: sections the author opens stay open, so a selection simply appears in
1006a panel that is already there. A workaround that only exists to undo another
1007behaviour is a sign the first behaviour is wrong.
1008
1009**What remains is state, not behaviour.** View and World start open, and Selected
1010starts open but stays hidden until something is selected — so the first selection
1011shows without anything having moved. After that the panels are wherever the author
1012put them: closing the editor keeps it closed through every later selection.
1013
1014**The general rule this is worth remembering by:** an interface that rearranges
1015itself in response to a click costs the user their place. Prefer a sensible initial
1016state and then hold still.
1017
1018---
1019
1020## D-033 — The slat control is disabled and its UI removed
1021
1022**Removed at the author's request:** changing the count did not fully work. It did
1023not, and for two independent reasons, both worth writing down because this is
1024deferred rather than abandoned.
1025
1026**The raster has no slats in it.** `terrain-color.png` is generated from
1027`content/geography.json` at canon's 0 slats. Raising the count drew slat bands as a
1028vector overlay on top of terrain that shows unbroken land underneath them — two
1029layers asserting different worlds. The corridor is authored the same way: Snowcrown,
1030the rivers and The Narrows all assume no slats, so bands above 0 cut straight
1031through them.
1032
1033**And the map's cut angle moves with the count.** `mapThetaOrigin()` returns
1034`slatCentre(n-1)` when slats exist and π when they do not (D-013, D-024). The raster
1035is drawn against π, so every non-zero count rotates the terrain out of register with
1036its own place markers — by 120° at 3 slats, 157° at 8, up to **137 km** of
1037displacement. This is the same desync D-024 fixed for the zero case, and the test
1038added there only guards the zero case.
1039
1040**What was done.** The section is gone from the sidebar and `loadSlatCount()` is no
1041longer called, so a stored non-zero count from before cannot leak back in. What was
1042*kept* is everything that makes the count correct: `src/canon/slats.ts`, the
1043derivation, and the tests covering counts 1 to 16 — including that land stays
1044exactly Wisconsin's 140,268 km² across the whole range, and that the map cut lands
1045on a slat centre at every count.
1046
1047**What re-enabling would need.** The terrain tool would have to bake slats into the
1048raster from the same count the app uses, which means the count becomes an input to
1049`npm run terrain` rather than a runtime toggle — and the geography would need
1050re-authoring around wherever the bands fall. That is a content decision as much as a
1051code one, which is the real reason it is parked.
1052
1053---
1054
1055## D-034 — The interior view: real geometry, and haze doing the work
1056
1057Phase 5, and the piece design.md §2.4 said would carry the illusion. The scope held:
1058**real geometry, simple ground.** The shell is the actual cylinder, displaced by the
1059height raster and painted with the colour raster; there is no scenery, no buildings
1060and no vegetation.
1061
1062**Nothing in the scene rotates, and that is the point.** The observer, the ground and
1063the air all co-rotate, so from inside a sealed hull the spin is genuinely invisible.
1064No stars wheel overhead because there are no stars (§1.4). What is left is
1065`src/canon/motion.ts`, and it says something the design did not anticipate:
1066
1067| | lean, antispinward |
1068|---|---|
1069| rain at terminal velocity | **1.5°** — fixed, whatever the cloud height |
1070| a 1 km waterfall, at the bottom | **11.3°**, and 133 m of drift |
1071
1072The waterfall figure is the *velocity* at the bottom, which is the angle a streak of
1073falling water is drawn at. §1.9.2 tabulates 7.6° for the same drop, which is the
1074tilt of the *path* — where it lands against how far it fell. Both are right and they
1075differ by exactly half again in tangent (`freeFallPathLeanRad` alongside
1076`freeFallLeanRad`, pinned in test/motion.test.ts), and noticing they were not the
1077same number is the only reason this is written down rather than silently
1078contradicting the canon.
1079
1080Rain barely leans, and it leans by a *constant* angle because drag holds the fall
1081speed constant. Only free fall keeps accelerating, so only free fall keeps leaning.
1082Design.md §2.4 offered "slanted rain and a leaning waterfall" as a pair; they are not
1083a pair. **The waterfall is the compass.** The rain is drawn, and the readout prints
10841.5° beside it so the smallness reads as the finding it is rather than as a bug.
1085
1086**Haze is integrated per fragment**, 24 midpoint samples through the rotational
1087density profile, and it reproduces the §1.8 table from the other direction: the
1088readout prints 78.1% straight up against 31.3% for the same 100 km along the ground.
1089The extinction is carried per channel (Rayleigh goes as λ⁻⁴, so red survives twice
1090as far as green and blue half as far), which is what makes distance blue rather than
1091merely grey.
1092
1093**The airlight is neutral, deliberately.** Tinting it sky blue is the obvious move
1094and it is wrong: blue already scatters 2.4× harder in the per-channel extinction, so
1095a blue source colours the haze twice and every distant thing turns mauve regardless
1096of what it is made of — desert included, which is how the bug was spotted. With a
1097neutral source the blue falls out of the physics and the far side stays recognisably
1098forest and desert at the 78% canon promises.
1099
1100## D-035 — Standing on the ground requires the ground on the CPU
1101
1102The shell is displaced in the vertex shader, which is fine until somebody has to
1103stand on it. The first build put the camera 1.7 m above the *undisplaced* cylinder —
1104underground everywhere the land is higher than 1.7 m, which is everywhere. From
1105inside the rock the ground is invisible: its underside is a front face and the shell
1106is drawn back-side only. The view showed sky below the horizon and nothing else, and
1107at 366 m of eye height it looked perfect, which is what made it puzzling.
1108
1109There is no way to ask the GPU where the ground is, so `terrain-height.png` is now
1110read back through a canvas and sampled here. Two mismatches had to be handled:
1111
1112- **The mesh is coarser than the raster.** Cells are 0.7 km, texels are 0.26 km, so
1113 the surface actually drawn is the raster sampled at the cell corners and stretched
1114 flat between them. The lookup interpolates the same four corners rather than
1115 sampling the raster directly, which would put the observer's feet tens of metres
1116 off in rough country.
1117- **Underground is much worse than hovering.** Half the spread across the cell is
1118 added as clearance: nil on the plains, and on a ridge it lifts you clear of the
1119 facet instead of dropping you inside it.
1120
1121The readback is 8-bit — browsers decode 16-bit PNGs down to 8 — so elevations
1122quantise to 19.5 m steps. Both the shader and this lookup read the same quantised
1123data, so they agree with each other, which is what matters for not being buried.
1124
1125## D-036 — A custom ShaderMaterial writes linear colour into an sRGB canvas
1126
1127Three inserts its colour-space handling into its own materials and never into a
1128custom one, and the first build silently got both halves wrong: the colour raster
1129was sampled as if it were linear, and linear output was written straight to the
1130canvas. Everything read dark and oversaturated, and the far side of the world came
1131out navy where it should have been hazy green.
1132
1133The fix is both halves explicitly — decode the raster with a `pow(c, 2.2)` and end
1134each fragment shader with `#include <colorspace_fragment>`. The trap on the way:
1135adding `#include <colorspace_pars_fragment>` too fails to link, because Three
1136already puts that chunk in the prefix it builds for *every* material in order to
1137define `linearToOutputTexel`. The duplicate definitions produced no console error at
1138all — only `INVALID_OPERATION: no valid shader program in use` once per frame, and a
1139window filled with clear colour.
1140
1141## D-037 — A shared `#view=` link was erased before it was read
1142
1143Found while opening the interior view by URL. `App`'s constructor rendered before it
1144decided which view to open, and `render()` rewrites the hash from current state — so
1145`#view=interior` was replaced by an empty hash a few lines before `initialView()`
1146went looking for it. Every shared link that named a view had been landing on the map
1147since D-029, and it went unnoticed because the last-used view is *also* persisted in
1148`localStorage`, which restored the right view for whoever had visited before. The
1149view is now decided before the first render.
1150
1151## D-038 — Selecting a place walks you there
1152
1153Turning to face a place was the wrong default, and standing in the view for a minute
1154makes it obvious: at ground level the haze horizon closes at 100–150 km (§1.8), so
1155"face the Deepward Sea" from the capital points at 270 km of nothing. Selecting a
1156place now *moves* the observer to it, which is what "focus" already meant in the
1157other two views — the map centres, the exterior swings round, the interior walks.
1158
1159Places with extent also get a heading: stand at the head of a river and look down
1160it, taking the centroid of the traced points as the aim. A point has no direction of
1161its own, so arriving is the whole answer and the heading is left alone. The separate
1162"Stand at the selected place" button is gone, since selection now does exactly that.
1163
1164## D-039 — The height raster gets a declared working range
1165
1166`terrain-height.png` was normalised 0..5,000 m against a fixed `maxElevationM`, and
1167both halves of that were wrong.
1168
1169**The floor mattered more than the ceiling.** Clamping at zero flattened everything
1170below the datum, so every sea and lake floor was a plane — the Deepward Sea read as
11710 m where it is actually −320 m. `content/geography.json` now declares
1172`minHeightM` / `maxHeightM` and the raster is stretched across that range.
1173
1174**The range is declared, not fitted.** Fitting it to the terrain would silently
1175change what every grey level means whenever the geography is edited, which is the
1176kind of drift D-024 came from. Instead the generator prints the range it found
1177against the range declared, warns and clamps if the terrain has outgrown it, and
1178test/raster.test.ts asserts the declared range still contains the terrain.
1179
1180**The precision win is smaller than it looks**, and worth stating plainly: the
1181terrain spans −508..4,392 m, so tightening the range to −520..4,400 m moves the step
1182from 19.6 m to 19.3 m. The quantisation is not set by the range at all — it is set
1183by browsers decoding a 16-bit PNG down to 8 bits, which leaves 256 levels however
1184wide the span. The fix for *that* is a second byte in a second channel, which is a
1185change to the encoder and the shader, and is not made here.
1186
1187## D-040 — The axial sightline names the distance it measured
1188
1189The readout compared "100 km up" against "100 km on" — except the along-axis probe
1190was clamped to the far cap, so standing 22 km from the end it measured 22 km and
1191still called it 100. It reported 77.5% where the honest figure for 100 km is 31%,
1192which is exactly backwards from the point the line exists to make. It now measures
1193whichever axial direction has the most room and prints the distance it used.
1194
1195## D-041 — The caps get a pattern, and it is polar
1196
1197*Superseded by D-078 for the pattern: the caps' frames are identical triangles, at this pitch.*
1198
1199*Half superseded by D-045: the far cap is now a window, and this pattern survives
1200on it as the window's mullions rather than as a filled checker. Everything below
1201still holds — the tessellation, the pitch, the mipmapping and the reason for all
1202three are unchanged, and the sun cap is still exactly this.*
1203
1204The end caps were flat tinted discs. They are the only surfaces in Tower that are
1205not landscape — 100 km of structure closing each end — and blank they read as fog
1206with an edge. A checker fixes that, and does something more useful besides: it is
1207the one thing in the world with a **known cell size**, so a glance at the far cap
1208tells you roughly how far away it is. That is worth having in a world where §1.8
1209says distance is otherwise unreadable.
1210
1211**Polar, not square.** A cap is a disc on a spinning cylinder, so its natural
1212coordinates are radius and angle; a square grid meets the rim at every possible
1213angle and reads as something laid over the geometry rather than part of it. Cells
1214stay roughly square by doubling the sector count each time the radius doubles, which
1215leaves a seam at each doubling — which is what a segmented diaphragm would have
1216anyway. The alternative is slivers at the hub or slabs at the rim.
1217
1218**One texture, both views.** Drawn once into a canvas and shared, so the exterior
1219and the interior cannot drift apart. Three maps UVs across the bounding box for both
1220`CircleGeometry` and `RingGeometry`, so the same image lands correctly on the far
1221cap's full disc and on the sun cap's annulus.
1222
1223Two things had to be walked back after looking at it:
1224
1225- **It must be mipmapped.** A high-contrast geometric pattern is the worst case for
1226 point sampling. My first pass disabled mipmaps on the theory that resolving the
1227 checker at distance was the whole point; minified, the far cap moiréd into a dark
1228 cross-hatch that read as damage. Averaging to grey once the cells fall below a
1229 pixel is both correct and honest — you cannot resolve them from 540 km either.
1230- **Six rings, not ten.** 5 km cells looked better face-on from inside and collapsed
1231 from outside, where D-030 deliberately frames the caps near edge-on and
1232 foreshortens them ten to one. A pattern has to survive its worst viewing angle.
1233
1234The cap framing also moved to ~40° off the axis rather than ~17°, since there is now
1235something on the face worth turning to see, and the caps carry an emissive copy of
1236the pattern for the same reason the hull carries one of the terrain: sunlight runs
1237along the axis, so a cap is edge-on to every real light and renders as a dark disc
1238exactly when it is being looked at.
1239
1240## D-042 — Screenshots are taken through the UI, and diffed per shot
1241
1242`npm run screenshots` drives the real application in a headless browser and
1243photographs it: nine views across two viewports, plus a contact sheet of all nine.
1244
1245**Through the UI, on purpose.** Every shot clicks the view buttons, picks places out
1246of the list, and drags to orbit and to look up, rather than posing a renderer
1247directly. Posing is easier and it defeats the point. A screenshot taken by using the
1248application fails when the application fails, so a black canvas or a sidebar that
1249has swallowed the window ends up *in the picture* instead of hiding behind a passing
1250unit test. The tests already pin the canon numbers; nothing was checking that the
1251thing still draws.
1252
1253**The contact sheet is for a human.** `overview.png` exists to be looked at once,
1254before committing — one image that answers "is it still there?". It is built by
1255laying the shots out as an HTML page and screenshotting that, because the browser is
1256already running and already knows how to lay images out; `tools/png.mjs` only
1257encodes, and this project has no reason to gain a PNG decoder.
1258
1259**Diffed per shot, not per sheet.** The first version measured divergence on the
1260contact sheet, and that was the wrong unit: rewriting two of the nine panels
1261outright moved the whole-sheet mean by 0.6%, because a mean over nine panels on a
1262dark page dilutes everything. Per shot the same change reads in tens of percent.
1263
1264**The 3% threshold is measured, at both ends.** Seven of the ten shots come out
1265byte-identical run to run — the rain is the only randomised thing in Tower and is
1266seeded on the way in, the exterior spin is switched off, and each shot gets a fresh
1267browser context so no camera leaks into the next. The two exterior shots are the
1268exception, drifting up to 0.98% because OrbitControls damps toward its target
1269asymptotically and lands a hair short of the same place each time. Against that,
1270hiding both 3D canvases scored 7–31%; a view going entirely black scores 12% to 39%.
12713% is three times the worst noise and less than half the mildest real failure.
1272
1273It is a smoke alarm, not a verdict: when it fires the answer is to look at the
1274sheet. And it only catches *regressions* — a view that never worked scores 0% when
1275it breaks further, which is exactly how mobile-exterior behaved during the sabotage
1276test that calibrated this, because at the time it was already a black slot.
1277
1278**Two browsers, one tour.** The contact sheet gets its own browser instance. In the
1279one that has just taken the tour — nine WebGL contexts and two Three.js scenes —
1280`Page.captureScreenshot` on the sheet fails outright with "Unable to capture
1281screenshot", and the sheet is plain HTML and images, so the only thing wrong with it
1282is its neighbours.
1283
1284**What it found immediately.** The mobile row: the sidebar was a fixed 340 px
1285against a 390 px viewport, so the world got about fifty pixels and both 3D views
1286rendered into a black slot. Fixed in D-043, which the same tool then verified —
1287the six desktop shots moved 0.00% and the three mobile ones moved 10–33%, which is
1288exactly the shape a layout fix should have.
1289
1290## D-043 — On a phone the sidebar is a drawer
1291
1292The layout was one grid, `1fr 340px`, with no media query anywhere. On a 390 px
1293phone that is a 340 px panel and fifty pixels of world, and nothing to hide the
1294panel with — the application was, on a phone, a control surface for something you
1295could not see.
1296
1297**A drawer, not a stack.** The other obvious move is to put the panel below the
1298world in a single column and let the page scroll. That is wrong here: the world is a
1299`position: absolute` canvas filling `main`, all three views are drag-to-look, and
1300`touch-action: none` means every gesture over the world belongs to the world. A
1301scrolling page and a canvas that eats touches are in permanent conflict. A drawer
1302keeps the canvas full-bleed and the gesture unambiguous.
1303
1304**Below 720 px**, where the panel would otherwise be taking more than half the
1305window. Above it nothing changes at all — the desktop shots come out of the
1306screenshot tour byte-identical.
1307
1308**Never the full width.** `min(340px, 86vw)` leaves a strip of world showing, which
1309is what tells you the panel is over the map rather than a page you navigated to.
1310That strip is live canvas, though, and in writer mode a stray tap on it drops a
1311marker, so a scrim covers it and closes the drawer instead.
1312
1313**Two buttons, not one.** The opener is fixed at the top right, opposite the view
1314switcher. The closer is in the panel header, in normal flow. A single fixed
1315toggle would have to float over the panel's own scrolling content once open, and
1316would occlude whatever scrolled under it; this way each button is only ever over the
1317thing it belongs to. The spin readout, which also lives at the top right, drops to
1318`top: 54px` to clear the opener.
1319
1320**Picking a place closes it.** In every view selecting a place moves you: the map
1321centres, the exterior swings round, the interior walks there (D-038). On a phone the
1322list *is* the drawer, so staying open means watching none of it happen.
1323
1324**The closed drawer is `inert`.** It is only translated off-screen, so without that
1325its fields stay in the tab order — typing into a panel nobody can see, scrolling it
1326under the world. It is set from the same media query the CSS uses, and crossing that
1327breakpoint (a phone turned on its side) resets to closed, which is also what clears
1328`inert` on the way back to the desktop column.
1329
1330**One thing came along with it.** The standing readout is ~65 monospace characters
1331of aligned columns and overran 390 px at 11 px type. It shrinks to 9 px on the same
1332breakpoint rather than wrapping — wrapping those columns destroys the thing they are
1333for.
1334
1335## D-044 — Tower's wall is a kilometre thick
1336
1337Canon had no figure for it. Both 3D views drew the hull, the caps and the collector
1338as zero-thickness surfaces, which is invisible until something is seen edge-on and
1339then reads as a paper cutout rather than a structure.
1340
1341**1 km, floor to outer skin** — 2% of the radius. The alternatives were a 200 m
1342technical minimum, which is defensible on shielding and services and is sub-pixel at
1343every exterior zoom, and 5 km, which is unmistakable and a great deal of mass to
1344justify later. A kilometre reads as built without making Tower a pipe.
1345
1346**It is only visible at the openings**, which is worth stating because it took a
1347false start to notice. A closed can has no visible thickness from outside — the
1348silhouette of a 50 km cylinder does not change when you give its wall a kilometre.
1349Depth shows only where there is a hole: the beam aperture, and now the far cap
1350window. So the caps sit at the **outer** face of the wall with a 1 km collar
1351standing between them and the inner surface, and it is the collar you see down.
1352
1353**The exterior camera had to be let closer.** A kilometre is 1% of the cap's
1354diameter, so at the old 120 km minimum distance the collar was two pixels and the
1355near plane sat at 8 km — the depth was being drawn correctly and could not be got
1356close enough to to see. 60 km and a 2 km near plane make the choice legible; without
1357that change the constant would have been a number in a file and nothing else.
1358
1359## D-045 — The far cap is a window, and the checker becomes its frame
1360
1361*Amended by D-078: the mullions are a triangle lattice, not the polar tessellation.*
1362
1363§1.4 has said since the first draft that the caps are the only views out of Tower
1364and that the sky through them turns rather than sweeps — a pinwheel about the axis
1365with one motionless star at the middle, one revolution every 7 min 29 s. It was
1366written and never built: §2.4 said flatly "no stars", and D-041 had meanwhile made
1367the far cap a checkered structural plate.
1368
1369**Glazed whole, not windows at its foot.** §1.4 put the windows at the base of a
137050 km cliff. A strip at the bottom of something 540 km away is nearly nothing to
1371look at, and the canon fact worth having in the view is the pinwheel, which needs
1372sky. So the whole far cap is a window. The sun cap is not — it faces the sun and
1373needs glare management rather than a view, and that asymmetry is already canon.
1374
1375**The checker survives as the mullions.** Deleting it would have retired the only
1376object in Tower with a known cell size, which is the only rule anyone has for
1377judging distance (D-041, §1.8). Instead the same polar tessellation at the same
1378pitch is drawn as the frame around transparent panes: the cells are exactly where
1379they were, and you look through them rather than at them. The rule now reads against
1380the sky, which is a better place for it than against a plate.
1381
1382**The physics fell out of the shader that was already there.** A pane is black with
1383stars in it, so the existing haze integration does the rest by itself: by day the
1384airlight term swamps everything and the cap is a disc of blue, which is what §1.5
1385already said would happen to anything 540 km off; at night the airlight collapses
1386with the beam and what is left is the sky. Nothing needed special-casing.
1387
1388**The stars are not scaled by the exposure, and that is the whole point.** Nothing
1389Tower does to its own beam changes how bright Vega is. What does change is the eye,
1390so a separate gain carries that half: full at night and below, 2% at temperate. The
1391stars are there at noon and unseeable, which is a fact about eyes and not glass.
1392
1393**Two projections were wrong before the third.** Gnomonic (xy/z) about the axis puts
1394the pole star at the origin and blows up as the ray goes perpendicular to the axis —
1395which is the ordinary grazing view from the foot of the cap, not an edge case.
1396Painting the field on the cap plane instead is stable everywhere and has no fixed
1397angular scale: from a kilometre away the entire 60° field lands inside one star
1398cell and the sky comes out empty, which is exactly what the first screenshot showed.
1399Azimuthal-equidistant has neither failure. It is defined for every direction but
1400exactly sunward, stars keep the same angular size however far off the window is —
1401which is what being at infinity means — and the pinwheel is not animated but falls
1402out of the projection, because azimuth plus spin phase *is* the rotation.
1403
1404The pole star is placed rather than left to the hash. It is the fixed point the
1405whole phenomenon is read against, and a sky that happened not to roll one there
1406would be missing the only part of it anybody in Tower would have named.
1407
1408**Settled by D-048:** the surviving quarter of the beam does not go through the
1409window at all. It arrives collimated in a 3.2 km spot — a thousandth of the cap —
1410and that spot is a mirror, not glass.
1411
1412## D-046 — Night is the beam banked, not the swarm aimed away
1413
1414*Still true under D-074, where night begins when the sun goes out at the far cap.*
1415
1416The obvious way to make night in a habitat lit by a steerable mirror swarm is to
1417aim the swarm off. It does not work here, and §1.3's own numbers say so.
1418
1419The beam glows **by itself**: §1.3 gives the glow to the beam and the air rather
1420than to the diffusers, and says outright it "would be there if the beam were empty".
1421779 W/m²/sr over a strip 3.65° wide running horizon to horizon is about 0.2 sr, and
1422with the mean cosine of a strip through the zenith that is **~100 W/m² on the
1423floor** — half of temperate. A lit beam is a light source no matter where the
1424mirrorlets are pointed, so a dark world requires a dark beam. There is a test for
1425this figure in `test/canon.test.ts`, because the conclusion rests entirely on it.
1426
1427So **night is the beam banked to a thousandth of full, 0.32 W/m²**: a rod you can
1428still find overhead and a world you can just make out under it. And **dark is the
1429beam shut down** — no airlight, no lit ground, no rod, and the only thing left in
1430the world is the sky through the far cap (D-045). The two settings are genuinely
1431different states rather than two points on a dimmer, which is what made both worth
1432having.
1433
1434The beam's brightness scales with the exposure already, so it banks and goes out on
1435its own; neither setting needed the beam shader touched.
1436
1437## D-047 — The exterior hull reads two ways, and they are different claims
1438
1439*Amended by D-076, D-077 and D-081: Rock, Piping, and the two together are on trial beside True and X-ray.*
1440
1441The exterior had a "Show terrain" checkbox that wrapped the raster onto the hull.
1442It was doing something stronger than a checkbox admits: from outside Tower is
1443structure, so terrain on the outside is not what Tower looks like — it is a diagram
1444of what is inside, drawn on the outside because the wall is opaque. Useful, and not
1445a view. The two readings are different claims about the same object, so the control
1446is now a two-way switch that names both:
1447
1448- **True** — the outer skin. Bare hull, checkered sun cap, glazed far cap.
1449- **X-ray** — the inner surface projected onto the outside.
1450
1451**The wall is drawn as two meshes now, not one double-sided one.** Its faces show
1452different things: the outer skin changes with the mode and the inner surface is
1453always the land, and a single `DoubleSide` material has no way to say so. The inner
1454mesh is emissive for the same reason the x-ray always was — it is lit by a beam this
1455scene does not model, and unlit it reads as a hole rather than a world.
1456
1457**This is what made the far cap window worth having from outside.** D-045 glazed it
1458for the sake of the interior, where the payoff is the pinwheel. From outside the
1459payoff is different and better: the panes are clear, so in true mode you look in
1460through the glass and the world is really there, mountains and desert and lakes
1461receding 540 km down the tube behind a grid of mullions. It is the only line of
1462sight into Tower that is not a projection, and it exists only because the wall got a
1463window and the hull got two faces in the same week.
1464
1465The dark backing disc the first version of D-045 put behind the glazing is gone. It
1466was there to stop the panes showing the inside of the tube — which turns out to be
1467exactly the thing worth showing.
1468
1469## D-048 — The far cap's bullseye is a mirror
1470
1471**Decided by the author**, from four options: mirror it, let it out, absorb it
1472behind fins, or grade the swarm so it never arrives.
1473
1474**The shape of the load is the argument.** §1.3.1 leaves 24% of the beam
1475unscattered after 540 km, and it survives *collimated* — so this was never 13 TW
1476spread over a 7,854 km² cap. It is **1.7 MW/m² in a 7.96 km² spot**, about 1,200
1477suns, and an absorber there sits at **2,330 K**: survivable for tungsten, and for
1478no structure. But the spot is **a thousandth of the cap's area**, which means the
1479whole problem lives on a 3.2 km bullseye and 99.9% of the far cap needs nothing
1480done to it at all. That is what makes D-045's window and this coexist.
1481
1482**A mirror is the cheap thermal answer, not the expensive one.** At 99.9% it
1483absorbs 1,665 W/m² — about 1.2 Earth solar constants, ordinary sunlight. The thing
1484that would have glowed white is merely warm. The absorber-with-fins option needed
1485the entire cap radiating at 400 K *and* 13 TW moved 50 km sideways from the spot to
1486get there; it was strictly worse than every alternative.
1487
1488**It also recovers the energy, and fixes a number that was already wrong.** The
1489return trip makes the optical column 1,080 km rather than 540, so utilisation is
14901 - e^(-2τ) = **94%** against 76%. That matters more than it looks, because canon
1491had a contradiction sitting in it: §1.3 sets full brightness at 54.3 TW ÷ 169,646
1492km² = **320 W/m²**, which assumes the whole beam reaches the inner surface, while
1493§1.3.1's optical depth says only 76% does — implying **243 W/m²**. Both were
1494printed, and a test pinned 320. With the cavity closed at both ends (the mirror at
1495one, the collector at the other, returning what still escapes) essentially all
149654.3 TW is eventually absorbed, and **320 is right as it stands**. No test moved.
1497
1498**What it does not do is make weather.** Aiming light at a region to warm it stays a
1499separate lever, and §1.9.4 already says that is how every climate in Tower is made.
1500The mirror is about not wasting the beam; the Dry Reach is about where the heat is
1501wanted. Keeping them separate means neither constrains the other.
1502
1503**A geometric trap, recorded because I fell into it.** The mirror sits on the axis,
1504so it looks like it must occult the pinwheel's motionless centre — I wrote that into
1505canon before checking. It does not. The pinwheel turns about the direction
1506*parallel* to the axis, and that direction lands on the window at the observer's
1507**own radius**, not at the cap's middle. From the floor the spin axis is horizontal,
1508so the centre of Tower's only moving sky is a point on the **horizon**, dead ahead
1509at eye level — while the mirror is ~10° above it from 270 km. The two coincide only
1510for an observer standing on the axis, and nobody can stand there.
1511
1512## D-049 — Rain is not drawn any more
1513
1514Rain existed in the interior view for one reason: with the hull sealed it was the
1515only moving thing in the sky, and therefore the only evidence Tower turns. §1.9.2
1516then undercut it — drag pins a drop's speed, so rain leans a fixed **1.5°** whatever
1517the cloud, against a waterfall's 11°. The readout printed the angle to stop the
1518smallness reading as a bug, which is an admission that the picture was not carrying
1519the claim.
1520
1521D-045 gave the view a whole sky going round once every 7 min 29 s. Against that, a
15221.5° lean is not evidence of anything, and 6,000 line segments were being stepped
1523every frame to fail to show it.
1524
1525**The physics stays.** `canon/motion.ts` still computes the lean and
1526`test/motion.test.ts` still checks it — it is a real fact about how anything falls
1527in here, and the waterfall is still the compass. It is only the *rendering* that is
1528gone, along with the toggle, the seeded raindrop field, and the six Three imports
1529that existed for it. The readout line it occupied now prints the pinwheel's period,
1530which is the interval you can actually time against the sky.
1531
1532## D-050 — The sun cap is a window too, and Tower never sees the sun
1533
1534**§1.4 was wrong about this cap, and the number that shows it was already in canon.**
1535It demanded "heavy glare management" on the grounds that the sun cap faces the sun.
1536It does not face the sun. It faces the **collector**, which is 244 km across at 320
1537km — **41.8° wide against the sun's 0.53°, seventy-nine times its diameter**. The
1538collector sits between Tower and the sun by construction, so it occults it entirely,
1539and the umbra is 120 km across where Tower's 51 km sits in it. There is no direct
1540sunlight on the sun cap and there never has been. Nothing to manage.
1541
1542So this cap is glazed on the same terms as the far one, and what fills it is the
1543**back of the collector**: a black disc, because its front is a mirror and its back
1544radiates 9.6 TW of waste heat at about 245 K, which is infrared. Around the edge of
1545it, stars. It does not turn with them — the collector flies in formation rather than
1546spinning — so the sun cap window is a still black disc against a wheeling sky.
1547
1548**Nobody in Tower can see the sun.** They already could not see Earth (§1.4, the
1549terminator orbit puts it out the side, which is solid hull). A habitat in orbit
1550around a planet it has never seen, lit by a star it has never seen.
1551
1552The occultation is computed in the shader rather than modelled: the fragment casts
1553its own ray on to the collector's plane and asks whether it lands on the disc. That
1554is the same trick the far cap uses for its stars, and it means the interior does not
1555have to carry a 244 km mesh 320 km outside the world to draw one black circle.
1556
1557The interior's checkered cap material is gone with this. Both caps are windows now,
1558and nothing inside was still using it — the checker survives as the exterior's sun
1559cap and as both windows' mullions.
1560
1561## D-051 — The range rings the world
1562
1563The mountains were "a 140 km arc, not a ring" — four named domes covering about a
1564third of the circumference, with the other two thirds simply unauthored. Asked to
1565close it, and closing it is better than the arc was: a ring of peaks is a *watershed*
1566that divides the whole world rather than a wall you can walk around the end of.
1567
1568**The ridge op grows optional ridged noise**, which is what turns a ridge into a
1569range. Without it a ring band is a smooth swell all the way round — right for the
1570three low walls across the waist, wrong for peaks.
1571
1572**It blends with `max` rather than adding**, and that is not a detail. Added, The
1573Girdle's 2,900 m would stack onto Snowcrown's 4,300 and push the massif to 6,900 —
1574past the raster's 4,400 m ceiling, where it would clip flat. Taking the greater of
1575the two leaves the named summits as the high points of their own range, which is
1576what they are: Snowcrown is not a separate mountain from The Girdle, it is the tall
1577part of it.
1578
1579Peaks reach about 3,900 m, so they cross the 3,000 m snowline and the ring reads as
1580snow the whole way round. The rivers still cut it, because carve runs after relief.
1581
1582## D-052 — A fourth noise octave, and colour that mottles instead of dimming
1583
1584**Height.** The base had three noise layers — 78 km, 12 km, 3.4 km — and a pixel is
1585264 m. Between the finest layer and the raster's ~530 m Nyquist limit there was an
1586octave and a half of nothing, so the ground read as smooth swells with no grain on
1587them at exactly the scale you look at it from inside. A fourth layer at 1.5 km and
1588±26 m fills that band. It goes in the *base*, deliberately: carve and flood run
1589afterwards, so rivers still cut through it and lakes keep flat surfaces rather than
1590inheriting a rippled floor.
1591
1592**Colour.** There was already per-pixel variation, and it was the wrong kind — one
1593noise field scaling all three channels together, which dims and brightens without
1594ever changing the colour. A second, finer field now **mottles**: it pushes the
1595channels apart, green moving most and blue least, so ground reads as patchy
1596vegetation against bare soil rather than as one paint turned up and down. Keeping
1597blue nearly fixed is what stops the desert drifting toward the same green as the
1598forest.
1599
1600**It exposed a live bug, and the fix matters more than the noise does.** The extra
1601relief pushed the terrain to **-521 m** against a declared floor of **-520 m**, so
1602every pixel down there was being silently flattened. The generator printed a warning
1603and carried on; nothing else noticed. That is D-024's failure mode exactly — a
1604clipped raster displaces terrain rather than breaking it, so nothing downstream
1605complains.
1606
1607Two changes: the floor moves to **-560 m**, and `npm run terrain` now **exits
1608non-zero** when any pixel is clamped instead of printing a line into a log nobody
1609reads. The unit test could never have caught this — it compares the declaration
1610against extremes written down when they were last true, not against the terrain that
1611just came out — and it now says so where it sits.
1612
1613## D-053 — The day is a front, not a dimmer
1614
1615*Superseded by D-074: the day is a sun that crosses the world, not a front.*
1616
1617The obvious day-night cycle is a global fade between the light settings. It would
1618have worked and it would have thrown away the most interesting thing about how this
1619world is lit.
1620
1621**What the physics allows.** D-046 already established that the swarm cannot make
1622night, because §1.3 gives the beam's glow to the beam and the air rather than to the
1623diffusers — a lit beam lights the world whatever the mirrorlets are doing. So beam
1624power has to ramp, and when it does, the sky brightens *everywhere at once*. There
1625is no way to have one end of the axis get its morning before the other.
1626
1627**What the swarm can still do is choose where the light lands.** The diffusers hand
1628beam light to the ground, and they are individually flown with absurd control
1629authority (§1.3.1). So the ground can be lit progressively even though the sky
1630cannot, and the natural direction is the one the beam already runs: sun cap to far
1631cap.
1632
1633**A Tower dawn is therefore a fully lit sky over dark ground**, with a line of
1634daylight travelling toward you at 225 m/s. That is not a compromise between the two
1635options, it is what falls out of taking both constraints seriously — and it is a
1636better image than either.
1637
1638**It gives Tower longitude.** §1.9.4 says the world has no latitude: every point is
1639equidistant from the light, which is why nothing about it varies. The cycle adds the
1640axis back as 40 minutes of time difference between the caps. Dusk runs the same
1641direction, so the sun-cap end has the shorter day by a hair.
1642
1643**The panel gets a time slider before it gets a clock.** Running the cycle is the
1644feature, but a 24-hour day at any watchable rate means waiting for dawn, and dawn
1645lasts 40 world-minutes out of 1,440. Being able to scrub straight to 06:17 is what
1646makes the front usable at all — and the front is a pure function of the phase, so
1647scrubbing and ticking are the same operation.
1648
1649The readout names the distance to the line rather than just the hour, because the
1650distance is the part you can act on: 27 km sunward is about two minutes' wait.
1651
1652## D-054 — The collector gets a lit rim
1653
1654Drawing the collector as pure black was right about its face and wrong about the
1655object. From inside the sun cap window at night you could not see it at all — only
1656that a piece of the sky was missing, which reads as a rendering hole rather than as
1657a 244 km structure.
1658
1659**The physics gives the rim for free.** The collector is backlit by definition: its
1660mirror face is pointed at the sun and away from Tower. Anything holding 46,938 km²
1661flat has structure standing proud of the mirror plane at the edge, and at the edge
1662the sun strikes that structure side-on. Add the scatter every real mirror throws at
1663grazing incidence and the boundary is a thin sun-coloured ring. That is simply how
1664backlit things look, and it is the one part of the collector that can be lit.
1665
1666**What it is not is sunlight past the edge**, and the distinction matters because the
1667first is a property of the object and the second would break canon. Tower sits wholly
1668inside the umbra — §1.3.0 — so the sun's disc is occulted from every point in the
1669world. Nobody sees a sliver of star; they see lit metal.
1670
1671Rendered as two exponentials: a tight core at the declared 4 km of edge structure,
1672and a wide dim skirt for the scatter around it. One alone reads as a drawn outline
1673rather than as light. The first attempt was three times too bright and its skirt
1674seven times too wide, which washed the whole disc gold instead of tracing it.
1675
1676**It does not scale with the beam.** The sun does not dim when Tower banks its own
1677lamp, so the rim is outside `uExposure`, like the stars. It does fade as the eye
1678adapts, but from a much higher floor than a star's — this is sunlit structure, and
1679it stays faintly visible at noon where a star cannot.
1680
1681## D-055 — The collector looks the same from outside as it does from inside
1682
1683The exterior view drew the collector as a near-white disc with a pale blue glow, and
1684the sun cap window drew it as black with a lit rim (D-054). Both cannot be right, and
1685the window is the one that follows from the physics: the collector's mirror face is
1686pointed at the sun and away from Tower, so what the world sees is the back of it, a
168746,938 km² radiator running at 245 K. That is infrared. In visible light it is black.
1688
1689**So the exterior draws two faces rather than one.** The disc is two meshes sharing a
1690plane, one front-facing and one back-facing, and the camera's own position picks which
1691one it gets — culling does the choosing, so they never fight over the same depth.
1692
1693- The Tower side is very dark and unlit, with just enough emissive to keep it a
1694 surface. Not zero: against black sky, a wholly unlit disc is not a dark object, it
1695 is a missing one.
1696- The sun side is bare metal at almost no roughness, which under one directional light
1697 is dark everywhere except a specular glint. That is what a flat mirror in vacuum
1698 actually looks like: it shows the empty sky in front of it, not the sun behind you.
1699
1700**The rim is what makes it an object.** The 4 km of sunlit edge structure from D-054 is
1701drawn here as a ring of geometry rather than as two exponentials in a shader, because
1702out here it is being seen rather than looked through — and it is placed just outboard
1703of the mirror, not overlapping its edge, so the two surfaces never share a depth
1704either. It reads from both sides, which is right: it is structure, not a reflection.
1705
1706The cost is that the largest object in the scene is now a dark one. The label, the
1707beam converging on it and the lit rim carry it — and being hard to see is the point,
1708since Tower has never seen this thing lit.
1709
1710## D-056 — Stars are drawn a pixel wide, so the sky stops flickering
1711
1712*Superseded by D-075: the stars are now the real catalogue; the pixel-wide gaussian stays.*
1713
1714The star field was a spike: brightness `(1 - d/r)^4` over a radius of about 0.11°,
1715which on a retina panel at the default field is a core roughly one pixel across. The
1716sky is sampled once per pixel, at the pixel's centre. So what got drawn was that spike
1717read at one arbitrary point along it, and half a pixel of head movement moved the
1718reading from the peak to a third of it. Every star did that independently, every
1719frame, and the whole sky boiled whenever anyone looked around.
1720
1721**That is wrong twice over.** Vacuum has no air to twinkle through — the sun cap
1722window looks at deep space and the far cap at the anti-sun sky, and neither has an
1723atmosphere in front of it. And the interior view's whole claim is that nothing in
1724Tower moves except the pinwheel: a sky that seethes contradicts the one thing §1.4
1725promises about it.
1726
1727**The star becomes a gaussian one pixel wide.** Sigma is tied to the pixel footprint,
1728so a star is one pixel on any display and at any zoom, and a curve that smooth barely
1729changes between neighbouring samples. This is not a compromise with the physics; it is
1730closer to it. No optic renders a point source as a point — it renders it as a blob the
1731width of its own resolution, and here the resolution is the pixel.
1732
1733The footprint is measured from the view direction rather than from the projected star
1734coordinates, because those carry the branch cut of an `atan` and a derivative across
1735that seam is a line of garbage down the middle of the sky.
1736
1737**Measured, facing sunward from The Capital at night on a 2x display, turning one
1738pixel at a time over sixteen frames:** the light in star-like points varied by 8.1% of
1739its mean before and 5.0% after, and the count of them by 5.2% before and 3.9% after.
1740About a quarter more stars are visible at any moment — not because the field got
1741denser, but because a star no longer has to land near a pixel centre to be seen at its
1742true brightness. The variation that remains is mostly stars genuinely entering and
1743leaving the frame as the view turns, which is not flicker; per star, the worst case
1744went from a factor of three between neighbouring samples to about 16%.
1745
1746## D-057 — The height raster carries no mipmaps, because the vertex stage cannot choose one
1747
1748Firefox on Apple Silicon rendered the interior wrongly, intermittently: the terrain
1749lost its colour to a flat blue-grey wash and its relief to a coarse grid of creases,
1750and it came back on a hard reload. It followed where the view was pointed rather than
1751where the observer stood — changing eye height did not clear it — which is what made
1752it read as a bug about *rotation* rather than about a texture.
1753
1754**The cause is a texture fetch in a vertex shader that never named its mip level.**
1755`elevation()` in the ground shader reads the height raster once per cell corner, in
1756the vertex stage. A vertex shader has no derivatives, so there is nothing for an
1757implicit `texture2D` to compute a level of detail *from* — the level is whatever the
1758driver decides, and the driver is entitled to decide anything. The raster was loaded
1759with Three's defaults, which means generated mipmaps, and with the card's maximum
1760anisotropy on top: a sampler with eleven levels and an ambiguous rule for picking one.
1761On a driver that picks badly the shell is displaced by a downsampled height field,
1762which flattens the terrain into blocks and shows the block seams as creases, and the
1763colour raster read at the same wrong scale averages into the flat wash. Every symptom
1764follows, including the reload: a fresh context re-uploads and re-mips the textures.
1765
1766**The ambiguity is removed at the sampler rather than at the fetch.** The obvious
1767guard is to name the level in the shader, and it is not available here — the
1768vertex-only `texture2DLod` does not survive Three's WebGL2 shader prefix, and asking
1769for it fails the program link outright, which trades an intermittent bug for a
1770permanent black world. So the height raster is built with `generateMipmaps` off,
1771`LinearFilter` at both ends and anisotropy 1: one level, nothing to choose between.
1772
1773This costs nothing. The height raster is read at a fixed rate — one texel per 0.7 km
1774cell corner — that has no relationship to how the cell lands on screen, so mipmaps and
1775anisotropy were never doing anything for it but occupying a third more texture memory.
1776The **colour** raster keeps both, and should: it is read in the fragment shader, at
1777every grazing angle the inside of a cylinder can offer, which is exactly what
1778anisotropic filtering is for.
1779
1780**Honest about the evidence.** The failure was not reproduced on demand — four hundred
1781scripted pan steps in Firefox on the same machine rendered correctly throughout, with
1782no WebGL warnings and no context loss. The diagnosis is fitted to the artefact: the
1783reported view was reproduced exactly from its saved state, and the difference between
1784what it should show and what was photographed is specifically the two rasters, not the
1785camera, the depth buffer or the geometry. If it recurs, the next thing to drop is the
1786anisotropy on the colour raster, which would move the suspicion from the vertex stage
1787to the sampler state as a whole.
1788
1789## D-058 — The interior keeps a flight recorder
1790
1791D-057 was diagnosed from a photograph. That worked, and it should not have had to:
1792the fault was intermittent, it was reproduced by the author and never once by the
1793tooling, and every question about it — was the geometry there, was the camera sane,
1794were the textures still live — had to be answered by inference from a screenshot
1795rather than by reading the number. So the interior now records what it is doing.
1796
1797**A ring of the last nine hundred frames**, about a quarter of a minute: camera,
1798heading, field, where the observer stands, draw calls, triangle count, and every
1799fifteenth frame a readback of a 64-pixel block from the middle of the frame. The
1800readback has to happen immediately after `render`, while the drawing buffer is still
1801the current target; a first attempt read it from a 2D canvas a moment later and got
1802nothing but zeroes, which looked for a while like a finding of its own.
1803
1804**`towerDump()` on the console prints the lot and copies it to the clipboard.** It is
1805present in the built site, not only in dev, because this fault has never once appeared
1806while anybody was looking for it. It is two properties on `window` that do nothing
1807until they are called.
1808
1809**The line that actually answers the question draws each surface alone.** A flat,
1810colourless frame can be the ground shader losing its raster, or the ground missing
1811altogether with a cap or the bare clear colour behind it — indistinguishable on
1812screen, entirely different faults. Rendering the ground, the two caps, the bore and
1813the beam one at a time and measuring each separates them in a single dump. The clear
1814colour reads 19 in chroma, so 19 means *that surface drew nothing here*; the ground
1815reads about 30 when it is carrying land. Verified by taking the colour raster away
1816from the ground shader mid-flight: ground fell from 31.3 to 19.5 while the far cap
1817stayed at 31.3, which is exactly the signature to look for.
1818
1819**There is deliberately no automatic alarm, and that is a result rather than a gap.**
1820The first version warned when the middle of the frame lost its colour, on the theory
1821that the fault paints a neutral wash. It does not. It paints the *haze* tint, and the
1822haze tint is itself a blue-grey measuring 19 — while a legitimate view of the far side
1823through 200 km of air measures about 20, against about 30 for land underfoot. Any
1824threshold that caught the fault would have fired every time anyone looked at the
1825horizon. The recorder therefore records and the human decides; a quarter-minute of
1826history is long enough that noticing the fault, reaching for the console and typing
1827still catches the frames on both sides of the change.
1828
1829## D-059 — Inside, the finger count is the mode
1830
1831*Amended by D-083: the pinch no longer zooms and the wheel moves rather than zooms; the field of view is fixed at 60°.*
1832
1833Until now the interior could not be moved through at all. Dragging looked around,
1834the wheel zoomed, and the only way to change where you stood was to select a place
1835and be walked there (D-038). That is fine for the places that exist and useless for
1836the ground between them, and on a phone it left the whole view as something to be
1837turned on the spot.
1838
1839**The gesture had to carry the mode, because a switch would have to live on the
1840world.** The alternatives were considered and dropped. A Look/Move toggle in the
1841corner is the CAD convention and it is honest, but it is a second pill bar over a
1842canvas that is already carrying the view switcher, the standing readout and the
1843place captions, and on a phone that is the scarcest surface in the application
1844(D-043). A virtual joystick is the mobile-FPS convention and reads as a game rather
1845than a map. So: **one pointer looks, two move.** The count is the mode, nothing is
1846drawn, and the gesture is the one every map on the device already uses.
1847
1848**The cost is discoverability, and it is paid in the panel.** A gesture with no
1849chrome is a gesture nobody finds, so the interior hint now leads with it in bold
1850rather than mentioning it in passing. This is the known trade and it was taken
1851deliberately.
1852
1853**A mouse has only ever one pointer**, so shift-drag or the middle button stands in
1854for the second finger. Without that the feature would exist only on touchscreens,
1855which is not what was wrong.
1856
1857**Moving means grabbing the ground, not steering.** The ray through the pointer is
1858met with the ground plane before the move and after it, and the observer is shifted
1859by the difference — so the patch of land under the finger stays under the finger.
1860The point of doing it this way is that no speed has to be chosen: the distance
1861covered falls out of the eye height and the field of view, and the same drag is a
1862few metres of pavement at 1.7 m and a hundred kilometres of plain from 10 km up.
1863Tower is 540 km deep and 314 km around, and no fixed rate is right at both ends of
1864that.
1865
1866**Two caps keep it from flinging.** A ray at the horizon meets the ground at
1867infinity and one above it never does, so the grab distance is capped at 1,200 eye
1868heights and at 60 km outright. Both numbers were measured rather than guessed, by
1869dragging the running view a screen at a time at five eye heights: a screen of level
1870drag carries 1.9 km at a standing 1.7 m, and 56 km from 100 m up, where the ceiling
1871has taken over. The ceiling sits above the honest grab distance from the highest
1872the eye goes — 25 km up, tilted well down, meets the ground about 60 km out — so it
1873only ever bites near the skyline and never lags the finger on a view aimed at the
1874ground. A first pass capped at 150 eye heights and a screen of drag moved four
1875metres, which is how the numbers came to be measured.
1876
1877**The tilt is the gearing, and it was not arranged.** Looking level grabs the far
1878ground and covers kilometres; looking down grabs what is underfoot and covers
1879metres — 26 m to the screen at standing height, tilted 23° down. That falls out of
1880grabbing the ground rather than being a control, which is the argument for the
1881whole approach: no speed had to be chosen and none can be got wrong.
1882
1883**The grab plane is flat, deliberately.** Meeting the ray with the terrain instead
1884would make the grab point jump as the ray crossed a ridge half a screen away, and
1885throw the observer with it. The plane through the observer's feet keeps the drag
1886smooth, and the height they actually stand at is resolved afterwards, as it always
1887was.
1888
1889**Both readings of the pointer set are taken over the same set.** The centroid is
1890sampled before the moving pointer is folded in and after, never across a finger
1891arriving or leaving — otherwise the second finger touching down would jerk the
1892world by half the distance between the two.
1893
1894## D-060 — The exterior looks in at the interior's own world
1895
1896**Implemented at the author's request, three changes at once.**
1897
1898**The place markers are gone from the exterior.** They were coloured spheres on the
1899hull, and they had become a crutch the view no longer needs: the sidebar lists the
1900places, the map shows them, and selecting one still swings the exterior camera round
1901to it (D-038). With the markers went their outline shells and their per-frame
1902scaling. A cap is still framed from 40° off its axis, which now also means looking
1903in through its window.
1904
1905**Looking in from outside now shows the interior view's world, not a sketch of it.**
1906Before this the exterior had its own inside: an emissive terrain texture on the back
1907of the hull, no haze, and a checkered sun cap that was opaque. The interior had
1908glazed that cap long ago (D-050), so the two views disagreed about the one surface
1909you could see from both. The fix is not to copy the interior's look but to use it:
1910the ground, both windows, the bore and the beam move out of `interior.ts` into
1911`interior-model.ts`, and both views add the same meshes with the same shaders.
1912
1913Two things had to change for the shaders to be looked at from outside.
1914
1915- **The haze integral clips to the air.** It used to integrate the whole segment from
1916 eye to fragment, which is right when the eye is inside and absurd when it is 300 km
1917 out in vacuum. The segment is now cut to the part inside the tube — radius out to
1918 the deepest lake floor, z from the sun cap to the far cap's glass — before it is
1919 sampled. From inside that is the whole segment, so the interior renders as before;
1920 the only difference is the density is held at floor value rather than rising past
1921 it below the datum.
1922- **The windows are see-through from outside.** Inside, a pane paints the computed
1923 sky and is opaque to the renderer. From outside the same shader writes the pane
1924 transparent, carrying only the airlight in front of it, and blends with whatever is
1925 really behind: the land beyond the near cap, and the real collector and stars beyond
1926 the far one down the tube. Mullions stay opaque and hazed. Alpha is taken on the
1927 green channel, where canon quotes extinction.
1928
1929The exterior turns its hull and the air turns with it, so the shaders work in the
1930tower's frame: `uFrame` carries world into tower coordinates and the exterior updates
1931it each frame after the spin. Inside, nothing turns and it is the identity.
1932
1933**The blue light on the collector was the fill lights, reflected.** D-055 made the
1934collector's sun face polished metal under the scene's lights, expecting a single sun
1935glint. But the scene also carries two fill lights that exist only to make the hull
1936readable (§2.4), and a mirror reflects them as faithfully as it reflects the sun: at
1937any angle lined up with the bluish rim fill, a blue-white sweep crossed the disc.
1938Reproduced by aiming the camera along that light's reflection. A standard material
1939cannot be told to ignore some lights, so the face is now a small shader that knows
1940exactly one: the same GGX lobe Three drew for the sun at the same roughness, over the
1941same dark base, and nothing for the fills.
1942
1943The consequence is worth writing down, because it surprised the author: **the bright
1944band that had been read as the sun's reflection was the fill light too**, white at
1945its core where it saturated. The sun is on the axis behind the collector, so its
1946real glint only appears when looking nearly straight down that axis from sunward.
1947
1948**The exterior's stars are the interior's.** The old field was 3,500 points 26 km
1949wide at 18,000 km — about a pixel each, and on a retina display simply absent. The
1950sky is now a sphere around the scene painted with the windows' own star field
1951(D-056), sampled by direction from the camera so the stars are at infinity and hold
1952still while it orbits. The pinwheel phase is zero, since the camera is not riding the
1953hull, and the gain is full, since there is no daylight out here to adapt to. The
1954same pole star sits on the deepward axis in both views.
1955
1956**Showing the whole sky exposed that the field was never uniform.** It was laid out
1957in an azimuthal-equidistant projection about the axis, which spends area as θ dθ dφ
1958where the sky has sin θ dθ dφ: stars per square degree grew as θ / sin θ, correct at
1959the deepward pole, half again by the equator and without bound sunward, where they
1960also shrank into grey dust. The windows had only ever shown narrow parts of it. The
1961author saw it at once from outside — sparse bright stars one way, a haze of faint
1962ones the other. Stars are now scattered in 3D instead: a cubic lattice of cells each
1963rolls a star, and only stars inside a one-cell shell around the eye count, which
1964holds the same number of cells per steradian in every direction. The pinwheel is
1965kept by turning the direction about z before the lookup, and the density matches
1966what the old field had at its pole, so the far cap window looks as it did.
1967
1968## D-061 — The exterior opens on the true hull, and remembers the choice
1969
1970**Implemented at the author's request.** X-ray was the default because, before D-060,
1971the true hull showed little: a bare cylinder, a checkered cap, and land only through
1972the far window. Now both caps are windows onto the hazed interior, so the true hull is
1973the better first look, and it is what Tower actually looks like.
1974
1975**The choice is remembered** in localStorage under `tower.exteriorHull`, alongside the
1976active view (D-025) and the camera. It is restored into the Hull control at startup,
1977before the exterior is built, because the exterior reads its reading from that control
1978when it is first constructed. Switching to another view and back never rebuilds the
1979exterior, so the reading survives that as well. An unreadable or unknown stored value
1980falls back to true. The screenshot script selects a reading explicitly for each shot,
1981in a fresh browser context, so it is unaffected.
1982
1983---
1984
1985## D-062 — The world generates itself, live, from features
1986
1987**Decided by the author** (2026-09-16), after two rounds of answers below. Full design in
1988`world-generation.md`; Phase 1 is D-063.
1989
1990The terrain stops being two baked images. `content/geography.json` and `world.json`
1991merge into one world file of typed features — mountains, lakes, rivers, seas, desert,
1992forest, city, roads and paths — and the browser generates the world from it in web
1993workers, on load and on every edit, streaming detail in around the observer. Trees,
1994buildings, water surfaces with waves and flow, and ships on the cap seas become real
1995geometry.
1996
1997**What it reverses.** D-006 chose "real geometry, simple ground" and ruled out scenery,
1998settlements and vegetation; this adds all three. D-019's build step, its PNG rasters
1999and its no-dependencies rule go, along with D-021, D-035, D-039 and D-057, which exist
2000only because of the rasters. `design.md` §2.4's "water has no surface" and §4's
2001"procedural scenery, settlements" out-of-scope line are reversed. These are listed in
2002`world-generation.md` §11. The author agreed to all of it by asking for the build to
2003begin.
2004
2005**Author's answers, 2026-09-16** (`world-generation.md` §12.1). Nothing inside Tower is
2006a remnant: every landform, including the desert's dry washes and shorelines, is there
2007because Tower chose to quote something it saw on Earth or elsewhere. Wind is Tower's
2008published weather plan, which it revises when it likes — storms are scheduled, and
2009cancelled if someone foolish sails into one. Settlements are generated to fill the
2010world. Trees are real size, with a tall forest of 90–115 m giants as a landmark. A
20113–8 s compile behind a Terraria-style status dialog is acceptable. Phones get the same
2012world at full density, with shorter draw distances. Second round: days count from 0; the seed lives in
2013the world file, so the world is identical for every reader; generated settlements are
2014named for their size; the tall forest is placed at random near the capital once and
2015then fixed; the status dialog lines are approved.
2016
2017---
2018
2019## D-063 — Phase 1: the ground is generated, and the rivers had to be engineered
2020
2021**Built.** `world-generation.md` phase 1: world file v2 and the one-off port of v1's
2022geography; the world compile and the elevation function (`src/terrain/`), in a pool of
2023web workers; the ground as a quadtree of tiles; the status dialog; keyboard flight; and
2024the map and the x-ray hull drawn from a picture rendered live from the features.
2025`tools/terrain.mjs`, its PNG encoder and noise, the three terrain PNGs, `raster.ts` and
2026`content/geography.json` are gone. Two runtime dependencies came in, **simplex-noise**
2027and **comlink**; the other three the plan adopts wait for the phases that need them.
2028
2029**One `geometry` field for every kind.** The plan sketched kind-specific fields —
2030`area`, `shore`, `course`. As built, every feature has `geometry` and its parameters
2031sit beside it, so the map, the list, the editor and the permalinks needed no per-kind
2032code. Parameters arrive with the phase whose generator reads them.
2033
2034**The rivers cross a lowland they cannot get out of, and the profile had to be chosen.**
2035v1 authored a 60 m lowland between the Girdle and the Dry Reach and a 800–1,100 m
2036plateau beyond it, and painted its rivers across at about 1,000 m. Generated honestly,
2037a river cannot do that. Three profiles were tried against the real ground:
2038
2039| Profile | Result |
2040|---|---|
2041| clamped to the ground | every river falls to 120 m in the lowland and cuts a 900 m canyon through 100 km of desert |
2042| graded curve, source to mouth | a 1,300 m embankment across the lowland |
2043| **least earthwork** (isotonic regression, ≥ 2 m/km) | The Narrows cut 300–500 m deep, as §1.11 says; up to 880 m of embankment under each river |
2044
2045The third is built. It is the only one of the three that is a rule rather than a guess —
2046Tower moves as little ground as it can — and it reproduces the canyon canon already
2047described. The embankments are the price, and they are real in the render. Whether the
2048world should change instead (raise the lowland, or let it hold a lake) is recorded as an
2049open question in design.md §1.11: that is a decision about Tower, not about the code.
2050
2051**Tiles were being measured against the tallest mountain.** A fixed 4.5 km allowance for
2052relief made every tile within 5 km of the eye look near enough to split, all the way to
2053level 10 — 11,000 tiles on screen. Each tile is now bounded by the relief it actually
2054has, and the split distance was measured down from 2.4 to 1.8 tile-widths: 1.9 M
2055triangles and about 200 draw calls, settled in 1.4 s at the Capital.
2056
2057**A coarse tile cannot show an 80 m river.** One vertex of a 500 m tile falling in a
2058channel painted a 500 m blue diamond, and the far side of the world wore a dotted line
2059down every river. Water now weighs by how much of the vertex's spacing it covers.
2060
2061**The x-ray hull had been mirrored end to end since D-024.** Three's cylinder puts v = 1
2062at the far cap after the rotation, and the UV rewrite flipped it, so the Girdle's snow
2063was drawn at the far cap. The committed v1 screenshot shows it. Fixed with the rest,
2064since the picture on the hull changed anyway.
2065
2066**Readers pay a compile on every visit, and it is short.** About 120 ms to compile and
20671.2 s to draw the map, so the status dialog rarely appears on the author's laptop; it
2068will on slower machines, and it will once erosion arrives in phase 2. A production
2069build reaches a fully detailed interior 4.3 s after a cold load.
2070
2071---
2072
2073## D-064 — The Uplands, a hull a kilometre thick, and rivers that prefer to cut
2074
2075**The author's call on D-063's open question: raise the lowland.** A region can now
2076carry an uplift, and **The Uplands** (provisional name) lift the ring of ground between
2077the Girdle and the Dry Reach — z 105 to 150 km — to about 1,000 m. A raised region lifts
2078the ground *to* a height rather than *by* one: it takes the higher of itself and what is
2079already there. Added, it stacked onto the plateau's rising edge and moved the river's
2080problem ten kilometres deepward instead of solving it; taken as a maximum, it meets the
2081plateau and stops.
2082
2083Measured before choosing, as each river's worst embankment:
2084
2085| | worst fill | The Narrows cut |
2086|---|---|---|
2087| v1's lowland | 895–980 m | ~670 m |
2088| Uplands at 1,000 m, least-squares profile | 240–350 m | ~390 m |
2089| Uplands at 1,000 m, **0.2-quantile profile** | **93–199 m** | ~540 m |
2090
2091**The profile now prefers cutting to filling.** Least squares treats a metre of
2092embankment like a metre of cutting, and so balances the two; but a cut through high
2093ground reads as a valley and a 300 m bank across a plain reads as a dam. Fitting the
20940.2-quantile instead keeps the water below four-fifths of the ground in any reach. The
2095Narrows comes out a little past design.md's "up to 480 m" at about 540 m, which is
2096recorded rather than tuned away.
2097
2098**The hull is a kilometre thick all the way round.** D-044 gave Tower a 1 km wall and
2099drew it only at the caps, reasoning that a closed can shows no thickness from outside.
2100That held while the ground never went far below the floor. Generated sea floors reach
2101330 m down and the tiles hang skirts below that, and with the outer skin at the floor's
2102own radius they came through the hull as a lattice of lines (the author's screenshot).
2103The skin now sits a kilometre out, rings close the wall at both ends, and nothing
2104generated may go deeper than 950 m below the datum.
2105
2106**Two smaller faults found on the way.**
2107
2108- *A `#view=` link built its 3D view twice.* Reading the hash and choosing the initial
2109 view each started the deferred import before either had finished, so the exterior
2110 drew every caption twice and the interior would have run two renderers and two sets
2111 of tiles. The import is now shared.
2112- *Rivers drew as dotted lines at a distance.* D-063 weighted water by the channel's
2113 width over the vertex spacing, but only at vertices that landed in the channel. It
2114 now weighs every vertex by the share of its own cell the channel covers, so a coarse
2115 tile draws a river as a faint continuous line.
2116
2117---
2118
2119## D-065 — Mountains are summits, ridges and valleys water cut
2120
2121**Built.** `world-generation.md` phase 2, in `src/terrain/mountains.ts`; the full account
2122of what changed from the plan is §16 there. In short:
2123
2124- Summits — authored ones held exactly, generated ones lower than the named summit near
2125 them — are triangulated with **delaunator**, and the minimum spanning tree plus some
2126 short spurs become ridges with saddles.
2127- Ridges are raised into an uplift field with broad, gently concave flanks, blended by a
2128 soft maximum so neighbouring ridges do not meet along straight creases.
2129- The field is eroded to the **stream-power steady state**, computed directly over a
2130 drainage network found by priority flood, rather than by iterating the implicit solver
2131 the plan named. Iterating cut almost nothing in a reasonable time, and what it cut ran
2132 along the grid. A roughened start, per-pass jitter, and a 1 km² channel threshold with
2133 hillslope diffusion are what keep the grid from showing.
2134- The steady state is scaled back to the authored field over 8 km, so the massifs keep
2135 the author's heights and gain the water's structure.
2136- Overlapping ranges are eroded as one group.
2137
2138**Named summits are the top of their ground, and that took four steps**, because every
2139earlier one broke the one before: scale the surroundings down smoothly if anything is
2140higher, pin the summit, trim the few cells pinning lifted past a gentle cone, pin again.
2141A flat cap instead left a grey disc with a rim around every summit; a single pin left
2142neighbours 9 m above Snowcrown.
2143
2144**A summit's elevation is above the datum.** v1 added a massif's height on top of the
2145ground under it, so Snowcrown's 4,300 m stood at 4,447 m. Heights everywhere else in the
2146world are above the datum, so the land under a summit is now subtracted before it is
2147pinned, and the checks that it is the top compare total heights.
2148
2149**Tarns keep a shelf.** Eroded honestly, the ranges ran valleys through three of v1's four
2150tarns and dropped them to 500 m. A cirque is exactly ground the rivers did not cut, so an
2151authored bowl lake now keeps its range at the uncut height around it. Sisters Tarn is at
21522,173 m; design.md puts the ski station beside it at 2,100 m.
2153
2154**Regions raise ground after the mountains, not before.** Lifted first, The Uplands'
21551,000 m stacked under the Girdle's foot and put Sisters Tarn at 3,665 m.
2156
2157**One compile at a time.** Dragging a summit updates the world on every pointer move, and
2158each would have started a four-second compile. The service now runs one, keeps only the
2159latest edit made during it, and runs that next.
2160
2161**Cost:** a whole-world compile of about 4 s in a browser worker, most of it the Girdle.
2162Loading to a drawn map is about 4 s; the status dialog shows for it.
2163
2164---
2165
2166## D-066 — Water is geometry, and waterfalls lean
2167
2168**Built.** `world-generation.md` phase 3; §17 there has the detail.
2169
2170**Every lake, sea and river is a surface at its own level**, meshed per tile, with the
2171ground beneath it drawn as bed and the depth test finding the shore. Waves are the wind's:
2172fetch-limited JONSWAP seas, capped at fully developed, which is what a ring sea always is.
2173Rivers run at Manning's speed and whiten on their rapids.
2174
2175**Waterfalls were not added; they were found.** The river profile of D-064 pools reaches
2176and steps between them, and a step is a fall. Merged where the steps come in a run, they
2177give the Long Water a 680 m fall off the range with a gorge below, a 170 m fall into The
2178Deep, and Eastwall Water a 440 m fall. The sheets drift antispinward as they drop, by the
2179free-fall drift canon already computed: §1.9.2 said the waterfall would be the compass,
2180and it now is one.
2181
2182**Three faults the falls surfaced**, each a straight cliff where there should be none: the
2183step applied across a whole valley's width (fixed by shaping walls from a smoothly ramped
2184wall level), across a whole embankment's (the embankment uses the ramp too), and a second
2185cliff kilometres out where a steep gorge wall's straight term fell below the valley's
2186curved one (the gorge shares the curvature). Tile skirts now reach past each tile's
2187steepest step, or a gorge wall's cracks showed as dark panels.
2188
2189**The beam went fuzzy over the seas** — the author saw it. Water blends and is drawn after
2190the ground; the beam was drawn before the water, so distant seas blended over it. The beam
2191is always in front of water, and is now drawn last.
2192
2193**A river met its lake badly** — the author saw that too. v1's Long Water starts at the
2194centre of Hollow Tarn, so its channel was cut across the lake floor and drawn as a stripe
2195across the tarn. Rivers are now trimmed to the water's edge, found by sampling the water
2196rather than the lake's outline, which a tarn's water does not reach.
2197
2198**The screenshot tour was running the author's laptop hot**, and deserved it: fifteen
2199shots, four at a time, each in a fresh browser context compiling the whole world with a
2200full pool of six workers. The compiled world is now cached in IndexedDB — which returning
2201readers get too, as §5.4 planned — keyed so a new build never reads an old world. The tour
2202compiles once, hands every shot the warm cache, caps each page at two workers, and reports
2203any shot that compiled anyway. It takes about 100 s and averages about 3.4 cores.
2204
2205**A regression this caused, fixed 2026-09-27.** Giving water render order 1 put it after
2206the cap windows, which kept the default 0. From outside, a window is transparent and in
2207front of the seas: drawn first, it wrote its depth, and the water behind it failed the
2208depth test — so looking in at the sun cap showed the Sunward Sea's bed, brown, instead of
2209the sea. See-through windows now draw at 2.5, after the water and the falls and before the
2210beam, and the exterior's rays share that order so they still sort against the glass.
2211*D-074 moved them to 4, after the beam and the sun, so both show from outside too.*
2212
2213---
2214
2215## D-067 — Tower's weather is a published plan, and ships read it
2216
2217**Built.** `world-generation.md` phase 4; §18 there has the detail.
2218
2219**The plan keeps both halves.** An event records when Tower published it; a revision
2220records when Tower changed its mind, and why. The world then has two readings of the same
2221plan: what Tower does (the event as revised so far) and what people were told (the event as
2222published). The waves follow the first. The ships follow the second, so a sailor warned of
2223a storm stays in harbour even after Tower quietly calls it off — and a storm nobody was told
2224about catches the fleet at sea. This is the author's answer to D-062 question 2 made
2225mechanical.
2226
2227**Weather and ships are functions of the clock, not simulations.** The interior's clock
2228gained a day number, counted from 0 (D-062). Wind, sea state, whitecaps and every ship's
2229position are pure functions of the world file and the hour, so a link to a moment shows the
2230same sea to everyone, and scrubbing the clock is the same as waiting.
2231
2232**The sea state left the compile.** Phase 3 baked waves into the compiled world; with
2233weather that changes hourly that would recompile the world for every gust. Water vertices
2234now name their body and the shader looks the waves up. Wind and ship counts no longer
2235change the terrain signature.
2236
2237**A float32 precision fault was fixed on the way.** The water shader's around-the-ring
2238coordinate was only good to about 6 m, and whitecaps drew as streaks radiating from the
2239eye. Water now measures from a reference near the eye that steps in whole noise periods,
2240with each wave's phase at the reference added from float64. River ripples were suffering
2241from the same fault unnoticed.
2242
2243**Simplifications, each recorded:** no tacking, no harbours drawn, ships only on ring
2244seas, flat sails. The default plan is 28 days long and repeats: storms and gales on both
2245cap seas and The Deep, a flat calm, and one storm Tower holds down to a gale because a
2246fishing boat ignored the plan.
2247
2248---
2249
2250## D-068 — A scenic tour, and screenshots taken from it
2251
2252**Built**, between phases 4 and 5 at the author's request. `world-generation.md` §19 has
2253the detail.
2254
2255**Particular things want a way to be found.** With a 678 m waterfall, a fleet and scheduled
2256storms in the world, standing still and dragging is a poor way to see them, and a
2257screenshot placed by hand at coordinates goes stale the moment the world is edited. The
2258interior view now flies a tour of nine scenes, thirty seconds each — helicopter arcs,
2259flights along rivers and over The Deep, chase cameras on ships — each worked out from the
2260compiled world rather than written down, so it follows edits. The screenshot tour
2261photographs those scenes, replacing four hand-placed shots, and a test flies all of them.
2262
2263**Ships can be caught out.** A ship chased through bad weather needs a ship at sea in bad
2264weather, and D-067's harbour rule emptied the sea the moment any gale was published, even
2265one published as it began. Ships now go in only for weather announced at least three hours
2266ahead — the time they are given to get in — and ride out anything announced later. The
2267default plan gains one such squall on the Sunward Sea, on day 11. Tower keeping the fleet
2268honest about its warnings is also a better story than a fleet that vanishes on cue.
2269
2270**A documentation fault, corrected.** design.md §3.1 and world-generation.md §10 said
2271readers were the default and `?writer=1` enabled editing. The app has always done the
2272opposite — writer by default, `?reader=1` to preview — and the README said so. The docs now
2273match the code; D-011's reasoning is unchanged.
2274
2275---
2276
2277## D-069 — The desert is quoted, not remembered
2278
2279**Built.** `world-generation.md` phase 5; §20 there has the detail.
2280
2281**Tower laid out the whole grammar of a desert, dry channels included.** The Dry Reach now
2282has tiered plateaus and mesas over sand, dune fields whose crests run across the wind Tower
2283sends, rocky hills, a network of dry washes, salt pans in its low ground, and the shoreline
2284benches of lakes that never stood. None of it is history — §0.1 — and the washes are routed
2285by the same drainage the ranges erode with precisely because a channel network that ignores
2286drainage is the one thing that would look wrong.
2287
2288**Two findings worth keeping.** A flat desert has no drainage: water fans out from every
2289edge and nothing gathers, so Tower crowns the Reach 150 m in its middle and the washes run
2290off it. And widening a one-cell channel by blurring destroys it — dilate first, blur after.
2291
2292**Terracing belongs to the tiles, not to the compile.** The grid stores how far up the
2293plateau the ground stands; the tile turns that into flat tops and steep risers at its own
2294detail. The other way round, every cliff band is one grid cell wide, which is a ramp.
2295
2296**Tower does not build rock across water it has already routed.** Mesas dropped across The
2297Narrows turned the world's whole drainage into a kilometre-deep slot. Rivers carry a 6.5 km
2298corridor clear of plateau rock, which reads as the sand-and-gravel valley a river makes
2299through a mesa field. The consequence recorded: the Long Water's canyon through the lower
2300Dry Reach now runs about 780 m deep where the old test expected under 700, because the rim
2301above it is the desert's rock as often as the ridge's.
2302
2303**Materials gained `salt`, and rock is now claimed rather than inferred.** Bare rock came
2304only from mountain relief before; it now comes from the desert's plateaus and wash floors
2305and from any steep desert face, since sand does not stand at 60°. The ground shader lays
2306strata by elevation — a level in Tower is a constant radius — so the bands line up across
2307separate mesas, which is what makes a mesa field read as one formation.
2308
2309---
2310
2311## D-070 — The Capital is a footprint, and it reaches the water
2312
2313**Built** alongside phase 5, at the author's suggestion.
2314
2315**The Capital was a dot 11 km inland of the lake it was said to stand on.** design.md §1.11
2316described it as sitting on The Deep's antispinward shore; the generated water starts
231710.75 km spinward of its marker, on ground falling from 300 m to the water's 120 m. The fix
2318the author chose is the better one: make the city big enough to be both things at once.
2319
2320**So a settlement is now a point with an extent** — a size and a radius — rather than a
2321marker alone. The author places where a town stands and how far it reaches; Tower lays the
2322streets and buildings out inside that footprint (phase 7). The map draws the extent as a
2323ring around the marker, and each size carries the usual radius a new settlement starts from.
2324
2325**The Capital is 20 km across, centred on the axial midpoint of the world** — the only
2326centre a cylinder has — which is just wide enough that its spinward edge is the waterfront.
2327It holds the centre of the world and a shore on The Deep, and nothing had to move to make
2328that true but 2 km of marker.
2329
2330**A canon figure corrected on the way:** §1.11 said the far shore was "32 km away and
23312.6 km above eye level". Thirty-two kilometres around the cylinder would stand 9.9 km above
2332the eye; 2.6 km is the figure for the 18 km of water The Deep actually fills, which is what
2333the crossing is. The outline is 154 × 32 km; a flooded basin's water is narrower than its
2334outline, as it should be.
2335
2336---
2337
2338## D-071 — Three billion trees, and only the near ones exist
2339
2340**Built.** `world-generation.md` phase 6; §21 there has the detail.
2341
2342**A forest is a rule, not a list.** Greatwood holds about three billion trees at 450 stems
2343per hectare. What exists at any moment is the few thousand near the eye, and they are found
2344by a stratified rule — a fixed grid about one tree apart, each cell hashing its own
2345coordinates — so every tile, every view and every visitor works out the same trees without
2346knowing about any other. Walk a kilometre and come back and every trunk is where it was.
2347
2348**Four ranges, and the handovers are the hard part.** Meshes to about 210 m, cards to
2349560 m, clumps — one card for every 30 m cell of wood — to 2.8 km, and under all of it from
2350620 m the canopy skin: the tile's own ground vertices lifted by the canopy's height, which
2351sinks back onto the ground as the eye comes close. The plan had three ranges ending at
2352760 m; the author saw the result at once — the wood ended a few hundred metres away and its
2353edge walked along with him. Clumps are what made the reach affordable: a card per tree is a
2354million of them at two kilometres, a clump every thirty metres is twenty-six thousand.
2355
2356**The skin was a black ceiling from underneath**, because a solid surface is what it is.
2357Real canopy is about a third gaps, and those gaps are the only reason you can see any
2358distance through a forest. From below it now drops half its fragments on a ground-fixed
2359pattern and lights the rest as light that has come through leaves.
2360
2361**Two numbers the plan had wrong, found by looking:** a meadow's edge cannot be a fixed
2362±0.02 of noise (the same lesson the desert taught about thresholds), and a giant's crown
2363cannot be 0.34 of its height — at that width a grove at 55 stems/ha closes over into
2364unbroken black. Redwoods carry about 0.18, and 30 stems per hectare puts 18 m between
2365trunks.
2366
2367**A city is not a wood.** The Capital sits in the middle of Greatwood's belt, so once the
2368forest was trees it was standing in one. Settlements now clear the forest inside their
2369footprint — which is also the first thing the Capital's new extent (D-070) has been used
2370for.
2371
2372**The Tall Wood was placed once and written down.** Drawn from the world's seed under the
2373author's rules — inside Greatwood, 15–50 km from the Capital, off the water and the rivers
2374— it landed 17 km from the city and is now a feature in `content/world.json` like any
2375other. Greatwood gives up its own ground wherever a smaller forest stands inside it.
2376
2377---
2378
2379## D-072 — The world is changed in the repository, not in the browser
2380
2381**Decided** by the author, 2026-09-26. Built.
2382
2383**The author writes alongside the app, not with it.** The writer mode of D-011 assumed
2384the map would be the place the world was built: draw a lake, name it, export, commit.
2385In practice the author wants to build it by describing changes to Claude, who edits
2386`content/world.json` directly — the Tall Wood (D-071) was placed that way. The editor
2387had become a second, stale path to the same file, and a hazardous one: a browser's local
2388draft was preferred over the committed world, so a draft from last week could quietly
2389shadow everything added since.
2390
2391**So the app only reads.** Removed: writer and reader modes (`?reader=1` now does
2392nothing), the Add tools, the feature editor and every per-kind control, dragging
2393markers and summits, weather scheduling and revision, the localStorage draft and its
2394autosave, backup and static exports, import, and "Reload published world". Everyone
2395sees what readers always saw — the three views, the list, a detail card, the published
2396weather board and the sightline probe — and the editor's read-only readouts moved onto
2397the detail card: a range's summits, a lake's or sea's waves now, a forest's tree count
2398and spacing, and what a desert's landforms came to.
2399
2400**`content/world.json` is the one authoritative copy.** `static/world.json` is gone:
2401the public file is made from `content/world.json` by a Vite plugin — served fresh by the
2402dev server, emitted into the build — with notes emptied and unpublished features left
2403out, exactly as `toPublicWorld` always did. Two committed copies had to be kept in step
2404by hand and a test; one copy and a derived file cannot disagree. Saving the file with
2405the dev server running reloads the page, so a change can be looked at as soon as it is
2406made.
2407
2408**What stays true from D-011:** no backend; the world lives in git beside the prose,
2409diffable and versioned; `notes` and `"public": false` keep things out of the app but
2410not out of the public repository.
2411
2412**A browser that used the old writer mode still holds its draft** under
2413`tower.world.v2` in localStorage. The app no longer reads or writes it, and does not
2414delete it either, so anything in it that never reached the repository can still be
2415recovered by hand.
2416
2417---
2418
2419## D-073 — Every branch is on Pages, and the app can switch between them
2420
2421**Decided** by the author, 2026-09-27. Built.
2422
2423**The author wants to look at a branch in the browser without running a dev server** —
2424`main` and `forest` side by side. GitLab does this natively with parallel deployments,
2425but they are Premium-only and this project is on the Free plan. So the default branch's
2426pipeline builds every branch into one site (`tools/build-pages.sh`): the default branch at
2427the root, each other branch in a folder named for it. A push to any other branch triggers
2428a default-branch pipeline, so its preview follows it within minutes. That goes through the
2429pipeline trigger API with a trigger token kept in the masked CI/CD variable
2430`PAGES_TRIGGER_TOKEN`: a `trigger:` job cannot start a pipeline in its own project, which
2431is how the first attempt failed (`invalid_bridge_trigger`). The token acts with the
2432author's permissions and cannot be limited to protected branches, since previews come from
2433unprotected ones; acceptable while the author is the only one who pushes. A branch that fails
2434to build is left out with a warning rather than failing the deploy, so a broken experiment
2435can never take the published map down.
2436
2437**The app has a branch picker.** Every folder of the site carries a `branches.json`
2438listing what was built, with links relative to that folder; the panel header shows a
2439picker when there is more than one branch, and switching keeps the hash, so the other
2440branch opens on the same view and place. Without the list — on a dev server — there is
2441no picker. A branch built from code older than the picker has none either, but it is
2442still listed in the others.
2443
2444**One origin, shared storage.** The branches are paths on one site, so localStorage and
2445the IndexedDB world cache are shared. That is harmless — the cache is keyed by the world
2446and the build — but it means a remembered view or camera follows you between branches.
2447
2448---
2449
2450## D-074 — Tower has a sun, and keeps Sydney's days
2451
2452*Amended by D-079: the rod no longer stops at the sun; it runs on to the far cap.*
2453*Amended by D-089: Tower keeps Pisa's days now, not Sydney's.*
2454
2455**Decided** by the author, 2026-09-27. Built. Supersedes D-053; amends D-008.
2456
2457**A sun at one point on the beam, crossing the world once a day.** The swarm gathers into
2458one shoal on the axis and throws the beam's light out sideways there: a single blazing
2459point, Tower's sun. It lights at the sun cap at sunrise, moves at a steady speed down the
2460whole axis, and goes out at the far cap at sunset. Night follows, with the beam banked to
2461a thousandth as D-046 has it, and the next morning the sun lights at the sun cap again.
2462This replaces D-053's dawn front, and it turns D-008's aim — no mirrorlet may read as a
2463sun — into its exception: Tower wants exactly one.
2464
2465**Sydney's days.** Tower keeps an Earth city's clock and its sunrises and sunsets, and the
2466author chose Sydney. The clock is Sydney's local time, daylight saving included; day 0 is
246723 September 2026, the southern spring equinox, so the calendar starts on a 12-hour day and
2468lengthens toward December. Sunrise and sunset come from NOAA's solar position equations,
2469within a few minutes of the published tables (test/sky.test.ts). The city and the date of
2470day 0 are a `calendar` in `content/world.json`, so both can be changed.
2471
2472**How it lights the world.** Tower aims the shoal's mirrorlets so the sun is as bright seen
2473from anywhere as from beneath it, so the ground is lit by how squarely it faces the sun, as
2474under Earth's — not by the inverse square of a lamp 50 km up, which would leave most of
2475the world in twilight all day. Each place has its own noon when the sun passes overhead;
2476the sun never sets, since from one cap the sun at the other is still 5° up. The shoal takes
2477the whole beam where it gathers, so the rod glows from the sun cap to the sun and is dark
2478past it, and the far cap's mirror is lit only by the banked beam at night.
2479
2480**Consistency, inside and out, was the author's main requirement.** So there is one of
2481everything: one world clock (`src/world/clock.ts`), one sky computed from it
2482(`src/world/sky.ts`), and one `setSky` that lights the meshes both 3D views share. The sun is
2483one of those meshes, lit by the same haze integral as everything else, and every lit surface
2484goes through one shader function, `direct`. The exterior used to render the inside at a
2485fixed noon; it now follows the clock like the interior does, the windows draw after the
2486beam and the sun so both show through the glass, and the rays from the collector dim when
2487the beam is banked. The map marks the sun and shades the world by the same light curve.
2488
2489**What changed in the app.** The Time controls — day, time, run the clock, its rate, and how
2490bright Tower runs its day — moved out of the interior's options into a Time section that
2491works in every view. The Light menu lost "Night", which the clock now brings; "Dark" still
2492shuts the beam down. Scenic tour scenes with no reason to choose an hour are shown in the
2493morning where they are, the sun 60 km sunward and about 40° up.
2494
2495**The caps have more than one name.** The sun cap; the far cap, also the star cap. The author
2496may use any of them, and more are added as they come into use.
2497
2498**The clock opens at the time in Sydney now** (author, 2026-09-27). It starts from Sydney's
2499current date and time, counted from day 0, and runs forward at the speed it was last run at;
2500the speed and the Light are remembered, the time is not. `?clock=<hours since day 0>` holds
2501it at a moment, stopped, which is what the screenshot tour uses to stay reproducible.
2502
2503## D-075 — The stars are Earth's
2504
2505**Asked for** (author, 2026-09-27): the sky was "sparse and random"; use the accurate night
2506sky from Earth instead.
2507
2508**What it is.** The Yale Bright Star Catalogue, 5th revised edition (Hoffleit & Warren 1991,
2509CDS V/50): 9,096 stars to about magnitude 6.5, which is everything the naked eye sees from
2510the darkest sites. `tools/bright-stars.mjs` writes it to `src/data/bright-stars.json` —
2511position (J2000), V magnitude, B−V colour — and the app loads it as a separate chunk the
2512first time a 3D view opens. The procedural field of D-056 is gone; both views still share
2513one `STAR_GLSL`, so the sky out of the far cap and the sky round the exterior are one sky.
2514
2515**Where it points.** Tower's axis points at the sun (§1.6), so the sky it sees is set by the
2516sun's ecliptic longitude, from the clock's real instant (`Sky.at`). The far cap looks at the
2517antisolar point; it walks the ecliptic a degree a day, so the pinwheel's centre is in
2518Pisces in late September and near Orion's feet at the December solstice. Roll about the
2519axis is the spin inside; outside nothing fixes it, so **ecliptic north is up** (scene +Y), the
2520one choice square to the axis every day of the year. Precession since J2000 is applied.
2521`src/world/celestial.ts` holds the maths and `test/celestial.test.ts` pins it: the sun where
2522the almanac has it at the 2026 equinox and solstice, the frame a rotation and not a mirror,
2523and Orion beyond the far cap in December.
2524
2525**How it draws.** A pixel cannot look at 9,000 stars. They are baked into cells on the six
2526faces of a cube (256 a side, each face bordered by its neighbours' stars), and a pixel reads
2527the nine cells round its own. Each star is still the pixel-wide gaussian of D-056, which
2528stays still rather than flickering; brightness goes as flux^0.8, and the brightest grow a
2529little wider. Colour follows B−V, kept pale, since starlight is too faint for much colour
2530vision.
2531
2532**The motionless star is not canon** (author, 2026-09-27). §1.4 promises "one motionless
2533star at the middle" of the pinwheel. The real sky has no star at the antisolar point, and it
2534moves a degree a day, so no real star could hold the place. The placed star at the centre
2535stays for now and is on the backlog (design.md §5) to revisit; removing it is one line.
2536
2537## D-076 — Trying other outsides: rock
2538
2539**Asked for** (author, 2026-09-27): the outside is very uniform; try different approaches,
2540starting with "a rocky surface, almost like someone tried to make it look like an asteroid".
2541
2542**An experiment, not canon.** It is a third reading in the Hull switch — True, **Rock**,
2543X-ray — so it can be compared with the bare skin and kept, changed or dropped. Nothing in
2544§1 changes. A shell kilometres thick round a 540 km cylinder would be over 10¹⁸ kg of rock
2545on a spinning hull, which canon would have to answer for if it stays.
2546
2547**The shape.** A shell laid over the metal skin, never cut into it: at least 0.2 km proud of
2548the wall's outer face everywhere, so nothing inside can show through, and the windows are
2549left clear, with rock heaped round their rims. Lumps a few kilometres high and 1,400 craters
2550from 0.8 to 16 km across, a power-law population with raised rims, in the geometry
2551(`src/views/rock-hull.ts`, ~0.7 km between vertices); grit, pits and colour mottling below
2552that in the shader, bump-mapped from noise in the hull's own frame and faded out as a pixel
2553grows wider than the detail. Seeded, so it is the same rock on every load. Clicks still land
2554on the smooth skin, hidden, so a picked point is the canon point beneath the rock. The rock
2555is lighter than a real carbonaceous asteroid's charcoal, under the same licence as the fill
2556lights.
2557
2558## D-077 — Trying other outsides: piping
2559
2560**Asked for** (author, 2026-09-27): "something industrial like the Alien movies, with
2561panels and pipes and exhaust vents, only on a huge scale." The second experiment after
2562D-076's rock, and like it a Hull reading labelled as an experiment, not canon.
2563
2564**What is on the skin** (`src/views/pipe-hull.ts`):
2565- **Plating**, in the shader of the hull cylinder itself: 40 panels round by 8 km along,
2566 some split in two or four, each its own shade, some ribbed, with grime streaked along the
2567 axis. The seams are bump-mapped grooves that widen with the pixel, so they fade with
2568 distance instead of aliasing. 40 round, so the plating closes on itself exactly.
2569- **Bands** every 30–42 km: a collar 2.6 km wide and 1.1 km proud, with a hoop pipe either
2570 side. They divide the 540 km into stations, as a ship's frames do.
2571- **Pipe runs**: fourteen lanes of two to five pipes, 0.6–2.2 km across, running from band
2572 to band on brackets, and skipping some stations so runs start and stop.
2573- **Plant**: 170 clusters of blocks, the odd one a tower up to 6 km, kept off the pipe
2574 lanes and the bands.
2575- **Vents**: 64 housings, each with a grille and a plume that fire **at random**, on
2576 separate clocks (author: "the plumes are constant, let's make them fire at random", and
2577 the same for the lights). A grille is lit 1–7 s at a time and an ember between; a plume
2578 shoots out for 1.5–4 s, then is gone for 2–12. There is no air to hold a plume out here;
2579 it is the look asked for, under the same licence as the beam's rays.
2580- **Thrusters** ("larger fires, light them less often"): six pods at each end, in 12 km
2581 kept clear for them, each with four bells — out, spinward, antispinward, and along the
2582 axis away from the world. They are what §1.6's never-ending reorientation would need. A pod
2583 burns one bell at a time for 2–5 s, then waits 25–80 s. The flame is 28–42 km long and fans
2584 out as it goes, since exhaust in vacuum has nothing to hold it together; the spread grows as
2585 the square of the distance, so a sideways flame stays clear of the plating it runs along.
2586 Two point lights follow the two brightest burns and light the hull round them — one per
2587 bell would recompile every material for 48 lights, and more than two at once is rare.
2588
2589**Dust** (author: "it's too shiny, let's add a nice layer of dust"). Every piping material —
2590plating, bands, pipes, blocks, pods, bells — goes through `dusty()`, which lays a dust
2591colour over the surface and makes it fully matte and non-metallic where it lies. Bare
2592metal under the fill lights read as chrome. Dust is thickest on faces turned outward, the
2593way everything on a spun hull is thrown, thinner on the sides and nearly absent
2594underneath, and broken into drifts by noise. It is worked out in the hull's frame from the
2595vertex, so instanced parts get it too.
2596
2597**Timing is on the CPU.** A small scheduler, `Bursts`, holds each emitter's on/off clock and
2598a level that rises over an attack and falls over a release, so nothing snaps on or off. The
2599shaders only read that level per instance. The lights need to know which bells are burning,
2600and keeping one schedule in JavaScript is simpler than mirroring a hash in GLSL. It is
2601tested in test/pipe-hull.test.ts, including a long pause such as a hidden tab.
2602
2603**What it must not do.** Nothing is inside the skin (test/pipe-hull.test.ts checks every
2604vertex), and the bands are open drums with ring faces, never closed cylinders, whose
2605discs would stand inside Tower and be seen through the windows. The plating is two-sided
2606so that, from inside the tube, it hides the far side's parts wherever the ground has not
2607been generated yet. All the parts are instanced: five draw calls for a few thousand pieces.
2608Seeded, so it is the same plant on every load, and built only when first chosen.
2609
2610## D-078 — The cap windows are framed in identical triangles
2611
2612**Asked for** (author, 2026-09-27): "the endcaps have circular window patterns. please
2613change these to be identical triangle pieces."
2614
2615Both caps' mullions are now a lattice of **identical equilateral triangles**: three
2616families of straight members at 60° to each other, spaced one triangle height apart, with a
2617vertex on the axis where six triangles meet. The rings and spokes of the polar tessellation
2618(D-041, D-045), including the heavy ring at 25 km, are gone. The side is 8.3 km — the old
2619cells' pitch, six across the radius — so the frame keeps its job as the one object in the
2620world with a known size (D-041), and it is coarse enough to survive the near edge-on views
2621from outside (D-030).
2622
2623A disc cannot be tiled by whole triangles, so the rim cuts the outermost ones; the heavier
2624rim mullion closes them. The far cap's mirror sits on the centre vertex, and the sun cap's
2625aperture on the same point of its own lattice. `test/cap-pattern.test.ts` pins the lattice:
2626three directions 60° apart, every line a whole triangle height from the axis. The polar
2627checker drawing, which nothing had used since both caps became windows, was removed with it.
2628
2629## D-079 — The beam runs from cap to cap, and the sun moves along it
2630
2631**Asked for** (author, 2026-09-28): "can we keep a permanent light beam in the sky (from cap
2632to cap) and the large sun still moving from cap to cap? … i'd like the beam extend all the
2633way, not just from sun cap to the sun."
2634
2635Under D-074 the sun's shoal took the whole beam where it gathered, so by day the rod glowed
2636from the sun cap to the sun and was dark past it, and the far cap's mirror was lit only at
2637night. Now the shoal takes **part** of the beam and the rest runs on: the rod is one line of
2638light from cap to cap at every hour, the sun still crosses the world along it, and the rod
2639always ends on the lit mirror at the centre of the far cap (D-048).
2640
2641**How much gets past.** `BEAM_PAST_SUN = 0.7` in `SUN_GLSL`: past the sun the rod is 70% of
2642itself, so the sun still reads as where the beam is being taken, and the mirror by day is
2643lit by that same share. One constant for both, so the rod and the mirror it ends on agree.
2644Night is unchanged: the banked beam runs the whole length evenly.
2645
2646**What did not change, and should be settled.** The light on the ground is still worked out
2647as if the sun took the whole beam (`direct`), so the world is exactly as bright as before.
2648Strictly, a sun taking 30% of the beam would light it less. That is on the backlog
2649(design.md §5): pick the split, then the light budget follows from it. The map, which does
2650not draw the rod, is untouched.
2651
2652## D-080 — A dead terrain worker no longer freezes the world
2653
2654**Reported** (author, 2026-09-28): now and then, over several days, the page froze on
2655"Teaching Eastwall Water which way is down", with no clear console errors.
2656
2657**What it was.** That label is the compile's **last** step, not a slow one: the whole compile
2658takes about 5 s, and that step 3 ms. After it, the page sent the compiled world to every
2659worker in the pool and waited for all of them, with no time limit. A worker that never
2660started never answered, so the wait never ended — and a comlink call to a dead worker fails
2661silently. The only trace was Firefox's warning "Attempting to create a Worker from an empty
2662source", which the author's screenshots showed a dozen times: Firefox had been handed an
2663empty script for a worker and started it with nothing to run. Why it sometimes gets an empty
2664script was not pinned down (twenty clean loads and eighteen with files changing mid-load all
2665worked); a page that made one worker silent on purpose reproduced the freeze exactly, on the
2666same label, with the same warning and nothing else.
2667
2668**What changed** (`src/terrain/service.ts`). Before a world is handed out, every worker must
2669answer a `ping` within 15 s. One that does not is terminated and replaced once; if the
2670replacement is silent too it is dropped, and the pool carries on smaller. Handing over the
2671world has a 30 s deadline per worker on the same terms. Each of these says so with a
2672`console.error` naming the worker, and a worker's own `error` event is logged too, so the
2673next time this happens it is visible. Only an empty pool is fatal, and it says so.
2674
2675Checked in Firefox with the same sabotage: one silent worker now costs about 15 s and a
2676console error; a silent worker with a silent replacement costs about 30 s and loads with five;
2677a normal load is unchanged.
2678
2679## D-081 — Trying other outsides: rock and piping together
2680
2681**Asked for** (author, 2026-09-28): "i like the outside rock and pipes views, do you think
2682we can combine them? have rock show through, and have pipes overlaying in other spots."
2683Offered three readings: rock heaped on the plant as shielding, the plant built on top of
2684the rock, or Tower half dug out of an asteroid. **Chosen by the author: shielding** — "it
2685seems like a reasonable approach to creating Tower". A fifth Hull reading, **Rock and
2686piping**, an experiment like D-076 and D-077, not canon.
2687
2688**Why the two together, and not just either.** The rock alone has nothing in it to give
2689scale — a crater could be sixteen metres or sixteen kilometres. The plating alone is
2690regular, which was D-076's complaint in the first place. Each fixes the other: a pipe
2691run that goes into a bank of rock and comes out ten kilometres on says how big both are.
2692
2693**One cover field decides where the rock is** (`rockCover` in `src/views/rock-hull.ts`):
2694seeded noise over the hull, 0..1, rock wherever it is at least 0.5. Two scales, 80 km and
269522 km, so it lies in banks and drifts tens of kilometres across with bare ground between.
2696It first covered about 43% of the hull; the author asked for "a slightly higher
2697concentration", and it is now about 60%.
2698- **The rock** is D-076's shell scaled by the cover, rising from 0.2 km to full height
2699 over a narrow band of it so each bank ends in a scarp. It is **1.6 times D-076's height**
2700 here (author: "the rock feels like it can be taller, at least another 50%") — banks
2701 with bare plating beside them need the height to read as heaps. The Rock reading on its
2702 own is unchanged. A first try that ramped up slowly
2703 left the edges a thin feathered skin that read as smoke, not rock. The cut is made per
2704 pixel from the cover carried on each vertex, broken up by noise finer than the mesh, so
2705 the shore of each bank is ragged rather than the mesh's 0.7 km grid.
2706- **The plant** — blocks and vents — goes only on bare ground, a little inside the edge.
2707 Each plant room is centred on bare ground, so they crowd into the gaps rather than
2708 thinning out. **The bands and pipe runs are laid as before** and go under the rock: that
2709 is what makes the rock read as heaped on rather than the plant as built round it.
2710- **The thruster pods are dug clear**, 14 km round each, with a sloped edge: a sideways
2711 flame runs along the hull, and would otherwise burn into the rock.
2712
2713**Built separately from the other two.** The plant crowds differently and the rock is
2714dug out, so neither half is the same as the dress on its own; the combined one is its own
2715pair of meshes, built the first time it is chosen. With nothing under rock, the piping
2716builder makes the same draws from its seed as before, so the Piping reading is unchanged
2717(tested).
2718
2719**Canon.** Nothing in §1 changes. The shielding reading gives D-076's rock a reason, and
2720rock over three fifths of the hull at 1.6 times the height is about the same mass as
2721D-076's shell — still over 10¹⁸ kg, so still a question for
2722§1.2 if this stays (design.md §5 Backlog). Real habitat studies do bury the hull under
2723regolith for shielding, but metres of it, not kilometres.
2724
2725**The plating's moiré** (author, 2026-09-28: "the repetitive pattern in the pipes creates
2726moiree lines — circle patterns"). Seen from a glancing angle near the far cap on a
2727high-density screen: rings of interference on the plating. The ribbed panels were the
2728cause. Their ribs are rings round the hull 0.9 km apart, and they faded out only once a
2729pixel was most of a rib wide — so they were still drawn at full strength while one to
2730three pixels apart, which is exactly the band where a regular pattern beats against the
2731pixel grid. They were also faded by the pixel's overall size, when what matters for rings
2732is how fast z runs across a pixel, which at a glancing angle is several times more. The
2733ribs now fade by that rate, and are gone before they are three pixels apart. The same
2734reasoning is applied to the two other regular patterns in D-077's plating: the panel seams
2735fade out once a panel is a few pixels across, and the hard-edged grime stripes are
2736averaged away as they near a pixel's width. Close up, nothing changes. This is D-077's
2737plating, so the Piping reading gets the fix too.
2738
2739## D-082 — Eye height is a gauge in the view, marked with things of known height
2740
2741**Asked for** (author, 2026-09-28): "please make the bottom-left panel collapsible", and
2742"move the eye height selector out of the right panel, and put it instead as a vertical
2743height, and indicate comparable heights when we reach those heights: average adult,
2744average home, eiffel tower, airplane cruising altitude, etc."
2745
2746**The gauge** (`src/ui/eye-gauge.ts`) runs up the right-hand side of the interior view,
2747from 1.7 m at the bottom to 45 km at the top (48.4 km, the beam, since D-084), on the slider's own logarithmic scale — so a
2748person and a weather balloon both get room. Eleven marks, all real-world, so none needs
2749explaining: an adult (1.7 m), a two-storey house (8 m), a ten-storey block (30 m), the
2750Statue of Liberty (93 m), the Eiffel Tower (330 m), the Burj Khalifa (828 m), Mount Fuji
2751(3.8 km), Everest (8.8 km), an airliner cruising (11 km), Concorde cruising (18 km) and a
2752weather balloon (35 km). The gaps between the author's examples were filled so no stretch
2753of the scale is empty. Marks brighten as you pass them and turn accent-coloured when you
2754are level with one — within a tenth of its height. The readout by the thumb gives the
2755height, the local gravity (which the old slider's label carried), and the comparison:
2756"≈ the Eiffel Tower" when level, otherwise a multiple of the highest mark passed,
2757"2.0× the Eiffel Tower".
2758
2759**A widget of its own**, not a rotated `<input type=range>`: the marks must sit exactly
2760level with the thumb, and a native range insets its thumb by an amount each browser picks.
2761It is still a slider to the keyboard (arrows, Page Up/Down, Home/End) and to screen
2762readers (`role=slider`, with the comparison as its value text). On a short view, a mark
2763too close to the one below it keeps its tick and drops its name — Everest and the airliner
2764are the pair that collide first. On a phone the names go altogether, and the readout by the
2765thumb says which mark you are near. It follows the view as the slider did: flying with E
2766and Q moves it, except while it is being dragged.
2767
2768**The standing readout folds** to its title, "Where you stand", with a click on it; the
2769rest of it stays click-through, as every overlay is. The fold is remembered. On a phone it
2770starts folded, since open it covers a third of the view, and it now stops short of the
2771gauge.
2772
2773## D-083 — No zoom inside; the trackpad swipe moves
2774
2775**Asked for** (author, 2026-09-28): "the current interior docs state that two fingers
2776move, but i'm seeing that two fingers changes the zoom. can the zoom be hard-coded
2777internally, and can we get the motion working instead?"
2778
2779**Why it zoomed.** D-059 made two *touches* move the observer and pinch the field of view,
2780and the wheel zoom. On a Mac trackpad, a two-finger swipe is not two touches: the browser
2781delivers it as the wheel. So the gesture the panel described went to the wheel handler and
2782zoomed, and the move D-059 built was reachable only on a touchscreen or with shift-drag.
2783
2784**The field of view is fixed at 60°** (`FIELD_DEG` in `src/views/interior.ts`). Pinch no
2785longer zooms, the wheel no longer zooms, a saved field is no longer restored, and the
2786readout's `field` line is gone, since it could only ever say 60°. The scenic tour still
2787frames its own shots at its own fields; stopping it puts 60° back. A trackpad pinch
2788arrives as the wheel with ctrl held, and is swallowed rather than zooming the page.
2789
2790**The wheel moves.** Not as a grab of the ground under the cursor, which was the first
2791try: it matched the touch drag exactly, but a trackpad's cursor sits wherever it was left.
2792Measured from standing, a 300 px swipe moved 2 m with the cursor near the bottom of the
2793screen and 99 m just under the horizon; and above the horizon every ray is held to the
2794same capped reach, so a swipe there barely moves at all. The travel depended on where the
2795cursor happened to be, which is no way to walk. So a pixel of swipe is a fixed fraction of a second of keyboard flight, which
2796already scales with height: 1/25 s, so a screen's swipe walks about 70 m from standing and
2797some 40 km from a kilometre up, wherever the cursor is and however the head is tilted. It
2798walks along the heading, in the same direction as the touch drag — the ground follows the
2799fingers, so swiping down goes ahead, as the page does under natural scrolling. No shift
2800boost, because browsers turn shift-wheel sideways.
2801
2802**Captions pass the wheel on.** They take the pointer so they can be clicked, and one sits
2803on the skyline more often than not; a swipe over one did nothing until the caption layer
2804was given the same wheel handler.
2805
2806**Touch is unchanged apart from the pinch**: two fingers still grab the ground and drag
2807it. Their centroid is what moves, so a pinch — fingers apart about a still centre — now
2808does nothing at all.
2809
2810## D-084 — The top of the eye-height gauge is the beam
2811
2812**Asked for** (author, 2026-09-28): "add 'sun beam' or whatever the technical name is to
2813the chart, since that's the top point we can go to." Canon's name is **the beam** (§1.3):
2814the column of concentrated sunlight on the axis, 3.18 km across, glowing where it runs
2815through the air.
2816
2817**It was not the top, so it is now.** The ceiling was 45 km above the ground, 3.4 km short
2818of the beam's edge, which is 48.4 km above the datum (the 50 km radius less the beam's
28191.59 km). The ceiling is now that edge: `EYE_MAX_M = RADIUS_M - BEAM_RADIUS_M` in
2820`src/ui/scales.ts`, derived from canon rather than typed in, and the gauge's last mark,
2821"the beam", sits at the top of the scale. The interior stops the eye 300 m short of the
2822edge over whatever ground is underfoot, so on high ground the ceiling comes lower and the
2823observer can never be inside the beam. From there it is 80° wide overhead and looking up
2824is white — a brilliantly sunlit cloud, 300 m off, as §1.3 describes it.
2825
2826**The gauge's gravity now counts the ground**, as the standing readout's always did;
2827the old slider's label measured from the ground up and read 0.044 g at the top where the
2828readout said 0.038 g.
2829
2830
2831## D-085 — The scenic tour flies the world's flyers, inside and out
2832
2833**Asked for** (author, 2026-09-28): automatic flight, to show Tower off inside and out —
2834a flight plan put together at random from components, a camera that points at interesting
2835things as it goes, about fifteen seconds before changing from one flyer to another, and
2836every point of view belonging to something in the world: paragliders, planes, ships,
2837spacecraft, trains. Planned before it was built, and the plan was agreed with these answers:
2838
2839- **A craft exists for its leg.** Nothing is simulated as traffic when nobody is watching;
2840 a paraglider is there for the fifteen seconds the camera rides it. The ships, which were
2841 already in the world (D-067), are followed as they are.
2842- **Legs last 12 to 18 seconds**, each its own length, and **fade through black** between.
2843- **Airships, yes**: a few ordinary ones low down, and a few slow giants near the axis.
2844- **It replaces D-068's tour**, whose nine thirty-second scenes are now legs among the rest.
2845
2846**Built.** `world-generation.md` §19 has the detail.
2847
2848**A leg is a craft on a path, and a camera on the craft.** Twenty-six inside — a helicopter
2849round the Long Water's fall and a paraglider across its face, ships in fair weather and in
2850a squall, a paraglider on a summit's flank, a glider in a thermal over the Dry Reach, light
2851aircraft down rivers and through The Narrows, a seaplane across The Deep, an airliner round
2852the Girdle, a giant airship climbing out of the Capital and another circling the sun, a
2853night flight toward the far cap, trains on both railways (D-086) — and nine outside, all
2854spacecraft: skimming the hull, circling it against its spin, docking on it, looking in
2855through the far cap, crossing the sun cap, over the collector's rim, down the beam, pulling
2856away, and Tower, the collector and the sun in one frame. The camera rides in the craft, or
2857behind it on the far side from what it is looking at, so the craft and its subject share
2858the frame.
2859
2860**The camera is pointed by a director**, not by hand. Everything worth a look — summits,
2861falls, lakes, the Capital, the Tall Wood, the sun, both caps, ships — is scored from the
2862craft by how big it looks, how clearly it can be seen through the air (the same integral of
2863the density profile the renderer hazes with), whether the ground hides it, and whether a
2864pilot could look that way. The leg's own subject wins most arguments; anything looked at for
2865long starts to bore it; turns are eased over three seconds and never swing faster than
28660.7 rad/s. So the tour does not stare at haze, and the far side of the world gets looked at,
2867because §1.8 says it is the clearest thing in the sky.
2868
2869**The deal is a deck.** Every leg is flown once before any is flown twice, no two in a row
2870are the same kind of craft, two legs about the same subject are kept apart where they can
2871be, and a new deck never opens on the craft the last one closed on.
2872
2873**Some legs are time-lapses, and say so.** A giant airship takes minutes to climb ten
2874kilometres and circles the sun at 28 m/s, so those legs run the world's clock ×20 to ×150,
2875and the caption says "time ×150". Everything else is real time.
2876
2877**What the numbers say, the captions say.** Moving round the world changes what you weigh
2878(`canon/motion.ts` `apparentGravity`): the Girdle's airliner, flying antispinward at 230 m/s
2879nine kilometres up, weighs about a third of what it would at rest, and a train on the Uplands
2880Ring is 31% heavier one way round and 27% lighter the other. Every circumferential journey in
2881Tower has a handedness, as every slope does for a skier (§1.11).
2882
2883**Outside, the hull turns at the leg's rate** — real time — whatever the Speed slider says,
2884so a craft skimming it sees 700 m/s of hull go by and not a blur. A leg flies in one of two
2885frames: space, where the hull wheels past, or the hull's own, where it holds still and the
2886stars wheel, which is what docking looks like. The near plane follows the nearest thing, so
2887a craft 400 m off is not clipped.
2888
2889## D-086 — Two railways: the Axial Line and the Uplands Ring
2890
2891**Asked for** (author, 2026-09-28): trains in the tour, with the lines "the ones you find
2892reasonable". There were no railways in the world, so there is now a `railway` kind in
2893world.json — a line, with a line speed — and two of them, chosen and laid by Claude:
2894
2895- **The Axial Line**, 378 km, 70 m/s: from Sisters Station across the Uplands, through **The
2896 Narrows** beside The Long Water — the only way along the world goes through the only gap
2897 in the ridges, for rails as for rivers — down The Deep's shore past the Capital, and through
2898 Greatwood to the shore of the Deepward Sea.
2899- **The Uplands Ring**, 314 km, 100 m/s: the one line round the world rather than along it,
2900 at z 140 km, where the Uplands are level at about 1,000 m the whole way round and no water
2901 stands. It crosses the four rivers off the range and meets the Axial Line near θ −0.06.
2902
2903**The track keeps its own grade, over the ground.** `terrain/rails.ts` lays the deck at the
2904highest ground within a kilometre either side along the line, eased over 900 m, so it is never
2905below the ground and a train never runs through a hill. Tower does not cut the ground for it.
2906The cost of that is honesty about viaducts: the deck stands 10–20 m up on piers over most of
2907both lines, and up to 260 m where the Ring crosses a gorge. The steepest grade is 7%, where
2908the Axial Line comes down off the plateau into Greatwood; all but 23 km of it is under 3%,
2909and the Ring never passes 3.7%. A kilometre window was measured against 400 m and 1.5 km:
2910shorter made grades of 17–28%, longer made the whole line a viaduct.
2911
2912The map draws them as lines in their own colour, and the interior draws the deck, its
2913girders and a pier every 100 m wherever it stands clear of the ground.
2914
2915## D-087 — Where spacecraft dock, and where aircraft may not go
2916
2917**Canon, from the author** (2026-09-28), answering questions the tour raised:
2918
2919- **Spacecraft can dock anywhere on the outer shell.** A docked ship turns with the hull at
2920 700 m/s, and the hull holds it on that circle: it hangs from its clamps, pressed outward at
2921 1.02 g (ω² × 51 km), its feet toward the hull and its floor toward space. The tour's docking
2922 leg matches the spin and sets a ship down that way.
2923- **Aircraft are not allowed into the beam, but they can fly around it** — and round the sun,
2924 which is in the beam (§1.3.1). Every craft in the tour keeps at least 3 km clear of it,
2925 inside and out, and the tests check that.
2926
2927**Consequences worked out with it, not canon until the author says so:** the giant airships
2928live near the axis because only there do they have to. At 44 km up the air is about a
2929twentieth of the floor's density and weighs about a tenth, and what a gasbag lifts per cubic
2930metre depends on the density of the air it displaces, not on gravity — so an airship there
2931needs some twenty times the volume for the same load. Hence giants, 420 m long, and slow.
2932
2933## D-088 — Tower has a moon
2934
2935**Asked for** (author, 2026-09-28): "a 'moon' will travel from night cap to the sun cap over
2936the course of the night. The brightness of the moon will be based on the moon phases, so this
2937will give Tower a moon of sorts." The **night cap** is the far cap: night starts there, when
2938the sun goes out at it (the cap names are in §1.4).
2939
2940**Built.** design.md §1.3.5 has the canon.
2941
2942**What it is.** A second gathering of the swarm on the banked beam, by night, the size of the
2943sun. It lights at the far cap at dusk — `SUN_RAMP_H`, the same quarter hour the sun takes to go
2944out — runs at a steady pace to the sun cap, and goes out there at sunrise as the sun lights. The
2945sun crosses the world one way by day and the moon the other way by night, so there is always one
2946of them on the beam except at new moon. With the beam shut down (the Dark setting) there is no
2947moon either.
2948
2949**Its phase is Earth's Moon's**, worked out for the moment on the clock (`world/celestial.ts`
2950`moonPhase`, Meeus's low-precision phase angle, checked against the almanac's new, full and
2951quarter moons). Day 0's night, 23 September 2026, has an 89% waxing moon; it is full on the 26th
2952in Pisa (the 27th in Sydney, which Tower kept when this was written) and new on 11 October. The disc is drawn with its terminator, lit toward the far cap while it
2953waxes and toward the sun cap while it wanes — where the sun set and where it will rise, as on
2954Earth.
2955
2956**How bright, and why that.** The brightness had to be chosen, because the honest one would be
2957invisible: Earth's full moon lights the ground at about 0.25 lux, and Tower's banked night is
2958already some 150 times that. So Tower's moon is sized to its own night: at full it throws as much
2959light on the floor as the whole banked rod (`MOON_AT_FULL_OF_NIGHT = 1`), and a moonlit night is
2960up to 0.64 W/m² against a moonless 0.32. It is lit from where the moon is, not from the whole rod,
2961so it has a direction: slopes facing it are brighter, and seas carry a path of light toward it.
2962
2963**Everywhere the sun is, the moon is.** The panel's time section says where it is and how lit,
2964the interior's readout gives its height and distance as it does the sun's, the map draws a pale
2965line where it is overhead, the exterior sees it in through the windows, and the scenic tour's
2966director will look at it by night. The tour has one new leg for it: a seaplane crossing The Deep by
2967moonlight, on the first night of the month with the moon at least 60% lit.
2968
2969**A canon question it raises, not answered here:** whether the moon takes its light from the
2970beam as the sun does — the rod dimmer past it — or is fed by Tower lifting the banked beam, as
2971written above. The second is what is drawn.
2972
2973## D-089 — Tower keeps Pisa's days
2974
2975*Amends D-074.*
2976
2977**Decided** by the author, 2026-09-28: "for reasons, Tower has actually decided to set its
2978internal 'time' to match that of Pisa, Italy." Built.
2979
2980**What changed.** The `calendar` in `content/world.json` is Pisa's: `Europe/Rome`, 43.7228° N,
298110.4017° E. Tower's clock is Pisa's local time, Italian daylight saving included (CEST, +2, until
298225 October 2026; CET, +1, after), the app opens at the time in Pisa now, and the sun rises and
2983sets with Pisa's sunrises and sunsets. The fallback calendar in `src/world/schema.ts`, used when
2984the world file's is missing or unusable, is Pisa's too.
2985
2986**The clock and the days stay together.** D-074 makes Tower keep one city's clock *and* its
2987sunrises and sunsets; changing the city changes both. So Tower's seasons turned over: day 0 —
2988still 23 September 2026 — is now the northern autumn equinox, and the days shorten toward
2989December instead of lengthening. The day runs 12 h 11 min at the equinox (the sun at 44 km/h),
29908 h 55 min at midwinter (61 km/h) and 15 h 27 min at midsummer (35 km/h), against Sydney's
29919 h 54 min and 14 h 24 min.
2992
2993**Tests.** Sydney's published sunrise tables still check the solar maths, on a Sydney calendar
2994of their own; Pisa's day lengths, daylight saving and clock have tests of their own.
2995
2996**One knock-on in the tour.** The ship leg starts in the morning over its sea, and the morning
2997moved; at the new hour the director looked up at the sun, which put the chase camera below the
2998ship, and the sea pushed it back up beside the ship, looking past it. A ship's chase camera now
2999looks no more than 20° up (`maxPitch` 0.35 in `shipLeg`).
3000
3001## D-090 — The far end is cold: ice at the far cap, a frosted window, and Tower sets the hull's heat loss
3002
3003*Amends §1.9.4, which said nothing breaks Tower's thermal uniformity.*
3004
3005**Worked out with the author**, 2026-09-28, starting from a question: if Tower sent no light
3006to the sea by the far cap, would it freeze? Canon only; nothing drawn yet.
3007
3008**What the question found.** Two things canon had missed.
3009
3010- **The far cap is a radiator.** D-045 glazed the whole cap, and glass is opaque to thermal
3011 infrared, so the window absorbs the world's heat and its outer face radiates it to space:
3012 80–250 W/m² for a pane 0.1–1 m thick, **0.65–1.9 TW** over the cap. That is a cold wall the
3013 size of the cap, and it contradicted §1.9.4's "nothing breaks the thermal uniformity". Air
3014 cooled against it slides down the cap and goes sunward over the Deepward Sea as a steady
3015 cold wind.
3016- **The hull cannot lose heat by itself.** 1 km of rock conducting 320 W/m² would need about
3017 100,000 K across it. So the heat reaches the skin because Tower sends it there.
3018
3019**Decided by the author:**
3020
30211. **Tower controls the hull's heat loss, region by region, up to about 5 TW** — a tenth of
3022 the beam. "Not infinite, but some control." Light makes the weather; heat rejection is how
3023 Tower keeps the books.
30242. **The far end is kept dim and freezes in bands**, sunward to cap: open water z 496–512 km,
3025 pack ice 512–525, fast ice frozen to the cap 525–540, then the window.
30263. **The rod stays as D-079 has it**, running past the sun to the cap. So the fast ice gets
3027 dusk by day, tens of W/m², not darkness — "no big deal if we don't have complete
3028 darkness". Stars by day, which a truly dark end would have given, are given up with it.
30294. **The sun still goes to the cap** (D-074), so it goes out over the ice each evening.
30305. **The far cap is single-glazed and frosts. Tower clears it one pane per turn of the
3031 world**, 7 min 29 s, in a random order that is reshuffled every loop but always the same
3032 for a given loop. 282 panes (216 whole, 66 cut by the rim) make a loop of 35.1 hours.
3033
3034**Consequences worked out, now in §1.9.6:**
3035
3036- **The whole Deepward Sea is at about freezing**, not only the ice. §1.9.1 means there is no
3037 Coriolis to hold cold water in place; a 1 K difference drives an exchange of ~0.45 m/s,
3038 which mixes the 44 km sea to within about a kelvin. No Tower sea can be frozen in one corner.
3039- **The air pays the window, not the sea.** Paid by the sea, the open water would need
3040 140–400 W/m², daylight. So the weather is one overturning cell — warm air deepward aloft,
3041 the window wind sunward below — and the strength of that cell sets the ice edge.
3042- **The hull is turned down under the sea**, or the sea would freeze from its bed up. The far
3043 end takes about 2 TW of the 5.
3044- **The fast ice holds**: ~1,500 N per metre of wind against ~3 MN of ice, and Tower has no
3045 tides. Storms on the weather plan (D-067) are what could break it out.
3046- **The Deepward Sea's ninety ships** keep the 16 km of open water.
3047
3048**Numbers that are rough:** the window wind (5–15 m/s) and the frost rate (millimetres a day,
3049to a factor of ten). The frost rate decides how many panes are clear at once; one in ten is a
3050guess.
3051
3052**The sun cap is double-glazed** (author, 2026-09-29). Single glazing there would have done
3053all the same things — radiated, frosted, and sent a cold wind onto the Sunward Sea. Double, its
3054inner pane stays near the air's temperature, so the Sunward Sea stays temperate and the far end
3055is the world's only cold place.
3056
3057## D-091 — Drawing the far end: sea ice, the dim, and the frosted panes
3058
3059*Builds D-090.* Asked for by the author, 2026-09-29: "please commit and implement". Also
3060recorded here: the author chose **double glazing for the sun cap** (D-090), so the Sunward
3061Sea has no ice and needs nothing drawn.
3062
3063**The ice is in the world file.** A sea may carry `ice: { edgeM, fastM }` — the ice edge and
3064where the fast ice begins, as z — and the Deepward Sea does: 512 and 525 km. The fast ice runs
3065on from `fastM` to the end of the sea beyond it, whichever end that is. It is part of the
3066terrain signature, so changing it rebuilds the tiles, as it must: every water sample carries
3067its ice cover (`terrain/ice.ts`, `field.ts seaIce`).
3068
3069- **Cover.** None on the open water; the pack closes from 0.3 at its edge to 0.95 at the fast
3070 ice; 1 on the fast ice. The edge wanders 1.5 km either way by noise and the fast ice's 400 m,
3071 so neither runs ruler-straight round the world.
3072- **Floes.** The water shader breaks the pack into Voronoi cells about 110 m across — straight
3073 edges at odd angles, which is how ice breaks — each ice or open by its own hash against the
3074 cover. Floes stop short of open neighbours, opening leads up to 12 m wide where the pack is
3075 loose, and meet ice neighbours in a dark crack. The fast ice is one sheet, cracked in cells
3076 of 660 m. Floes are big enough to need no surface reference: absolute metres are good to
3077 centimetres in float32, the grid around is a whole number of cells, and the hash is a
3078 permutation mod 289, exact in float. Far off, the sea whitens by its share of ice.
3079- **Ice stills the sea.** Waves die away under the pack and are gone under the fast ice.
3080- **Ships keep to the open water**, their lanes 2 km short of the ice edge.
3081- **The map** whitens the water by the cover, and the exterior's x-ray, which is the map,
3082 with it.
3083
3084**The dim.** Tower's aimed light — the sun's and the moon's — falls from the open water's
3085shore (the sea's end away from the ice) to 0.35 at the ice edge, 0.1 at the fast ice, and
3086nothing on it. The rod is never aimed, so by night nothing changes but the moon, and by day
3087the fast ice keeps the rod's glow, taken as a twentieth of the sun's light (`ROD_GLOW_BY_DAY`
3088— tens of W/m² against about a thousand). Each lit surface is multiplied by its share **put
3089through the same 0.35 power the exposure gives the light dial**: an eye standing in the dim
3090has adapted to it, and at a straight twentieth the fast ice drew as a black hole. So it reads
3091as dusk, a third as bright as elsewhere. The stars go by the light where the eye is, so from
3092the fast ice the brightest of them show at dusk.
3093
3094**The frost** (`world/frost.ts`). The window's 282 panes are named by lattice cell and half,
3095the same triangles the mullions draw. Tower clears one per turn of the world (7 min 29 s) in
3096an order shuffled by the world's seed and the loop's number, reshuffled every 35.1-hour loop;
3097a cleared pane frosts over again as 1 − e^(−age / 10 h). Ten hours is chosen to leave about one
3098pane in ten nearly clear, §1.9.6's guess — the frost rate is rough to a factor of ten, and this
3099is the knob for it. The pane being cleared thaws across its turn. The clock writes the frost
3100into a 32 × 32 table the far cap's shader reads, so both views, inside and out, show the same
3101panes clear at the same moment. Frost is feathered rather than a grey sheet, and lit by the
3102light at the cap, which is the fast ice's dusk.
3103
3104**Left open** (design.md §5): the sun is drawn as bright from the fast ice as from anywhere,
3105though Tower aims none of its light there.
3106
3107**Tests** (`test/far-end.test.ts`): the bands and their cover, a sea with its ice the other
3108way round, ships clear of the ice, the dim by day and by night, 282 panes (216 whole) found
3109from points inside them, a loop of 35.1 hours, every pane cleared once a loop in a reshuffled
3110order, a pane thawing across its turn, no jump between loops, and about one pane in ten clear.
3111
3112## D-092 — A dead terrain worker is replaced at once, not after 15 s
3113
3114*Amends D-080.*
3115
3116**Reported** (author, 2026-09-29): "every so often, the page freezes on the loading screen
3117and there are errors in the console. a reload fixes it, or just waiting." The console showed
3118Firefox's "Attempting to create a Worker from an empty source" many times, and three workers
3119failing with an `error` event.
3120
3121**What it was.** D-080 made a silent worker survivable but slow: it was given 15 s to answer
3122its ping before being replaced, so a load with a dead worker sat on "Reading the plans" for
312315 s or more — the "just waiting". Workers die two ways, and both were waiting out that
3124deadline:
3125
3126- **A script that throws or fails to load** fires the worker's `error` event. That was only
3127 logged; the ping, which comlink never settles for a dead worker, went on waiting.
3128- **An empty script** — the "empty source" — fires nothing. Firefox starts it, it runs
3129 nothing, and it never answers.
3130
3131Why Firefox is handed an empty or broken script is still not pinned down. It was not
3132reproduced by fifteen plain reloads, nor by changing files mid-load. The likeliest cause is
3133the dev server answering while it rebuilds after an edit: the author reported it while files
3134were being edited under an open page. So this is a cure, not a prevention.
3135
3136**What changed** (`src/terrain/service.ts`):
3137
3138- The start-up ping is raced against the worker's `error` event, so a script that failed is
3139 replaced the moment it says so.
3140- A silent worker is judged **against its siblings**. They load the same modules at the same
3141 time, so once one has answered, a worker still silent after that sibling's own load time
3142 again, and never less than 3 s, is taken to be empty rather than slow. Only a pool with no
3143 worker alive at all still waits out the 15 s.
3144- A dead worker is replaced up to four times, 0.4 s, 0.8 s and 1.2 s apart, rather than once.
3145 Each failure is a `console.warn` naming the worker, and a worker that came back says which
3146 try it started on.
3147
3148**Checked in Firefox**, with the worker's script replaced by Playwright for the first few
3149requests:
3150
3151| Sabotage | Before | After |
3152|---|---|---|
3153| none | 7.4 s | 7.4 s |
3154| three scripts that throw | 22.8 s | **7.7 s** — no delay |
3155| three empty scripts | 22.3 s | **11.1 s** |
3156| all six empty, twice over | — | 38.6 s, and all six start on the third try |
3157
3158## D-093 — Ice mountains at the far cap: the Cap Range
3159
3160**Asked for** (author, 2026-09-29): "the ice at the far cap should slope up, since the
3161snow/ice would build up closer to the window. i'm imagining ice mountains that go a bit up
3162the window, even."
3163
3164**Why ice alone was not enough.** The ice is thickest against the window — snow off the
3165window wind and the meltwater of every cleared pane (D-090) come down at the cap's foot — so
3166it should rise toward the glass. But grounded ice spreads under its own weight until the
3167stress at its bed is what the bed will take, which caps it at the plastic profile of an ice
3168sheet, H² = H0² + (2τ/ρg)·x. With τ = 150 kPa, the top of what ice sheets show, it has to
3169fill 420 m of sea before any of it stands out, and over 8 km it reached about 280 m above the
3170water: built and drawn, it read from the pack as a pale strip under 1° high at the foot of a
317150 km window. Three ways on were offered — keep that, raise the sea bed (500–700 m, still a
3172kerb), or a rock range under the ice. **Chosen by the author: the range.**
3173
3174**What it is.** The **Cap Range** (provisional name): rock Tower raised at the foot of the far
3175cap for the ice to stand on, as it raised the Uplands (D-064). It is part of the Deepward
3176Sea's ice in the world file — `ice.range: { fromM: 524000, heightM: 2400 }` — and a label
3177feature, a region band from 528.5 km to the cap, carries its name.
3178
3179- **Rock** rises from the sea bed at 524 km to about 2,400 m at the cap, as t^1.6 so it
3180 steepens toward the window; ±30% round the ring, in massifs and saddles 30 km across; and
3181 ridged by up to 0.7 of its height, so it is a range and not a ramp.
3182- **Ice** mantles it from where the rock leaves the water (about 528.5 km), in a front of a
3183 few tens of metres over the fast ice, thickening as an ice sheet does to 350 m. It drapes
3184 the ridges (60% of their relief) rather than burying them level; where a crest stands out
3185 of it, it is bare rock.
3186- **At the window** it stands 2.2 to 3.3 km up the glass. The far cap's shader buries the
3187 glass and frame in ice to the height the ground stands at the cap, read off the terrain
3188 into a strip round the ring; the kilometre of bore between the world's end and the glass
3189 is ice too.
3190- **The sea no longer meets the cap.** The fast ice now runs 525 to 528.5 km and ends
3191 against the range. The Deepward Sea's band in the world file still runs to 540 km: the
3192 range stands in it.
3193
3194**Tests** (`test/far-end.test.ts`): the rock from bed to height, steepening; the ice's margin
3195at the waterline and its mantle thickening to 350 m; and on the compiled world, all round the
3196ring, dry ground 1.5–4 km high at the cap that varies, fast ice at 526 km, open water at
3197505. `test/terrain.test.ts` counts the new feature and probes the Deepward Sea short of it.
3198
3199## D-094 — The tour shows how long a leg has left: its caption's border fills in
3200
3201**Asked for** (author, 2026-09-29): "for the tour, it would be nice to have an indicator of
3202'we've been here 11 second, 3 more until transition', but preferrably in some intuitive way,
3203like the border of the tour text filling out or collapsing or something."
3204
3205**What it is** (`views/tour-player.ts`, `style.css .tour-ring`). The caption's border fills in
3206clockwise from its top-left corner as the leg runs, in the accent colour over the caption's own
3207faint border, and closes as the leg starts to fade to the next. The next leg's caption starts
3208empty. No numbers: a clock read at a glance, the way a progress ring is. It is drawn as an SVG
3209rectangle over the caption's border, its dash measured in pixels of the border's own length,
3210which follows the caption's size as its text changes. Both tours, inside and out, have it.
3211
3212## D-095 — The Fields: farmland where each crop is lit on its own day
3213
3214**Asked for** (author, 2026-09-30): "please add farm fields to the model. these should be
3215realistic fields, in their perfect bloom, so colorful. and this will be a new thing, but these
3216fields will receive 100% sunlight, even at night."
3217
3218**The conflict, raised before building.** Tower's night is made by banking the beam, because
3219the rod's glow cannot be aimed away (§1.3.2). Fields lit at full sun all night need the beam
3220up, and the whole world gets the rod's glow with it: 250 km² of fields would add about
32210.5 W/m² everywhere, all night; 1,000 km², about 2 W/m², a permanent twilight. **The author
3222chose 250 km²**, at z 430–496 km (the unauthored belt by the far end) — and, asked how the
3223flowers stay in bloom, changed the brief: "flowers get darkness, and other things (corn,
3224potatoes, tomatoes, etc.) will have appropriate timings. i thought 100% daylight would be
3225helpful for plants, but if it's not, adjust as appropriate, since Tower can provide whatever
3226lighting."
3227
3228**What it is.**
3229
3230- **The Fields** (provisional name), a new feature kind, `farmland`: 250 km² at z 455–480 km,
3231 10 km around, where the Axial Line ends. Its edge wanders by noise; it clears forest, and
3232 stays off water, sand, rock and broken ground.
3233- **The fields** (`terrain/farms.ts`) are cells 480 m along the axis by about 360 m around —
3234 the ring a whole number of them — some split in two, with 6 m tracks between. Each is one of
3235 sixteen crops by a weighted hash, three-quarters of them flowers: tulips red, yellow, pink
3236 and white, lavender, sunflowers, rapeseed, poppies, flax, phacelia, buckwheat; and maize,
3237 potatoes, tomatoes, wheat, lettuce. Each crop has its bloom, fruit or ripe-head colour, its
3238 leaf colour, how much of it shows, and its row spacing. The layout is a pure function of
3239 position, written out in GLSL too, so the map and both views agree field by field.
3240- **Each crop's day** follows its plant: 18 h for rapeseed, flax and wheat; 16 h for lavender,
3241 poppies, phacelia and tomatoes; 14 h for tulips, sunflowers, maize and lettuce; 12 h for
3242 buckwheat and potatoes. Every crop gets at least six hours of dark. Its day starts at sunrise
3243 and is lit as at noon throughout — on into the night where it is longer than the sun's;
3244 where it is shorter, cut to the rod's glow, since Tower cannot make a field dark by day.
3245- **Drawn** as rows over soil with flowers or fruit among the leaves, clumps and gaps close in,
3246 and the crop's average colour once rows are under a pixel. Lit fields are drawn as at noon
3247 at the dial's brightness whatever the hour, so after sunset they blaze in a dark world and
3248 go out crop by crop. The map colours each field by its crop.
3249
3250**The night it costs.** At the equinox about 91% of the fields are still lit at sunset —
3251220 GW, 0.4% of the beam — adding about 0.4 W/m² of rod glow everywhere, falling to 0.2 at
3252two hours, 0.1 at four, and nothing at six. **This is canon but not drawn**: the world's
3253night light in the app does not yet include it (design.md §5).
3254
3255**Tests** (`test/farms.test.ts`): the ring closes on whole cells; the same field at the same
3256place and across the seam; tracks between cells; every crop planted, flowers most; a long-day
3257crop lit into the night then dark, a short-day crop dimmed before sunset; six hours of dark for
3258every crop; and on the compiled world, farmland inside The Fields, cleared of trees, and none
3259outside it.
3260
3261## D-096 — The farms spread round the world, and the world holds still while it is made
3262
3263*Amends D-095.*
3264
3265**Asked for** (author, 2026-09-30): "the fields are concentrated in a single region. let's
3266split them up and rearrange them around the world." And, separately: "don't run the world
3267while the building process is going on. the 'sun' moves while the world is still building,
3268and that seems weird. have everything be paused until the build process is all finished."
3269
3270**Nine farms instead of one.** The 250 km² D-095 chose for the night stays the total; it is
3271now nine farms of about 28 km² each, rounded rectangles 6.2 km along the axis and 4.8 km
3272around, each its own `farmland` feature with a provisional name:
3273
3274| Farm | θ | z | Where |
3275|---|---|---|---|
3276| Ringside Fields | 1.7 | 129 km | the Uplands, beside the Uplands Ring |
3277| Far Ringside Fields | 4.3 | 131 km | the Uplands, the other side of the world |
3278| Greatwood Clearing | 2.6 | 240 km | Greatwood, near its sunward edge |
3279| Capital Fields | 5.4 | 262 km | Greatwood, 44 km antispinward of the Capital |
3280| Deepwood Fields | 3.8 | 320 km | Greatwood, opposite The Deep |
3281| Greatwood Fields | 2.0 | 380 km | Greatwood's deepward half |
3282| Railhead Fields | 0.0 | 470 km | where the Axial Line ends |
3283| Far Fields | 3.4 | 455 km | before the far end, opposite the railhead |
3284| Last Fields | 5.0 | 470 km | the nearest to the Deepward Sea and the ice |
3285
3286Each site was checked on the compiled world for open ground — no water, desert, bare rock
3287or range — before it was placed. The Uplands are the roughest: part of each Uplands farm
3288falls on broken ground and is left unploughed, as the farmland's own rules leave it. The
3289ones in Greatwood clear their ground of trees, so they read as clearings.
3290
3291**The world holds still while it is made.** The clock (`world/clock.ts`) gains `held`: it
3292keeps whether it is running — the panel still says so — but no time passes. The app holds it
3293from the moment it starts until the status dialog has gone, or, for a compile quick enough
3294never to show the dialog, until the compile is done; and again whenever the world is
3295recompiled. Both views read it too: while it is held their frame time is zero, so the hull
3296does not spin, the water does not move, the sky through the far cap does not turn and a tour
3297does not advance.
3298
3299**Tests:** nine farms sharing 240–260 km², spread more than 4 rad round the ring and 300 km
3300down the axis, each farmland at its heart and cleared of trees.
3301
3302## D-097 — Farms grow over the land, and their fields are irregular
3303
3304*Amends D-095 and D-096.*
3305
3306**Asked for** (author, 2026-09-30): "the current rectangular regions (both the general 9 and
3307the farms inside them) are very orderly, can we do some sort of natural growing pattern that
3308results in more random-looking patterns?"
3309
3310**A farm grows** (`terrain/farms.ts growFarm`, compiled last, over the land as it is). A farm
3311in the world file is now a **point and an area** — the nine of D-096 keep their places, each
3312`areaKm2: 27.8`. At compile time it grows from the point over a 200 m lattice of patches, always
3313taking next the patch on its edge with the lowest key: half how far it is from the seed (over
3314the radius a disc of that area would have), half luck, by hash. It never takes ground that is
3315wet, sand, rock, a range (over 150 m of relief), ice, or steeper than 12%, so it goes round a
3316river or a hill instead of over it, until it has its area. Near-and-lucky is what makes it a
3317blob with lobes and bays — a test holds it to at least 1.3 times the edge of a disc the same
3318size, all in one piece. A field belongs to the farm if the patch under the point it was laid
3319out from does, so **the farm's edge runs along its fields' edges** and no field is cut in half;
3320the ground shader sharpens the vertex-interpolated weight so the edge does not smear. A farm
3321may still be drawn as a polygon or band, which is its outline as before.
3322
3323**The fields are irregular** (`fieldAt`, and its GLSL twin). In place of D-095's grid:
3324
3325- **Blocks** about 1.5 km across: a jittered Voronoi of the surface, a whole number of them
3326 round the ring, with 6 m farm tracks along their edges.
3327- **Each block on its own terms**: its fields run at its own angle, are its own size (220 to
3328 440 m) and drawn out long by its own stretch (1.3 to 2.2), as land cleared at different times
3329 by different hands is.
3330- **Fields within a block**: a second Voronoi, in the block's frame — straight edges at odd
3331 angles — with grass margins between. A third are cut into two to four long strips.
3332- Rows run along each field's length. Everything inside a block is measured from the block's
3333 centre, so the pattern has no seam round the ring and its numbers stay small for float32.
3334
3335**Tests** (`test/farms.test.ts`): no seam; fields of many sizes (the largest fifth at least
3336twice the smallest fifth); tracks along the blocks and only there; every crop, flowers most;
3337a grown farm taking its area in one piece, ragged, and going round ground that does not fit;
3338and in the world, nine farms grown to 240–260 km² between them, farmland and no trees about
3339each seed.
3340
3341## D-098 — On a phone, the height readout fades when the height is still
3342
3343**Asked for** (author, 2026-10-01): "the height label takes up a lot of space on mobile, can the
3344big rectangle box fade away or something?"
3345
3346**What it does** (`ui/eye-gauge.ts wake`, `style.css`). Below 720 px the eye-height readout —
3347the boxed height, gravity and comparison beside the gauge's thumb — fades out over 0.6 s once
3348the height has been still for two seconds, and comes back at once when the height changes by
3349more than half a percent (flying, a tour, the gauge) or the gauge is touched; it stays while the
3350gauge is held. The view re-telling the same height every frame does not wake it. The thumb and
3351the ticks stay, so the height is still read off the scale. Wider screens are unchanged: there the
3352box is not in the way. Screen readers are unaffected — the gauge's value text is always current.
3353
3354## D-099 — Photographs: a view saved as numbers, rendered again on demand
3355
3356**Asked for** (author, 2026-10-03): "i've been finding interesting photo opportunities, like
3357lining things up at a particular time, etc. and i've started to use these photos as
3358illustrations, after messing with them in lightroom. let's come up with a way for me to choose
3359the parameters, then render the latest version of that image." And, of the choices then put to
3360the author: save from the app with a key and a button that show up only locally; 4K; no labels at
3361all; the renders in a `photos/` folder that git ignores; the exterior first, then the two caps
3362from the Capital.
3363
3364**The list** (`content/photos.json`, `src/world/photos.ts`). A photograph is a view, a moment
3365and a frame: the world clock in hours, the Light, the frame's aspect, and then for the
3366interior where the observer stands and looks (`theta`, `z`, `eyeM`, `yaw`, `pitch`, `fovDeg`)
3367and how far the far cap's sky has wheeled (`spin`); for the exterior the camera and its target
3368in kilometres, the hull's turn (`phase`), the field of view and the hull reading. The exterior
3369has no zoom, so a saved shot's field is always its own 42°; the number is there to be typed,
3370to frame a shot tighter than the view can. Its `id` is its file name.
3371`parsePhoto` checks every field and says which is wrong, because the list is typed by hand as
3372often as it is saved, and the id is held to letters, digits and hyphens because it becomes a
3373path.
3374
3375**Saving** (`ui/app.ts savePhoto`, `vite.config.ts`). **P**, or **Save this shot** under View,
3376takes the open 3D view, asks for a name, and posts it to the dev server, which writes the
3377list; a name already there is re-framed after a confirm, keeping its place and its note. This
3378is the one place the browser causes a file to be written, and D-072 stands: it is not the
3379world, it is not in the build (the row is hidden and the key does nothing unless
3380`import.meta.env.DEV`), and the endpoint answers only the machine the server runs on, since
3381`npm run dev` is open to the network. A shot is refused while a scenic tour runs: the tour's
3382flyers and camera are its own and would not come back.
3383
3384**Going back to one** (author, the same day: "it would be helpful to be able to navigate back
3385to existing points, like replaying. show a list of current ones and allow loading one?").
3386**Saved shots**, under the button, lists the file as the dev server has it now, read again
3387each time the list is opened. Choosing one opens its view and poses it (`poseShot`, not held),
3388with the clock stopped at its hour and, outside, the spin switched off, since otherwise the
3389framing is gone before it is seen; the view carries on from there, and the shot's name is
3390offered when the next one is saved, so adjusting and re-framing is two keys.
3391
3392**Rendering** (`tools/photos.mjs`, `npm run photos`). Each shot is opened with its clock in
3393the address, posed through `window.towerPhotos.show` — which sets the Light and the hull and
3394calls the view's `holdShot` — and photographed with everything but the canvas taken away:
3395panel, readouts, gauge, view buttons, labels. The window is the shot's aspect, 1920 CSS px on
3396its long edge at two device pixels, so the picture is 3840 px and the stars and lines keep the
3397weight they had on the author's screen. `--long-edge` changes the size, `--only` picks shots.
3398A held view stands still — the hull does not turn, the far cap's sky does not wheel, the water
3399is at the held scenes' moment, the plant does not vent — and an interior entry rendered twice
3400gave the same bytes. Posed directly, not through the UI: the screenshot tour (D-042) is a test
3401of the application and stays as it is; this is a camera. The two share `tools/browser.mjs`,
3402and the tour hides the save button, since it runs on a dev server and photographs what
3403readers get.
3404
3405**Not done.** A shot taken during a tour.
3406
3407**The list as it starts.** The far cap and the sun cap from 300 m over the Capital, at 08:45
3408(author: "i like the cap photos, but please change them to be at around 8:30-9am"). And
3409`tower-from-the-far-end`: the author's blog illustration, whose settings were never saved,
3410worked back from the JPEG. Its stars matched a render's at 1.28 times the size, so it is a
3411crop — hence the 33.31° field; the camera was fitted by least squares to the far cap's hub
3412and outline, the collector's rim and the sun, to about 3 px; the stars put it on day 5, and
3413the lit rod seen through the far cap puts the sun 525 km along, 18:47. The cap's frame
3414repeats every sixth of a turn and the rock could not be matched through the processing, so
3415`phase` is 0 by default rather than by measurement.
3416
3417**Tests** (`test/photos.test.ts`): both kinds of shot read back as written; a bad field named;
3418ids that could leave the folder refused; every entry in the list valid and uniquely named.