fuzzyworld

tower / commit

feat(exterior): open on the true hull, and remember the choice

aa2015009b4ebbfb0ce74452717642405d6a53cc · Pavel Safronov · 2026-09-14 13:09:21 -0700 · parents 6bb061a6144c

Claude Code usage: this commit ≈0 out / ≈0 cached · cost unknown (no per-model trailer) · session 1 tally 6.5K in / 453.3K out / 42.4M cached · usage by commit and session

feat(exterior): open on the true hull, and remember the choice

The Hull switch now defaults to True, which since D-060 looks in through
both windows at the hazed interior. The last reading chosen is kept in
localStorage under tower.exteriorHull and restored into the control at
startup, before the exterior is built, so it survives reloads as well as
switching views. An unreadable or unknown value falls back to True.

D-061.

Usage: 6.5K in / 453.3K out / 42.4M cached (session tally)
rtk:   3.3M saved, 38.2% over 3411 cmds (global/lifetime)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
 README.md             |  3 ++-
 decisions.md          | 15 +++++++++++++++
 design.md             |  3 ++-
 index.html            |  4 ++--
 src/ui/app.ts         | 11 +++++++++++
 src/views/exterior.ts |  3 ++-
 6 files changed, 34 insertions(+), 5 deletions(-)

diff --git a/README.md b/README.md
index 93f1f16..f1bf538 100644
--- a/README.md
+++ b/README.md
@@ -209,7 +209,8 @@ Any selection updates the URL, so a link is always shareable:
 kilometre of hull, a window at each cap, the interior's own star field, the 244 km collector
 flying ahead and the beam converging into the sun cap. Each piece is captioned.
 
-The hull reads two ways and the **Hull** switch picks one. **True** is the outer
+The hull reads two ways and the **Hull** switch picks one; it opens on True and
+remembers whichever you last chose. **True** is the outer
 skin: bare structure, and both caps real windows — look in through either and the
 land is actually there, hazed exactly as the interior view hazes it and receding
 540 km down the tube. It is the only line
diff --git a/decisions.md b/decisions.md
index 33f85c7..eaa1c4b 100644
--- a/decisions.md
+++ b/decisions.md
@@ -1936,3 +1936,18 @@ rolls a star, and only stars inside a one-cell shell around the eye count, which
 holds the same number of cells per steradian in every direction. The pinwheel is
 kept by turning the direction about z before the lookup, and the density matches
 what the old field had at its pole, so the far cap window looks as it did.
+
+## D-061 — The exterior opens on the true hull, and remembers the choice
+
+**Implemented at the author's request.** X-ray was the default because, before D-060,
+the true hull showed little: a bare cylinder, a checkered cap, and land only through
+the far window. Now both caps are windows onto the hazed interior, so the true hull is
+the better first look, and it is what Tower actually looks like.
+
+**The choice is remembered** in localStorage under `tower.exteriorHull`, alongside the
+active view (D-025) and the camera. It is restored into the Hull control at startup,
+before the exterior is built, because the exterior reads its reading from that control
+when it is first constructed. Switching to another view and back never rebuilds the
+exterior, so the reading survives that as well. An unreadable or unknown stored value
+falls back to true. The screenshot script selects a reading explicitly for each shot,
+in a fresh browser context, so it is unaffected.
diff --git a/design.md b/design.md
index e328e57..d4f6258 100644
--- a/design.md
+++ b/design.md
@@ -736,7 +736,8 @@ is the outer skin: bare structure, with a window at each cap.
 **X-ray** wraps the terrain raster onto the outside instead — from outside Tower is
 structure, so what that texture shows is what is *inside*, which makes it a diagram
 and not a view. Both are worth having and they are not the same claim, so the
-control is a two-way switch rather than a checkbox.
+control is a two-way switch rather than a checkbox. **True is the default**, and the
+last reading chosen is remembered across reloads and view switches (D-061).
 
 **The inside of Tower is the interior view's own model** (D-060), not a lookalike.
 The displaced ground, both glazed caps, the far cap's bore and the beam are built
diff --git a/index.html b/index.html
index f1367c4..023f758 100644
--- a/index.html
+++ b/index.html
@@ -45,8 +45,8 @@
       <div id="exterior-options" class="hidden">
         <label>Hull
           <select id="f-ext-view">
-            <option value="true">True — what Tower looks like</option>
-            <option value="xray" selected>X-ray — the inner surface projected out</option>
+            <option value="true" selected>True — what Tower looks like</option>
+            <option value="xray">X-ray — the inner surface projected out</option>
           </select></label>
         <p class="hint">True shows the outer skin: bare hull, and a window at each
           cap — look in through either and the land is really there, hazed as it is
diff --git a/src/ui/app.ts b/src/ui/app.ts
index cc33396..86f0a5a 100644
--- a/src/ui/app.ts
+++ b/src/ui/app.ts
@@ -22,6 +22,8 @@ import { eyeHeightFromSlider, sliderFromEyeHeight } from './scales.js'
 type Tool = 'select' | PlaceKind
 type ViewName = 'map' | 'exterior' | 'interior'
 const VIEW_KEY = 'tower.view'
+/** The exterior's hull reading, remembered across reloads like the view itself (D-061). */
+const HULL_KEY = 'tower.exteriorHull'
 const isView = (v: string | null): v is ViewName =>
   v === 'map' || v === 'exterior' || v === 'interior'
 
@@ -565,8 +567,17 @@ export class App {
       this.map.draw()
     }
     const ext = $<HTMLSelectElement>('f-ext-view')
+    // Restored into the control itself, which is what the exterior reads when it is
+    // first built — so a view opened on load starts in the remembered reading.
+    try {
+      const stored = localStorage.getItem(HULL_KEY)
+      if (stored === 'true' || stored === 'xray') ext.value = stored
+    } catch { /* unreadable storage just means the default, true */ }
     ext.onchange = () => {
       if (this.exterior) this.exterior.showTerrain = ext.value === 'xray'
+      try {
+        localStorage.setItem(HULL_KEY, ext.value)
+      } catch { /* not remembering the hull is survivable */ }
     }
   }
 
diff --git a/src/views/exterior.ts b/src/views/exterior.ts
index f63568f..2c0d288 100644
--- a/src/views/exterior.ts
+++ b/src/views/exterior.ts
@@ -119,7 +119,8 @@ export class ExteriorView {
   private worldToTower = new Matrix4()
   private eyeInTower = new Vector3()
   private terrainMap: Texture | null = null
-  private _showTerrain = true
+  /** True mode by default (D-061); the app sets the remembered reading on construction. */
+  private _showTerrain = false
   private raycaster = new Raycaster()
   private running = false
   private lastFrame = 0