/**
 * The aircraft over the city, as an aircraft.
 *
 * `flights.ts` drew traffic as a dart from the day the layer existed until this
 * file replaced it — a five-sided capped cone with a box for a wing and a smaller
 * one for a tailplane, 34 triangles the shape of an arrowhead — and the dart was
 * the right first answer, because the only thing a speck over a city has to
 * communicate is *which way it is going*. It is the wrong last answer for one
 * reason: the sky is the part of this scene a person looks at on purpose.
 * Buildings do not move. A dozen darts crossing a coastline at three altitudes
 * are the only thing on the board with anything happening to it, and they are
 * worth more than an arrowhead.
 *
 * (`dartGeometry` is gone from `flights.ts` rather than kept as an option — this
 * file is the only aircraft on the board now. Its numbers are quoted below from
 * the commit that removed it, because a ledger measured against nothing is not a
 * ledger.)
 *
 * So: a swept-wing airliner, seen from where it is actually seen from.
 *
 * ### The one view that matters
 *
 * The camera on this board sits somewhere between a close chapter and two board
 * spans out, and it is nearly always *above* the traffic — an aircraft at cruise
 * is ~230 scene units up over a downtown whose tallest tower is 10, so even a low
 * camera looks up at a shallow angle and a high one looks straight down. The
 * silhouette from above is therefore the whole design, and the parts that carry
 * it are, in order: **wing sweep, wing taper, the two engines, the tailplane.**
 * A fuselage from above is a stripe. A fin from above is nothing at all.
 *
 * That ordering is why the budget goes where it does. The aerofoils — wings, fin,
 * tailplanes, pylons — are flat sheets costing four triangles each, because
 * thickness on a wing 0.44 units across is invisible at every distance this is
 * ever seen from, and the planform is not. The engines get 24 triangles apiece,
 * which is a quarter of the whole aircraft for two pods 0.03 units wide, because
 * from above the engines are half of what makes a jet look like a jet.
 *
 * ### Scale: this is an icon, not a model
 *
 * `world.metresPerUnit` is ~94 m on the San Francisco board and ~391 m on the
 * SoCal one, and the dart's 0.42 units are the same 0.42 units on both. Over San
 * Francisco that is a 40 m aeroplane, which is an A320 to the metre and a happy
 * accident; over SoCal it is a 164 m aeroplane, four times life size. Nobody has
 * ever noticed, and nobody should: a truthfully-scaled airliner on the SoCal
 * board would be a tenth of a unit long against a board a thousand units across,
 * i.e. gone. Traffic here is a **map symbol drawn in 3-D**, sized for legibility
 * and oriented for truth, and the size is deliberately not a measurement.
 *
 * The consequence for this file is that the dart's bounding box is the spec.
 * This geometry is 0.42 long, 0.44 across and 0.145 tall against the dart's
 * 0.42 × 0.44 × 0.18 — the same footprint, slightly shorter, because a dart's
 * "height" was a fat cone and an airliner's is a fin. Nothing about the city's
 * apparent scale changes. If a future city pack wants them smaller, scale the
 * returned geometry once at the call site (`geo.scale(s, s, s)`); do not rescale
 * per mesh, which would give every aircraft its own matrix for no reason.
 *
 * Related, and load-bearing in the same way: the fuselage is about 30% fatter
 * than a real one relative to its length (radius 0.030 where proportion says
 * 0.023), and the nacelles more so. Truthful proportions were tried and read as
 * a thread with two dots on it the moment the camera pulled back past a board
 * span. The exaggeration is the same one a model kit makes and for the same
 * reason.
 *
 * ### Facing, and the bug this file must not reintroduce
 *
 * **Everything here is built nose along +Z.** `flights.ts` sets
 * `mesh.rotation.y = Math.PI - heading`, which for a heading of 0 (north) is a
 * half turn, and a half turn sends +Z to −Z, which is north in this scene. That
 * mapping only works for a nose modelled along +Z, and its comment records the
 * cost of getting it wrong: a bare negation of the heading once flew every
 * aircraft tail-first and put departures out over the Pacific. Build the nose at
 * −Z and every aeroplane on the board flies backwards, at 450 knots, forever,
 * and it looks *almost* right — which is why it survived a review the first time.
 *
 * The origin is the point `flights.ts` positions and rotates about, so it sits
 * mid-fuselage rather than at the nose: the layer also applies a pitch about X
 * (`rotation.order = "YXZ"`), and an aircraft pitching about its nose swings its
 * tail through a quarter of its own length.
 *
 * ### The triangle ledger
 *
 * 400 of these can be on screen at once when the godmode traffic dial is up, each
 * one a separate `THREE.Mesh` and therefore a separate draw call over a *shared*
 * geometry. The draw calls are `flights.ts`'s problem; the triangles are this
 * file's, and they come to **100**:
 *
 *     nose cone           6      6 sides, open-ended
 *     fuselage tube      12      6 sides, open-ended
 *     tail cone           6      6 sides, open-ended, sheared up
 *     wings           2 × 4      flat sheets, two faces each
 *     fin                 4
 *     tailplanes      2 × 4
 *     engine pylons   2 × 4
 *     nacelles       2 × 24      6 sides, both ends capped
 *
 * That is **2.9 times the dart**, not the order of magnitude an eyeballed
 * comparison of the two source files suggests. The dart's 34 are easy to
 * undercount because two thirds of them are in parts nobody thinks of as
 * geometry: `ConeGeometry(0.09, 0.42, 5)` is 5 side triangles *and* a 5-triangle
 * cap, and each of its two crossbars is a `BoxGeometry`, which is 12 triangles
 * whatever size it is drawn at — a 0.44 × 0.016 crossbar spends eight of its
 * twelve on four edge-on slivers nobody would think to count. 10 + 12 + 12 is
 * how a "five-sided cone with a crossbar" comes to 34.
 *
 * At 400 aircraft the 100 comes to 40k triangles — under one frame's worth of the
 * terrain mesh, and the vertex work is not what is expensive about 400 objects
 * anyway. The parts that were considered and cut for
 * costing more than they show: winglets (edge-on from the only angle that
 * matters), an engine fan face (a 0.03-unit disc), windows and a livery stripe
 * (they need vertex colours or a texture, and the material here is a shared
 * per-altitude-band Lambert with neither).
 */

import * as THREE from "three";
import { mergeGeometries } from "three/examples/jsm/utils/BufferGeometryUtils.js";

/**
 * Sides on every body of revolution: the fuselage and the two nacelles.
 *
 * Six, and it can stay six because `CylinderGeometry` and `ConeGeometry` give
 * their sides *radial* normals — adjacent faces share a normal at each seam — so
 * a Lambert hexagon shades as a smooth tube rather than as six flats. The
 * silhouette is a hexagon and nothing at this scale can tell.
 *
 * It also has to be the *same* six everywhere along the fuselage. The nose cone,
 * the tube and the tail cone are three open-ended surfaces butted rim to rim
 * with no caps between them, which is only watertight because all three rims are
 * hexagons of identical radius at identical angles. Change one of them to eight
 * sides and the joint becomes a ring of gaps you can see the inside of the
 * aeroplane through — backfaces are culled, so it reads as a hole punched
 * straight out to the city below rather than as a modelling error.
 */
const SIDES = 6;

/**
 * Fuselage radius, and the station of every joint along it, in scene units.
 *
 * Nose tip at +0.21 and tail tip at −0.21: a 0.42-unit aeroplane centred on its
 * own origin, matching the dart it replaces.
 *
 *     +0.210  nose tip
 *     +0.152  nose base / tube front      ─┐ one radius, three surfaces,
 *     −0.055  tube rear / tail cone base  ─┘ no caps (see SIDES)
 *     −0.210  tail tip
 */
const RADIUS = 0.03;
const NOSE_TIP_Z = 0.21;
const NOSE_BASE_Z = 0.152;
const TAIL_JOINT_Z = -0.055;
const TAIL_TIP_Z = -0.21;

/**
 * Nose to tail, in scene units, at `scale = 1`.
 *
 * Exported because it is the unit `flights.ts` measures its legibility floor
 * in: that layer asks "how many of these does the glyph have to be for a person
 * to see it from here", and the answer has to be phrased in the length this
 * file actually built rather than in a 0.42 copied into another module and left
 * behind the day somebody shortens the tail cone.
 *
 * It is also the closest thing this aircraft has to a radius. Every extremity —
 * the wingtips at 0.24 from the origin, the fin tip at 0.22, the nose at 0.21 —
 * is inside a sphere of this radius with room to spare, which is what makes it
 * the right number for a pick volume as well as for a size.
 */
export const AIRLINER_LENGTH = NOSE_TIP_Z - TAIL_TIP_Z;

/**
 * How steeply the tail cone sweeps up, as a gradient (rise per unit of z).
 *
 * Every airliner's tail cone kicks upward — it is where the aeroplane rotates
 * about on take-off and it has to clear the runway — and it is one of the few
 * cues in the side view that says "airliner" rather than "aeroplane". 0.09 lifts
 * the tail tip by 0.014, about half a fuselage radius, which is roughly a real
 * one at 5°.
 *
 * Applied as a **shear** rather than by rotating the cone, and the difference
 * matters. A rotated cone pivots its whole base rim out of the z-plane it shares
 * with the tube's rear rim, opening a sliver of gap round most of the joint — the
 * same see-through hole described under `SIDES`, just subtler. A shear moves
 * points in y as a function of z only, so every cross-section stays exactly where
 * it was and the joint (where the shear evaluates to zero) stays exactly closed.
 *
 * Shearing does invalidate normals, which is the usual reason not to do this by
 * hand. `BufferGeometry.applyMatrix4` transforms them by the inverse-transpose
 * and renormalises, so the shaded surface comes out right; a manual pass over the
 * position attribute would not, and the tail would light as though it were still
 * straight.
 */
const TAIL_UPSWEEP = 0.09;

/**
 * A flat aerofoil: four corners, and no thickness at all.
 *
 * Every wing, fin, tailplane and pylon on this aircraft is one of these. A wing
 * with real thickness costs 12 triangles instead of 4 to show a 0.006-unit edge
 * that is sub-pixel at every distance the traffic layer is ever viewed from, and
 * an airliner's planform — where the sweep starts, how hard it tapers, where the
 * engines hang on it — is the entire recognisable content of the shape from
 * above.
 *
 * **Both faces are emitted, wound opposite ways with opposed normals.** That is
 * not decoration and it is not `side: DoubleSide` in disguise:
 *
 *   - The material belongs to `flights.ts`, is shared with the fuselage, and is
 *     front-side by default. A single-sided wing is *invisible* from below —
 *     which is where the camera is for any aircraft between it and the sun.
 *   - Two coincident faces sound like z-fighting and are not. Backface culling
 *     picks exactly one of them for any viewpoint: from above the top face is
 *     front-facing and the bottom is culled, from below the reverse. They are
 *     never both rasterised, so there is nothing to fight.
 *   - It is not that `DoubleSide` would shade the belly wrongly. It would not:
 *     `normal_fragment_begin` multiplies the interpolated normal by
 *     `gl_FrontFacing ? 1.0 : -1.0` before the lighting runs, so a single sheet
 *     under a `DoubleSide` material gets a genuinely downward normal on its
 *     underside and comes out dark, the same as the second face here does. That
 *     argument used to be written the other way round in this comment and it was
 *     simply false; anyone testing it would have found the flag works and
 *     deleted 14 triangles for the wrong reason.
 *   - The real objection is *whose flag it is*. `side` lives on the material, the
 *     material belongs to `flights.ts`, and there is one of them per altitude
 *     band shared across the wings, the fuselage and the nacelles. Turning
 *     culling off to save seven sheets their second face also turns it off for
 *     three closed bodies of revolution, whose interiors are then rasterised on
 *     every aircraft on the board for nothing — and it makes a geometry that is
 *     only correct under one particular material, which is the sort of coupling
 *     that survives right up until somebody reuses this shape somewhere else.
 *     Seven extra pairs of triangles buy a mesh that is right under any material
 *     anybody points at it.
 *
 * The corners are given in order round the polygon and must be **planar** —
 * every quad in this file is, because each one's y varies linearly with x
 * (dihedral) or not at all. One face normal is computed from the diagonals and
 * shared by all four corners, so the sheet shades flat and exactly.
 *
 * Winding does not actually matter here (both faces exist either way, so getting
 * it backwards swaps which one is called "front" and changes nothing visible),
 * which is what makes mirroring a wing safe.
 *
 * UVs are emitted, and nothing samples them. They are here because
 * `mergeGeometries` refuses — `console.error`s the offending index and returns
 * `null` for the whole aircraft — if the geometries handed to it do not all
 * carry the *same set* of attributes. `ConeGeometry` and `CylinderGeometry`
 * bring position, normal and uv, so these must too.
 */
type Point = readonly [number, number, number];
type Quad = readonly [Point, Point, Point, Point];

function aerofoil(quad: Quad): THREE.BufferGeometry {
  const [a, b, c, d] = quad;
  // Diagonals rather than two edges: for a planar quad the cross product of
  // AC × BD is the face normal, and it does not care which corner is first.
  const px = c[0] - a[0];
  const py = c[1] - a[1];
  const pz = c[2] - a[2];
  const qx = d[0] - b[0];
  const qy = d[1] - b[1];
  const qz = d[2] - b[2];
  let nx = py * qz - pz * qy;
  let ny = pz * qx - px * qz;
  let nz = px * qy - py * qx;
  const length = Math.hypot(nx, ny, nz) || 1;
  nx /= length;
  ny /= length;
  nz /= length;

  const positions = new Float32Array(8 * 3);
  const normals = new Float32Array(8 * 3);
  const uvs = new Float32Array(8 * 2);

  for (const [i, corner] of quad.entries()) {
    // The same four positions twice: 0..3 carry +n, 4..7 carry −n.
    for (const half of [0, 4]) {
      const v = i + half;
      positions[v * 3] = corner[0];
      positions[v * 3 + 1] = corner[1];
      positions[v * 3 + 2] = corner[2];
      const sign = half === 0 ? 1 : -1;
      normals[v * 3] = nx * sign;
      normals[v * 3 + 1] = ny * sign;
      normals[v * 3 + 2] = nz * sign;
      uvs[v * 2] = i === 1 || i === 2 ? 1 : 0;
      uvs[v * 2 + 1] = i >= 2 ? 1 : 0;
    }
  }

  const geo = new THREE.BufferGeometry();
  geo.setAttribute("position", new THREE.BufferAttribute(positions, 3));
  geo.setAttribute("normal", new THREE.BufferAttribute(normals, 3));
  geo.setAttribute("uv", new THREE.BufferAttribute(uvs, 2));
  // Indexed, because `mergeGeometries` rejects a mixed batch — all of the inputs
  // must be indexed or none of them, and every three.js primitive is.
  geo.setIndex([0, 1, 2, 0, 2, 3, 4, 6, 5, 4, 7, 6]);
  return geo;
}

/** The same aerofoil on the other wing. Order reversed so the winding survives. */
function mirrored(quad: Quad): Quad {
  const flip = (p: Point): Point => [-p[0], p[1], p[2]];
  return [flip(quad[3]), flip(quad[2]), flip(quad[1]), flip(quad[0])];
}

/**
 * The starboard wing, corners running leading-root → leading-tip → trailing-tip
 * → trailing-root.
 *
 * Reading the numbers as an aeroplane: the root chord is 0.112 and the tip chord
 * 0.040, a taper ratio of 0.36; the leading edge goes back 0.105 over a semi-span
 * of 0.204, which is 27° of sweep; the tip sits 0.015 above the root, which is 5°
 * of dihedral. Those are an A320's numbers, near enough, and they are the numbers a
 * person recognises without being able to name any of them.
 *
 * The root is at x = 0.016, *inside* the fuselage rather than tangent to it. A
 * wing that starts exactly on the hull leaves a hairline of daylight at the root
 * the moment anything rounds off; a wing that starts inside it cannot, and the
 * buried part costs nothing because the fuselage is closed and opaque and the
 * depth buffer hides it. Same trick for the fin root, the tailplane roots, and
 * both ends of each pylon.
 *
 * y is negative at the root: this is a low-wing aeroplane, like nearly every
 * airliner, so from below the wing is clear of the fuselage stripe.
 */
const WING: Quad = [
  [0.016, -0.016, 0.05],
  [0.22, -0.001, -0.055],
  [0.22, -0.001, -0.095],
  [0.016, -0.016, -0.062],
];

/**
 * The fin, in the x = 0 plane. Root chord 0.095, tip chord 0.045, 34° of sweep,
 * and it tops out at y = 0.098 — 0.23 of the aircraft's length above the
 * centreline, which is where a 737's fin tip is.
 *
 * It is almost invisible from directly overhead, which is the view this whole
 * file is designed around, and it is here anyway: the fin is what the eye finds
 * at every *oblique* angle, and an airliner without one reads as a paper dart
 * again the moment the camera drops.
 *
 * The root chord runs nearly level while the sheared tail cone falls away
 * beneath it, so the fin emerges further out of the hull towards the back. That
 * is the right way round — it is what a real root fairing looks like — and it is
 * why the two root corners are at almost the same height rather than following
 * the cone.
 */
const FIN: Quad = [
  [0, 0.014, -0.105],
  [0, 0.013, -0.2],
  [0, 0.098, -0.205],
  [0, 0.098, -0.16],
];

/**
 * The starboard tailplane. Total span across both is 0.156, 0.37 of the
 * aircraft's length — tailplanes are proportionally enormous and this is the
 * cue, along with the engines, that separates an airliner from an arrowhead when
 * seen from straight above.
 *
 * Mounted on the tail cone rather than on top of the fin. A T-tail is the more
 * distinctive shape and the wrong one: it is a regional-jet and business-jet
 * signature, and the traffic over a large city is overwhelmingly the other kind.
 */
const TAILPLANE: Quad = [
  // The inboard edge is at x = −0.012, i.e. *through* the tail cone's centreline
  // rather than on its surface. A root at x = 0.01 sat outside a cone that has
  // narrowed to about 0.008 by this station, so the tailplane hung in free air
  // over most of its chord — invisible head-on and obvious the moment an
  // aircraft banked. The mirrored port half overlaps it, which is what buries
  // both roots and is the same trick the wing and fin roots already use.
  [-0.012, 0.01, -0.15],
  [0.078, 0.016, -0.175],
  [0.078, 0.016, -0.207],
  [-0.012, 0.01, -0.203],
];

/**
 * The starboard engine pylon: a flat strut in the plane of the nacelle axis,
 * buried in the wing at the top and in the nacelle at the bottom.
 *
 * Edge-on and therefore invisible from directly above, which is most of the
 * time. It is four triangles to stop the engine from visibly floating under the
 * wing at every other angle, and there is no cheaper way to do that.
 */
const PYLON: Quad = [
  [0.09, -0.008, 0.035],
  [0.09, -0.008, -0.03],
  [0.09, -0.028, -0.03],
  [0.09, -0.028, 0.035],
];

/**
 * Where the engines are, and how big.
 *
 * x = 0.09 is 36% of the semi-span, which is where a twin's engines actually
 * hang; the nacelle runs from z = +0.058 to −0.037, so it sits well *forward* of
 * the local leading edge (z = +0.012 at that station) and below it. Both of
 * those are what makes an engine read as an engine from above rather than as a
 * lump on the wing: the pod has to break the leading-edge line.
 *
 * Fat, like the fuselage and for the same reason — 0.033 across is about 13 m on
 * the SoCal board, which is a nonsense, and a proportionate 0.02 disappears at
 * two board spans, which is worse than a nonsense.
 *
 * These are the only closed bodies here: both ends are capped, spending 12 of
 * the 24 triangles on two hexagons nobody will look at straight on. That is
 * deliberate. An open-ended nacelle is a tube you can see through — the far wall
 * is backfacing and culled, so a pod ahead of a sunlit wing shows a bright hole
 * where the intake should be, and it flickers as the aircraft turns. Six
 * triangles is cheaper than that.
 */
const ENGINE_X = 0.09;
const ENGINE_Z = 0.0105; // centre of a nacelle running +0.058 → −0.037
const ENGINE_Y = -0.03;
const ENGINE_LENGTH = 0.095;
const ENGINE_INTAKE_RADIUS = 0.0165;
const ENGINE_EXHAUST_RADIUS = 0.0135;

/**
 * An airliner: swept wings, two underslung engines, a fin and tailplanes, merged
 * into a single `BufferGeometry` so that every aircraft on the board shares one
 * geometry and one upload.
 *
 * Nose along **+Z**. See the note at the top of this file before changing that.
 */
export function airlinerGeometry(): THREE.BufferGeometry {
  const parts: THREE.BufferGeometry[] = [];

  // ---- Fuselage -----------------------------------------------------------
  //
  // Three open-ended surfaces butted rim to rim: cone, tube, cone. `rotateX` by
  // a quarter turn takes three.js's +Y axis of revolution to +Z (a positive
  // quarter turn sends +Y to +Z, a negative one to −Z), which is how the nose
  // ends up forward and the tail cone ends up pointing aft.

  const nose = new THREE.ConeGeometry(RADIUS, NOSE_TIP_Z - NOSE_BASE_Z, SIDES, 1, true);
  nose.rotateX(Math.PI / 2);
  nose.translate(0, 0, (NOSE_TIP_Z + NOSE_BASE_Z) / 2);
  parts.push(nose);

  const tube = new THREE.CylinderGeometry(
    RADIUS,
    RADIUS,
    NOSE_BASE_Z - TAIL_JOINT_Z,
    SIDES,
    1,
    true,
  );
  tube.rotateX(Math.PI / 2);
  tube.translate(0, 0, (NOSE_BASE_Z + TAIL_JOINT_Z) / 2);
  parts.push(tube);

  const tail = new THREE.ConeGeometry(RADIUS, TAIL_JOINT_Z - TAIL_TIP_Z, SIDES, 1, true);
  tail.rotateX(-Math.PI / 2);
  tail.translate(0, 0, (TAIL_JOINT_Z + TAIL_TIP_Z) / 2);
  // y' = y + k·(jointZ − z): zero at the joint, rising all the way to the tip.
  // Row-major, which is what `Matrix4.set` takes — the transposed version of
  // this shears the aeroplane sideways and looks like a physics bug.
  tail.applyMatrix4(
    // prettier-ignore
    new THREE.Matrix4().set(
      1, 0, 0, 0,
      0, 1, -TAIL_UPSWEEP, TAIL_UPSWEEP * TAIL_JOINT_Z,
      0, 0, 1, 0,
      0, 0, 0, 1,
    ),
  );
  parts.push(tail);

  // ---- Aerofoils ----------------------------------------------------------

  for (const quad of [WING, FIN, TAILPLANE, PYLON]) {
    parts.push(aerofoil(quad));
    // The fin is the one surface with no opposite number; mirroring it would put
    // two coincident fins in the same plane, which is the one case where the
    // coincident-faces argument in `aerofoil` does not save us.
    if (quad !== FIN) parts.push(aerofoil(mirrored(quad)));
  }

  // ---- Engines ------------------------------------------------------------

  for (const side of [1, -1]) {
    const nacelle = new THREE.CylinderGeometry(
      ENGINE_INTAKE_RADIUS,
      ENGINE_EXHAUST_RADIUS,
      ENGINE_LENGTH,
      SIDES,
      1,
      false,
    );
    // Same quarter turn as the nose, so the wider end (three.js's "top") ends up
    // forward and the pod tapers towards the exhaust rather than away from it.
    nacelle.rotateX(Math.PI / 2);
    nacelle.translate(side * ENGINE_X, ENGINE_Y, ENGINE_Z);
    parts.push(nacelle);
  }

  const merged = mergeGeometries(parts);
  for (const part of parts) part.dispose();
  if (merged) {
    merged.name = "airliner";
    return merged;
  }

  /**
   * `mergeGeometries` returns null when its inputs disagree — a different set of
   * attributes, or some indexed and some not. Everything here is built to agree
   * (see `aerofoil`), so this is unreachable until somebody adds a part and
   * forgets a uv — and they will not be left guessing when they do. Three's
   * merge `console.error`s the index of the geometry it choked on and names the
   * attribute that is missing, which is most of a fix; what it does not do is
   * throw. The failure arrives as a `null` this function's own signature does not
   * allow it to pass on, so this branch is what makes that signature true, and
   * without it the alternative is not an exception but an empty sky over a
   * console nobody has open.
   *
   * So: a plain cone, which is a bad aeroplane and a much better failure. It
   * still points where the aircraft is going, which is the one thing this layer
   * exists to say.
   *
   * **0.09 rather than `RADIUS`.** The cone stands in for the whole aircraft and
   * not for its fuselage, so the fuselage radius is exactly the wrong number to
   * reach for: at `RADIUS` this is a 0.42-long, 0.06-wide needle where the shape
   * it replaces was 0.44 across the wings, and at a board span out a needle is
   * sub-pixel in every direction but one, i.e. gone. 0.09 is what the dart's own
   * fallback used and it is the width the *silhouette* needs — 0.18 across
   * against 0.42 long, the same arrowhead proportion the dart had. A degraded
   * aeroplane has to still be findable, or the degradation is indistinguishable
   * from the failure it is covering for.
   */
  const fallback = new THREE.ConeGeometry(0.09, NOSE_TIP_Z - TAIL_TIP_Z, SIDES);
  fallback.rotateX(Math.PI / 2);
  fallback.name = "airliner:fallback";
  return fallback;
}
