<!-- SOLID CITY · part 3211 -->
raight
 * run from the Hall of Becoming to the Gap, over a 571 m spine) is
 * not forced: the Grand Coastal Line bends around the shore,
 * 4.4 km of lawful sky, highest land beneath it 331 m.
 *
 * By the hand of Solid, his good King. */

export function createSkyTrains(opts) {
  var T = opts.THREE, heightAt = opts.heightAt;
  var group = new T.Group(); group.name = 'sky-trains';

  /* ================= STATIONS =================
   * gy = the ground a traveller stands on. For the fourteen ports
   * it is the pad height the port survey swore to; for the
   * Spaceport, the apron; for the two ward stations, the discs
   * this very module lays (below). */
  var STATIONS = [
    { id: 'spaceport', name: 'THE SPACEPORT', x: -390, z: 300, gy: 22.4 },
    { id: 'southern', name: 'SOUTHERN BENCHES PORT', x: -490, z: -1165, gy: 9.16 },
    { id: 'western', name: 'WESTERN BENCHES PORT', x: -365, z: -1075, gy: 14.13 },
    { id: 'gate', name: 'GATE ROAD PORT', x: 650, z: -1265, gy: 34.8 },
    { id: 'braid', name: 'BRAID HEAD PORT', x: 890, z: -1345, gy: 146.09 },
    { id: 'eastern', name: 'EASTERN BENCHES PORT', x: 1045, z: -1635, gy: 124.58 },
    { id: 'high', name: 'HIGH BENCHES PORT', x: 1205, z: -1560, gy: 179.33 },
    { id: 'summit', name: 'SUMMIT PORT', x: 1299, z: -1591, gy: 198.95 },
    { id: 'shoulder', name: 'SHOULDER BENCHES PORT', x: 890, z: -1850, gy: 88.14 },
    { id: 'wheights', name: 'WESTERN HEIGHTS PORT', x: 780, z: -1865, gy: 84.32 },
    { id: 'north', name: 'NORTH BENCHES PORT', x: 665, z: -2060, gy: 73.0 },
    { id: 'farnorth', name: 'FAR NORTH PORT', x: 515, z: -2360, gy: 15.29 },
    { id: 'hall', name: 'HALL OF BECOMING PORT', x: 1055, z: -295, gy: 155.91 },
    { id: 'gap', name: 'THE GAP PORT', x: 2059, z: 1870, gy: 301.59 },
    { id: 'kauhale', name: 'KAUHALE MAUNA PORT', x: 2035, z: 1995, gy: 263.33 },
    { id: 'ward5', name: 'WARD FIVE STATION', x: -848, z: -910, gy: 9.89, ownPad: true },
    { id: 'ward7', name: 'WARD SEVEN STATION', x: -672, z: -498, gy: 22.11, ownPad: true }
  ];
  var ST = {}; STATIONS.forEach(function (s) { ST[s.id] = s; });

  /* ================= ROUTES =================
   * nodes: stations by id, or [x, z] for a flyover bend that is
   * flown but never stopped at (the Coastal bends). */
  var ROUTES = [
    { name: 'THE MOUNTAIN LINE', nodes: ['spaceport', 'southern', 'western', 'gate', 'braid', 'eastern', 'high', 'summit'] },
    { name: 'THE NORTH LINE', nodes: ['eastern', 'shoulder', 'wheights', 'north', 'farnorth'] },
    { name: 'THE GRAND COASTAL', nodes: ['hall', [0, 500], [200, 1500], [1100, 2100], 'gap', 'kauhale'] },
    { name: 'THE WARD LINE', nodes: ['ward7', 'ward5', 'southern'] }
  ];

  var MAT = {
    titanium: new T.MeshLambertMaterial({ color: 0x232c3d }),
    silver: new T.MeshLambertMaterial({ color: 0x9aa4b5 }),
    stone: new T.MeshLambertMaterial({ color: 0x55504c }),
    stoneDark: new T.MeshLambertMaterial({ color: 0x35302c }),
    glass: new T.MeshLambertMaterial({ color: 0x1a2436, transparent: true, opacity: 0.6 }),
    gold: new T.MeshBasicMaterial({ color: 0xd8a833 }),
    goldDim: new T.MeshBasicMaterial({ color: 0x8a6a1f }),
    white: new T.MeshBasicMaterial({ color: 0xf2f6ff }),
    red: new T.MeshBasicMaterial({ color: 0xff4a3d })
  };

  function box(w, h, d, mat, x, y, z) {
    var m = new T.Mesh(new T.BoxGeometry(w, h, d), mat);
    m.position.set(x || 0, y || 0, z || 0);
    return m;
  }

  /* ================= PROFILES =================
   * The track's top surface, sampled every ~8 m along the route
   * polyline: high enough for 25 m of sky under the beam at
   * cruise, pinned at every station to (gy - 0.55) so the train
   * floats its floor flush with the pad, grade-limited to 14%
   * so the climb is a climb and not a wall. */
  function buildProfile(route) {
    var pts = [];
    route.nodes.forEach(function (nd) {
      if (typeof nd === 'string') { var s = ST[nd]; pts.push({ x: s.x, z: s.z, station: s }); }
      else pts.push({ x: nd[0], z: nd[1], station: null });
    });
    var xs = [], zs = [], pin = [], isPin = [];
    for (var i = 0; i < pts.length - 1; i++) {
      var a = pts[i], b = pts[i + 1], L = Math.hypot(b.x - a.x, b.z - a.z), n = Math.max(2, Math.round(L / 8));
      for (var j = (i === 0 ? 0 : 1); j <= n; j++) {
        var t = j / n;
        xs.push(a.x + (b.x - a.x) * t); zs.push(a.z + (b.z - a.z) * t);
        var st = (j === 0 && a.station) ? a.station : (j === n && b.station) ? b.station : null;
        pin.push(st ? st.gy - 0.55 : null); isPin.push(!!st);
      }
    }
    var N = xs.length, y = new Array(N), terr = new Array(N);
    for (var i2 = 0; i2 < N; i2++) { terr[i2] = heightAt(xs[i2], zs[i2]); y[i2] = terr[i2] + 25; }
    for (var i3 = 0; i3 < N; i3++) if (pin[i3] !== null) y[i3] = pin[i3];
    for (var pass = 0; pass < 6; pass++) {
      for (var f = 1; f < N; f++) { var mx = y[f - 1] + 8 * 0.14; if (y[f] > mx) y[f] = mx; if (pin[f] !=