1
0

Shadows land on the building, and the sky layers stop repeating themselves

**Shadows were the right size and pointed at nothing.** Last round fixed the
missing `updateProjectionMatrix()`, so the frustum finally became the size
every caller asks for — but nothing aimed it, and `sun.target` sits at the
world origin. A pack's origin is the **north-west corner of its slab**, so
for lumbridge-hq the box was off-centre by half the building: 14.4 m of a
48 m plate, about a third of the floor, fell outside the frustum and
neither cast nor received. Invisible while three's broken ±5 default made
shadows useless everywhere; obvious the moment they started working.

`SceneKitOptions` takes a `shadowTarget` now, both callers pass one, and
the light's target is added to the scene — which is the part that actually
matters, because `LightShadow.updateMatrices` reads `target.matrixWorld`
and an unparented `Object3D` is never reached by the traversal that
updates it. The sun is also placed relative to the target rather than the
origin, so light-to-target is exactly `sunDistance` for every direction,
which is the invariant each caller's `shadowNear`/`shadowFar` were chosen
against.

**`flights.ts` could not be tested, and that is why it was untested.** It
used a TypeScript parameter property — the one piece of TS syntax that
*emits code* rather than annotating a type — so Node's type stripping
refused the whole module. The bundler never cared, so nobody found out
until the first `node --test` file tried to import it. The module carrying
the worst bug this project has shipped was, by construction, the one
module that could not have a test. It has eleven now, including one that
fails if the live-aircraft repeat-skip is removed.

**Robots are on the plan panel** — a turned marker with a bow for heading,
in the one hue left that is neither the people-blue nor the camera-amber.

Review findings cleared across the four new sky/robot modules: a real
24 mm void at the ankle and an 8 mm hole through each forearm, a
per-frame allocation in the robot heading picker, a per-frame sort in the
starlink ranking, `uTime` growing unbounded until the cloud breath
quantises, and `DAY_REFERENCE`'s derivation which did not reproduce.

`createStarlinkMeshLayer` now takes a **board** radius — the same unit its
sibling takes — instead of a dome radius with nothing in the types to tell
them apart. That is the exact confusion that has already caused one real
bug here. `DOME_RADIUS_FACTOR` has one owner and is imported, not copied:
the points and the meshes must be on the same dome or a satellite that
grows geometry also jumps.

Several comments were wrong rather than merely stale — a fabricated claim
about `Object3D.clone`, a fabricated attribution to `Plan`, an inverted
`DoubleSide` argument, a triangle ledger citing a function that no longer
exists, and a defensive-call rationale that contradicted the paragraph
above it. In a codebase where the comments are the design record, those
are defects.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-07 02:50:25 -07:00
parent af0d4a7d57
commit 51979feea0
13 changed files with 1760 additions and 166 deletions
+17 -10
View File
@@ -34,11 +34,7 @@ import { createFlightLayer, type FlightLayer } from "./flights.ts";
import { createCloudLayer, type CloudLayer } from "./clouds.ts";
import { createMarkerLayer, type MarkerLayer } from "./markers.ts";
import { solarPosition, sunDirection } from "./solar.ts";
import {
createStarlinkMeshLayer,
DOME_RADIUS_FACTOR,
type StarlinkMeshLayer,
} from "./starlinkMesh.ts";
import { createStarlinkMeshLayer, type StarlinkMeshLayer } from "./starlinkMesh.ts";
import {
createSatelliteLayer,
type SatelliteCatalogue,
@@ -265,6 +261,16 @@ export async function createScene(
*/
maxDistance: boardSpan * 2.0,
shadowExtent: boardSpan * 0.75,
/**
* The middle of the board, which is **not** the origin.
*
* Scene space is centred on `city.center` — the city — and the Bay Area
* board runs forty kilometres down the peninsula from there, so a shadow
* box centred on the origin spends half itself on empty ocean and leaves the
* far end of the peninsula outside the frustum entirely. `shadowExtent`
* sizes the box and says nothing about where it is; this says where.
*/
shadowTarget: new THREE.Vector3((westX + eastX) / 2, 0, (northZ + southZ) / 2),
shadowFar: boardSpan * 2.2,
});
// Held, because the cloud layer needs the same opening rig the kit just got —
@@ -308,11 +314,12 @@ export async function createScene(
if (options.satellites) {
satelliteLayer = createSatelliteLayer(boardRadius);
scene.add(satelliteLayer.group);
// The same dome the points are on, so a satellite that grows geometry does
// not also jump. `DOME_RADIUS_FACTOR` is exported for exactly this: the two
// layers must agree, and the only safe way for them to agree is to be
// multiplying the same number by the same constant.
starlinkMeshes = createStarlinkMeshLayer(boardRadius * DOME_RADIUS_FACTOR);
// Board radius, the same unit `createSatelliteLayer` takes above — the
// mesh layer applies the dome factor itself. It used to take the *dome*
// radius while its sibling took the *board* radius, with nothing in the
// types to tell them apart, which is precisely the confusion that has
// already produced one real bug in this file.
starlinkMeshes = createStarlinkMeshLayer({ boardRadius });
scene.add(starlinkMeshes.group);
}