fuzzyworld

tower / cascadia / files / decisions.md

Branch cascadia · default branch · all branches

decisions.md

96.6 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

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.

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.

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.

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.

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

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

§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

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

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

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 — Cascadia at 1:1, as a ruler

Tower's inner surface is 169,646 km², which design.md §1 has always given as Wisconsin's total area. That is a fact about a number, and a number is a poor way to feel a size. The question this decision answers is the reader's: how big is this actually?

The obvious answer — put Wisconsin inside it — does not work, and finding out why is what produced the answer that does. Wisconsin has Tower's area but not its shape: it spans about 525 km east–west and 510 km north–south, while Tower unrolls to a 540 x 314 km rectangle. At 1:1 it cannot be laid down at all, because 525 km of state will not go round a 314 km circumference; it overlaps itself by 67%. Scaled to fit undistorted it covers a third of the surface, and squashed to fill it it is stretched 1.77:1 and no longer Wisconsin. The areas match and the shapes do not, which is worth recording as the honest form of the coincidence.

Cascadia works where Wisconsin cannot, because it is a corridor and Tower is a corridor. The coast runs north–south, the Sound runs north–south, the Cascade crest runs north–south. And the width is a second coincidence, better than the first: measured in UTM 10N, the ground from the Pacific surf at Kalaloch east to the Columbia River at Entiat is 312.8 km, against a circumference of 314.2 — 1.4 km of slack. Western Washington closes the ring on real terrain. Walk east from the beach, over the Olympics, across Puget Sound, over the Cascades, down the east flank to the Columbia, and you arrive back at the beach.

Nothing is invented and nothing is rescaled. The consequence, which is the reason this reads as a world rather than as a diagram: the point 100 km straight over Seattle is both the surf north of Kalaloch and the Columbia at Entiat — one place, reached by walking either way round. Seattle sits 150 km deepward of the sun cap, Olympia 88, Bellingham 277, Vancouver 336.

Tower has no north, which is what makes this legitimate rather than a cheat. The compass is sunward/deepward/spinward/antispinward (content/geography.json), so orienting a real region to the cylinder distorts nothing — there is no north to distort against.

The seam, and what was tried against it

Theta wraps and real geography does not, so column 0 and column 1191 are neighbours on the cylinder while being 314 km apart on Earth. The step between them is 852 m median, 1,537 m at p90 — small through the settled half (369–844 m from the Columbia bar to Bellingham) and largest past 49°N, in fjord country where kilometre walls are already normal.

Two levers were measured rather than argued about:

So the seam stays where it is, and is treated as a landform rather than an error: a scarp running the length of the world exactly where the wet side meets the dry one, which is the truest thing about this coast. Real analogues are not hard to find — the Blue Ridge escarpment, the Drakensberg.

What it costs the codebase

Almost nothing, deliberately. The Cascadia rasters borrow canon's grid and height window rather than declaring their own (-560..4400 m, 2048 x 1192). That is not a convenience, it is what keeps the mode cheap: src/views/interior.ts bakes the height window into its GLSL as literals, so a surface with its own range would need a second shader. It costs nothing to share because Cascadia happens to fit — the highest land here is Rainier at 4,352 m, under the 4,400 m ceiling. Deep ocean is clamped instead, which is invisible under water; carrying the abyssal plain's 2.6 km would coarsen every hillside in the world to pay for it. test/raster.test.ts reads the constants back out of tools/cascadia.mjs and holds it to them, because this is D-024's failure mode exactly: a surface that quietly disagreed would displace its ground rather than fail.

So a surface is a swap of two PNG filenames (src/views/raster.ts), and the three views each gained a setSurface.

What it is not

This is not canon and contains no Tower geography. It contradicts §1.9.3 outright — all relief in Tower is engineered, because a bare cylinder is an equipotential with no slope anywhere — and the rivers are wrong in a way worth naming: they were carved under Earth gravity with no Coriolis, so they do not lean antispinward the way §1.9.2 says everything falling in Tower does. Snoqualmie Falls is a compass pointing the wrong way.

It is therefore labelled in the panel, defaults off, and draws no places. The world list still shows them, because that is the author's data and hiding it would look like loss, but nothing is drawn on the terrain: a marker reading "The Capital" pinned to Puget Sound would claim a correspondence that does not exist. Probes and sightlines stay, because those measure the cylinder, and the cylinder is the same either way. The choice is not carried in the URL hash either — a permalink names a place in Tower, and Cascadia has none.

Elevation comes from the AWS open terrain tiles (SRTM/3DEP over land, GEBCO below water, Canadian ground north of 49°N in the same grid), cached under .cache/ and re-fetchable; they are not source and are not committed.

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