---
name: suspended-highway
description: >
  Build an unfinished suspended-highway scene in one Three.js file: a
  Catmull-Rom road swept into a deck, and floating fragments placed by
  findSpot. Use when changing the road, the pieces, or the orbit on
  WebGPU. The reference file is highway.html beside this skill. If they
  disagree, the HTML wins.
---

# Suspended highway

One HTML file. No bundler, no npm, no glTF. Three.js 0.186.1 from a CDN via an import map (`three` and `three/webgpu` both point at `three.webgpu.js`). `WebGPURenderer` only. Set `renderer._getFallback = null`. If `navigator.gpu` is missing, or the backend is not WebGPU after `init()`, stop. No WebGL fallback.

Open it through a server. `file://` will not load the ES modules. `php -S localhost:8000` or `npx serve` in the folder is enough.

The scene is abandoned on purpose. It is a sketch of a dream city, not a driving game. Do not add a walker, a collision world, or a road the player can drive unless the user asks. New pieces float.

## Road

`roadCurve` is a `CatmullRomCurve3`. `buildFrames` samples position, tangent, right and up. `sweepTube(frames, offR, offU, halfW, halfH, material)` builds one box-section mesh. Asphalt, curbs, edge lines and rails are that function with different offsets.

Dashes are instanced or repeated boxes oriented with `orientAlong`. A sweep cannot leave a gap, so do not try to cut the centre line out of the tube.

Cars on the ramp use `frameAt` and face along the tangent. They stay parented to `highway`.

## Fragments

`findSpot` rejects points that overlap `occupied`. Every loose mesh goes through it, then `addFloating` (bob and spin). The bridge fragment is always placed, even if `findSpot` fails. Reload changes the layout because the seed starts from `Date.now()`.

`viewSize` never returns 0. A hidden iframe was handing WebGPU a 0x0 target. Keep the `Math.max(1, ...)` and the early-out in the animation loop.

## Orbit

Match `THREE.Spherical`. Drag right decreases theta.

```
x = target.x + dist * sin(phi) * sin(theta)
y = target.y + dist * cos(phi)
z = target.z + dist * sin(phi) * cos(theta)
```

Right-drag, middle-drag, shift-drag and two-finger drag pan. The wheel zooms. `autoSpin` runs until the first pointer down.
