tower / interior-flight / files / README.md
Branch interior-flight · default branch · all branches
README.md
cf4f8772 2026-09-07 Pavel Safronov | 1 | # Tower |
| 2 | ||
| 3 | **Live map — <https://pavelsafronov.gitlab.io/tower/>** · [Source](https://gitlab.com/PavelSafronov/tower) | |
| 4 | ||
| 5 | Companion piece to a novel set inside **Tower** — a fictional smart cylinder orbiting Earth, | |
| 6 | spun for 1 g, with an inner surface the size of Wisconsin, lit by 54 TW of sunlight | |
769726ea 2026-09-08 Pavel Safronov | 7 | piped down its axis and scattered by mirrors that make its days and nights. Its wall |
| 8 | is a kilometre of sealed hull: no windows along all 540 km, and no view of the Earth | |
9c891d2e 2026-09-08 Pavel Safronov | 9 | it orbits. Both end caps are windows, though — 100 km of glass each, and the only |
| 10 | places in the world with a horizon that is not Tower. Nobody aboard can see the sun | |
| 11 | either: the 244 km collector sits between Tower and it, 79 times its diameter, so | |
49b0d64d 2026-09-08 Pavel Safronov | 12 | the sun cap looks out at a black disc and the stars around it — black except at its |
| 13 | edge, where the structure holding it flat catches the sun side-on and traces the | |
| 14 | whole 244 km in a thin sun-coloured ring. | |
cf4f8772 2026-09-07 Pavel Safronov | 15 | |
17ccee99 2026-09-08 Pavel Safronov | 16 | **Tower's clock is painted on it**, because it is the only thing in the sky that |
| 17 | does not turn. Two lit lines from the hub: one fixed normal to the plane of the | |
| 18 | solar system, one sweeping a full turn over a day. You read the *angle between | |
| 19 | them* — from inside, the whole face wheels once every 7 min 29 s along with the | |
| 20 | stars, so a hand against a dial would be useless, but an angle survives it. | |
| 21 | ||
28f9a6bc 2026-09-07 Pavel Safronov | 22 | Explore Tower from outside, pick a spot on the inner surface, and stand there — with |
| 23 | the ground curving up on both sides and the far side of the world hanging 100 km | |
| 24 | overhead, clearer than the ground at your feet. Turn its sun up to Earth clear-sky | |
769726ea 2026-09-08 Pavel Safronov | 25 | noon, down to permanent dusk, or shut the beam off entirely and watch the sky come |
| 26 | out: through the far cap the stars wheel once every 7 min 29 s about a single | |
| 27 | motionless star on the axis. Nothing inside Tower rotates, because you and the | |
| 28 | ground and the air all turn together — so that window is the only place the spin is | |
| 29 | visible at all. | |
cf4f8772 2026-09-07 Pavel Safronov | 30 | |
| 31 | Tower flies all 78 billion mirrors of its own sunlight, individually, because | |
| 32 | nothing passive would stay in the beam past mid-afternoon. It is a person. | |
| 33 | ||
| 34 | ## Status | |
| 35 | ||
28f9a6bc 2026-09-07 Pavel Safronov | 36 | **All three views are built** — map, exterior and interior — over an authored |
| 37 | geography and a working writer mode. | |
cf4f8772 2026-09-07 Pavel Safronov | 38 | |
28f9a6bc 2026-09-07 Pavel Safronov | 39 | Built: the unrolled surface map, Tower from outside, standing inside it, place |
| 40 | editing for thirteen kinds (points, lines, polygons and bands), localStorage | |
| 41 | autosave, backup and static exports, import, permalinks, and the sightline probe. | |
cf4f8772 2026-09-07 Pavel Safronov | 42 | |
| 43 | - [design.md](design.md) — canon, physics and application architecture | |
| 44 | - [decisions.md](decisions.md) — choices made and why | |
| 45 | ||
| 46 | Both are series-bible material: coherence outranks spectacle, and the prose is | |
| 47 | downstream of them. | |
| 48 | ||
fc11b064 2026-09-08 Pavel Safronov | 49 | On a phone the sidebar is a **drawer** rather than a column: below 720 px the world |
| 50 | gets the whole screen and the panel slides in over it, one tap away and one tap | |
| 51 | gone. Picking a place closes it, because in every view selecting a place is how you | |
| 52 | travel there — see [decisions.md](decisions.md) D-043. | |
| 53 | ||
6364748a 2026-09-08 Pavel Safronov | 54 | ## Screenshots |
| 55 | ||
| 56 | [](docs/screenshots/overview.png) | |
| 57 | ||
| 58 | `npm run screenshots` drives the real application in a headless browser — clicking | |
| 59 | the view buttons, picking places out of the list, dragging to orbit and to look up — | |
| 60 | and writes [`docs/screenshots/`](docs/screenshots/). Taking them *through the UI* is | |
| 61 | the point: a shot taken this way fails when the UI fails, so a black canvas ends up | |
| 62 | in the picture instead of hiding behind a passing unit test. | |
| 63 | ||
18958d3d 2026-09-08 Pavel Safronov | 64 | The whole tour takes about **35 seconds**. It used to take six and a half minutes, |
| 65 | nearly all of it spent rendering Tower's haze integral on the CPU — the default | |
| 66 | headless browser has no GPU. It now asks for the full Chrome build, which reaches | |
| 67 | the real one, and photographs four shots at a time, since a shot is mostly spent | |
| 68 | waiting. The cost is that the committed images are tied to the GPU that drew them: | |
| 69 | another machine will show a large one-off divergence rather than a regression. | |
| 70 | ||
6364748a 2026-09-08 Pavel Safronov | 71 | | Desktop — 1440×900 | | |
| 72 | |---|---| | |
| 73 | | [map](docs/screenshots/desktop-map.png) | the unrolled inner surface, with a place selected | | |
| 74 | | [map-sightline](docs/screenshots/desktop-map-sightline.png) | shift-click two points to measure a sightline through the air | | |
4699e6cb 2026-09-08 Pavel Safronov | 75 | | [exterior](docs/screenshots/desktop-exterior.png) | x-ray hull — the inner surface projected out, with the caps and the collector | |
| 76 | | [exterior-true](docs/screenshots/desktop-exterior-true.png) | true hull — bare skin, and the land visible through the far cap window | | |
6364748a 2026-09-08 Pavel Safronov | 77 | | [interior](docs/screenshots/desktop-interior.png) | standing at The Narrows, the ground curving up on both sides | |
| 78 | | [interior-overhead](docs/screenshots/desktop-interior-overhead.png) | looking up: the far side 100 km overhead is clearer than ground 100 km away | | |
17ccee99 2026-09-08 Pavel Safronov | 79 | | [interior-night](docs/screenshots/desktop-interior-night.png) | the far cap window at night: stars, and the beam banked to a thousandth | |
| 80 | | [interior-suncap](docs/screenshots/desktop-interior-suncap.png) | the sun cap window: the collector, its sunlit rim, and Tower's clock on its face | | |
1fcef8e6 2026-09-08 Pavel Safronov | 81 | | [interior-flying](docs/screenshots/desktop-interior-flying.png) | flying — 4 km up, off the ground, with the world closing overhead | |
2bb3e0de 2026-09-08 Pavel Safronov | 82 | | [interior-dawn](docs/screenshots/desktop-interior-dawn.png) | dawn — a lit sky over dark ground, the light still 27 km away | |
6364748a 2026-09-08 Pavel Safronov | 83 | | [reader](docs/screenshots/desktop-reader.png) | `?reader=1` — terrain, places, a detail card and the probe, no editing | |
| 84 | ||
| 85 | | Mobile — 390×844 | | | |
| 86 | |---|---| | |
| 87 | | [map](docs/screenshots/mobile-map.png) | the world with the whole screen, the panel one tap away | | |
| 88 | | [exterior](docs/screenshots/mobile-exterior.png) | the spin readout dropped clear of the panel button | | |
| 89 | | [interior](docs/screenshots/mobile-interior.png) | the standing readout at a size that fits 390 px | | |
| 90 | | [panel](docs/screenshots/mobile-panel.png) | the sidebar as a drawer, opened over the world | | |
| 91 | ||
| 92 | ### Looking before committing | |
| 93 | ||
| 94 | [`overview.png`](docs/screenshots/overview.png) is a contact sheet of all ten. It | |
| 95 | exists to be **looked at, once, before committing** — one image that answers "is the | |
| 96 | application still there?". Regenerate, glance at it, then commit. | |
| 97 | ||
| 98 | `--check` puts a number on the same question. Every shot is compared against the | |
| 99 | committed one and the run fails if any of them moved too far: | |
| 100 | ||
| 101 | ``` | |
| 102 | npm run screenshots -- --check # fail past 3% divergence on any shot | |
| 103 | npm run screenshots -- --threshold 8 # report only, looser bar | |
| 104 | npm run screenshots -- --url http://localhost:5173 # use a dev server already up | |
| 105 | ``` | |
| 106 | ||
18958d3d 2026-09-08 Pavel Safronov | 107 | The threshold is measured at both ends, not guessed. The 2D shots come out |
| 108 | **byte-identical** between runs — nothing in Tower is stochastic, the exterior spin | |
| 109 | is switched off, and every shot gets a fresh browser context so no camera leaks into | |
| 110 | the next. **The WebGL shots do not**, because the tour draws on the real GPU and | |
| 111 | Metal does not schedule floating-point work identically twice; they drift by 0.1% to | |
| 112 | **0.9%**. Against that, hiding both 3D canvases scored **7% to 31%**, and a view | |
| 113 | going entirely black scores 12% to 39%. | |
6364748a 2026-09-08 Pavel Safronov | 114 | |
| 115 | **3% is three times the worst noise and less than half the mildest real failure.** | |
| 116 | It is a smoke alarm, not a verdict: when it fires, go and look at the sheet. Note | |
| 117 | that a view which never worked scores 0% when it breaks further — this catches | |
| 118 | regressions, not things that were already broken. | |
| 119 | ||
cf4f8772 2026-09-07 Pavel Safronov | 120 | ## Development |
| 121 | ||
| 122 | ``` | |
| 123 | npm install | |
| 124 | npm run dev # http://localhost:5173 | |
28f9a6bc 2026-09-07 Pavel Safronov | 125 | npm test # 94 tests; canon figures are pinned to design.md |
cf4f8772 2026-09-07 Pavel Safronov | 126 | npm run build |
| 127 | npm run terrain # regenerate the maps from content/geography.json | |
6364748a 2026-09-08 Pavel Safronov | 128 | npm run screenshots # drive the app and rephotograph docs/screenshots/ |
cf4f8772 2026-09-07 Pavel Safronov | 129 | ``` |
| 130 | ||
| 131 | Vite + TypeScript, deployed to GitLab Pages by `.gitlab-ci.yml` on every push to the | |
28f9a6bc 2026-09-07 Pavel Safronov | 132 | default branch. Three.js is loaded only when a 3D view is opened, so a reader who |
| 133 | stays on the map never downloads it. | |
cf4f8772 2026-09-07 Pavel Safronov | 134 | |
4a8aed84 2026-09-07 Pavel Safronov | 135 | Assets are referenced **relatively**, so the same build works whether Pages serves |
| 136 | it from `https://pavelsafronov.gitlab.io/tower/` or from a generated unique domain | |
| 137 | at a root. Set a `BASE` CI variable to force an absolute base if that is ever | |
| 138 | needed. | |
cf4f8772 2026-09-07 Pavel Safronov | 139 | |
| 140 | ## Geography | |
| 141 | ||
| 142 | [`content/geography.json`](content/geography.json) is the authored source of truth | |
28f9a6bc 2026-09-07 Pavel Safronov | 143 | for terrain — coordinates in canon (θ, z), elevations in metres relative to the base |
| 144 | shell, negative for anything cut below it. `render.minHeightM` / `render.maxHeightM` | |
| 145 | declare the range the height raster is stretched across; the generator prints the | |
| 146 | range the terrain actually occupies and warns if it has outgrown the declaration. `npm run terrain` renders it, with no dependencies beyond Node: | |
cf4f8772 2026-09-07 Pavel Safronov | 147 | |
| 148 | | | | | |
| 149 | |---|---| | |
28f9a6bc 2026-09-07 Pavel Safronov | 150 | | `static/terrain-height.png` | 16-bit greyscale, across the declared `minHeightM`–`maxHeightM` | |
62458672 2026-09-07 Pavel Safronov | 151 | | `static/terrain-color.png` | the whole world, 264 m/px | |
| 152 | | `static/terrain-corridor.png` | the developed corridor, 122 m/px | | |
| 153 | | `static/world.json` | seed places for the app | | |
cf4f8772 2026-09-07 Pavel Safronov | 154 | |
| 155 | It reports each stage as it goes — every relief op, colourisation, and PNG encode — | |
| 156 | and takes about 6 seconds. Flags: | |
| 157 | ||
| 158 | | | | | |
| 159 | |---|---| | |
| 160 | | `--probe <theta>` | print elevation jumps along a row, for checking river gradients and finding discontinuities | | |
| 161 | | `--quiet` | one line per stage instead of one per feature | | |
| 162 | | `--force` | let the seed overwrite places added in the app | | |
| 163 | ||
| 164 | ## Reading and writing | |
| 165 | ||
| 166 | The map opens in **writer mode**: tools, editor, canon controls and import/export. | |
| 167 | Add **`?reader=1`** to preview what a reader gets — the terrain, the places, a | |
| 168 | detail card and the sightline probe, with no editing. Writer is the default while | |
| 169 | the world is still being built; that is a one-line change when it is ready to | |
| 170 | publish. Either way it is a UI mode, not a security boundary — with no backend an | |
| 171 | editor can only change their own copy (decisions.md D-011). | |
| 172 | ||
| 173 | Any selection updates the URL, so a link is always shareable: | |
| 174 | ||
| 175 | | | | | |
| 176 | |---|---| | |
| 177 | | `#place=the-narrows` | select and centre a place | | |
| 178 | | `#at=0.03,170000` | drop a sightline endpoint at (θ, z) | | |
| 179 | ||
5f38efd0 2026-09-07 Pavel Safronov | 180 | ## Views |
| 181 | ||
a3f01bd7 2026-09-08 Pavel Safronov | 182 | **Map** is the unrolled inner surface. **Exterior** is Tower from outside — a |
| 183 | kilometre of hull, the checkered sun cap, the glazed far cap, the 244 km collector | |
| 184 | flying ahead and the beam converging into the sun cap. Each piece is captioned. | |
| 185 | ||
| 186 | The hull reads two ways and the **Hull** switch picks one. **True** is the outer | |
| 187 | skin: bare structure, and the far cap a real window — look in through it and the | |
| 188 | land is actually there, lit and receding 540 km down the tube. It is the only line | |
| 189 | of sight into Tower that is not a diagram. **X-ray** wraps the terrain onto the | |
| 190 | outside instead, which is a diagram of what is inside rather than a view of it, and | |
| 191 | useful precisely because the wall is opaque. The active view is remembered and travels in the permalink, so | |
8b6287ba 2026-09-07 Pavel Safronov | 192 | `#view=exterior&place=the-narrows` opens where you left off. Drag to orbit, scroll to zoom, click the hull to drop a point; the spin runs at |
5f38efd0 2026-09-07 Pavel Safronov | 193 | 1x to 1200x real time, and a point picked off the turning hull still comes back in |
| 194 | canon coordinates. | |
| 195 | ||
28f9a6bc 2026-09-07 Pavel Safronov | 196 | **Interior** is standing inside. The shell is the real cylinder, displaced by the |
| 197 | height raster and painted with the colour one, so the ground curves up on both sides | |
| 198 | and closes overhead. Haze is integrated through Tower's rotational air column for | |
| 199 | every pixel, which reproduces the strangest fact about the place: the land 100 km | |
| 200 | straight up is **78%** clear while the ground 100 km away is **31%** — distant | |
| 201 | geography here is best seen by looking up. | |
| 202 | ||
| 203 | Drag to look, scroll to zoom in like a pair of binoculars, and select a place — in | |
| 204 | the list or by its caption — to **walk there**. Most of the world is beyond the haze | |
| 205 | horizon from any given spot, so travelling is more use than turning. | |
| 206 | ||
769726ea 2026-09-08 Pavel Safronov | 207 | **The far cap is a window** — 100 km of it, looking into deep anti-sun space. By day |
| 208 | it is simply a disc of blue, because 540 km of air in front of it scatters more | |
| 209 | light than anything behind it sends; the stars are there at noon and unseeable, | |
| 210 | which is a fact about eyes rather than about glass. Turn the light down and they | |
| 211 | come out. The sun cap stays a checkered plate: it faces the sun, and needs glare | |
| 212 | management rather than a view. | |
| 213 | ||
da6bcb0e 2026-09-08 Pavel Safronov | 214 | The one part of the far cap that is not glass is a **3.2 km mirror** on the axis. |
| 215 | A quarter of the beam survives all 540 km and arrives collimated, so it lands as | |
| 216 | 1.7 MW/m² — 1,200 suns — on a spot a thousandth of the cap's area; the mirror sends | |
| 217 | it back up the column for a second pass, which lifts the beam's utilisation from 76% | |
| 218 | to 94%. You can see it: the rod of light visibly terminates on a bright disc, and | |
| 219 | the disc banks and goes out with the beam. | |
| 220 | ||
769726ea 2026-09-08 Pavel Safronov | 221 | The checker survives on the far cap as the window's **mullions** — the same polar |
| 222 | tessellation at the same pitch, so it is still the only object in the world with a | |
| 223 | known cell size and still the only rule anyone has for judging distance. It is just | |
| 224 | read against sky now instead of against a plate. | |
| 225 | ||
2bb3e0de 2026-09-08 Pavel Safronov | 226 | **Dawn is a line, not a fade.** The beam glows by itself, so when its power comes up |
| 227 | the sky brightens everywhere at once — but the ground is lit by the diffusers, and | |
| 228 | they hand the light down the axis. So a Tower morning is a **fully lit sky over dark | |
| 229 | ground**, with daylight arriving as a front running deepward from the sun cap at | |
| 230 | 225 m/s, crossing all 540 km in 40 minutes. Tower has no latitude; the cycle gives it | |
| 231 | longitude. Scrub the **Time** slider to 06:17 and stand at The Capital to watch it | |
| 232 | come, or tick **Run the clock** and let it. | |
| 233 | ||
| 234 | The light has five settings when the clock is not driving it. **Dark** shuts the 54 TW beam down entirely and is the | |
769726ea 2026-09-08 Pavel Safronov | 235 | only one that is genuinely black — there is no airlight, no lit ground and no rod |
| 236 | overhead, and the only thing left in the world is the sky. **Night** banks the beam | |
| 237 | to a thousandth instead of killing it, which is what night here has to be: the beam | |
| 238 | glows by itself, so a lit beam lights the world whatever the mirrors are doing, and | |
| 239 | aiming them away would leave the floor at about 100 W/m² — half of noon. | |
| 240 | ||
567fe8ad 2026-09-08 Pavel Safronov | 241 | The eye-height slider runs from 1.7 m to 25 km with the local gravity beside it, and |
41516065 2026-09-08 Pavel Safronov | 242 | **Fly** lets go of the ground altogether — an autopilot on a permanent helix, 18° |
| 243 | off the axis, crossing the world lengthwise while it rolls around it and reflecting | |
| 244 | off the caps rather than stopping. **Your input turns your head, not the aircraft**, | |
| 245 | so you can watch whatever you like while it flies. The slider becomes the altitude | |
| 246 | it holds, terrain-following underneath: climbing at once, sinking slowly. Gravity | |
| 247 | falls linearly to zero at the axis, so height makes flight *easier* — 0.91 g at | |
| 248 | 4.3 km up, and the beam widens overhead as you close on it. | |
1fcef8e6 2026-09-08 Pavel Safronov | 249 | |
| 250 | Nothing *inside* rotates, because you, the ground and the air all turn together — so | |
| 251 | for 539 of the 540 km there is no evidence Tower turns at all. The exception is the | |
| 252 | window: the sky out there does not co-rotate, and wheels once every 7 min 29 s about | |
| 253 | the one motionless star on the axis. Rain used to stand in for that, leaning a | |
| 254 | permanent 1.5°, and is no longer drawn — the lean is real and still computed, but | |
| 255 | 1.5° was never a picture of anything. It is by a wide margin the plainest sign of the spin anywhere in the view, and | |
769726ea 2026-09-08 Pavel Safronov | 256 | it is only available at night, at the deepward end, looking the one direction that |
| 257 | has ever had anything in it but Tower. | |
28f9a6bc 2026-09-07 Pavel Safronov | 258 | |
cf4f8772 2026-09-07 Pavel Safronov | 259 | ## Working in the map |
| 260 | ||
| 261 | Pick a tool, click to place; **Lake**, **Forest**, **Desert** and **Region** trace a | |
| 262 | polygon and **River** and **Canyon** trace an open line (Enter finishes, Esc | |
| 263 | cancels). Drag markers to move them, **shift-click two points** to | |
| 264 | measure a sightline, **f** to fit the view. | |
| 265 | ||
cf37225c 2026-09-07 Pavel Safronov | 266 | Canon is **0 star slats** — the hull is sealed, the inner surface is one unbroken |
| 267 | 169,646 km², and the only views out are at the end caps. The runtime control for | |
| 268 | this is disabled and hidden (decisions.md D-033): the terrain raster is baked at 0 | |
| 269 | slats, so any other count draws bands over land that has none. | |
cf4f8772 2026-09-07 Pavel Safronov | 270 | |
| 271 | Everything autosaves to this browser's localStorage. Two exports: | |
| 272 | ||
| 273 | | | | | |
| 274 | |---|---| | |
| 275 | | **Export backup** | everything, private notes included. Keep out of the public repo — `.gitignore` already excludes `tower-backup-*.json`. | | |
62458672 2026-09-07 Pavel Safronov | 276 | | **Export static map content** | public places only, `notes` stripped. This is `static/world.json`, the world readers see, and the seed for a browser with no local draft. | |