/**
 * A Tesla Optimus, procedurally, as a rig you can pose — plus the merged static
 * version for a pack that just wants one standing in a corner.
 *
 * This is the first asset in the library that is **not** a single merged mesh,
 * and the reason is the only reason that would justify it: something has to walk
 * it around the office (`interiors/robots.ts`), and a walk cycle needs limbs
 * that move independently. `parts.ts` says it plainly — anything that has to
 * move on its own belongs in its own object rather than in a bin — so the figure
 * is eleven small bins, one per articulating joint, each merged internally and
 * hung off the joint it belongs to. Eighteen meshes per robot rather than two.
 * That price is stated again, in draw calls, at the bottom of this comment and
 * in `robots.ts`, because it is the whole cost of the feature.
 *
 * ### The figure
 *
 * Gen 3 from reference knowledge, and the proportions are what carry it, because
 * ten metres is the distance this is normally seen from and at ten metres a
 * silhouette is all there is. What has to be right, in order of how much it
 * matters:
 *
 * 1. **1.73 m and slim.** Optimus is human-height and noticeably narrower than a
 *    human — a 0.35 m shoulder span on a 1.73 m frame. Build it at human width
 *    and it reads as a person in a costume.
 * 2. **The waist.** The single most identifying line: a wide pelvis, a wide
 *    chest, and a genuinely thin dark column between them. It is 0.15 m across
 *    where the pelvis is 0.28 and the chest is 0.34. Widen it and the whole
 *    thing turns into a mannequin.
 * 3. **The visor.** A smooth black panel filling the front of a small pale head,
 *    with no features on it at all. Two eyes, a mouth line, a "friendly" curve —
 *    any of them and it stops being Optimus. It has to *follow the head*: a flat
 *    plate on a rounded shell overhangs at its corners, and a face with the
 *    corners of its visor hanging in the air beside it is the single most
 *    conspicuous thing this figure can get wrong. See `emitHead`.
 * 4. **Pale shells over a dark frame.** Every limb is a light shell that stops
 *    short of the joint, with dark structure showing in the gap. That gap is
 *    what makes it read as a machine rather than as a white plastic doll, and it
 *    costs nothing but a few millimetres of geometry. The ankle, knee, hip,
 *    wrist and elbow all have one; the shoulder has a dark drum showing past the
 *    edge of its cap instead, for a draw-call reason written out at the drum.
 *    Whatever else changes, do not let two pale parts meet each other — and do
 *    not let them merely *touch*, either. Every join in this file overlaps by a
 *    few millimetres, because parts that share a face z-fight along it and parts
 *    that stop at the same plane by arithmetic eventually stop a millimetre
 *    short instead. Both of those have happened here.
 * 5. The knee actuator, the shoulder caps, the five-fingered hands. Detail, not
 *    silhouette. Present because they are cheap, not because they are load-
 *    bearing.
 *
 * ### Two materials, and why not three
 *
 * `paper` for the shells and `screenBezel` for the dark frame and the visor.
 * Both are borrowed — `materials.ts` has a closed role list with no robot in it,
 * and inventing a role is not on offer — so the borrow is chosen to survive a
 * self-hoster recolouring the palette. `paper` is the library's palest
 * untextured neutral, which is exactly what an Optimus shell is; `screenBezel`
 * is its near-black, and the visor genuinely *is* a bezel, so a self-hoster who
 * darkens their screen surrounds darkens the robot's face, which is the right
 * coupling rather than a coincidental one.
 *
 * `metalTrim` was tried as a third material for the joint barrels and dropped,
 * and then taken back up. The original reasoning was sound and is no longer
 * true, so it is worth recording both halves rather than quietly deleting one:
 * the office rig carried **no environment map**, so a `metalness: 0.85` role had
 * nothing to reflect and rendered as a dull dark grey — indistinguishable from
 * `screenBezel` at ten metres — while costing another mesh in nine of the eleven
 * groups.
 *
 * `engine/environmentRig.ts` now supplies one. A metal barrel therefore picks up
 * the room around it and reads as a machined surface rather than as a darker
 * patch of plastic, which is the whole difference between "a robot" and "a
 * figurine of a robot" at the distance somebody stands next to one.
 *
 * The cost was re-scoped rather than re-accepted, though. `metalTrim` is used
 * **only for the four joint barrels** — hip axle, shoulder, knee, elbow — so it
 * appears in four groups rather than nine, and the figure is three materials and
 * twenty-two meshes rather than three materials and twenty-seven. The sole, the
 * ankle and the visor stay `screenBezel`, because a sole is rubber, an ankle is a
 * gap and a visor is glass, and none of the three wants a specular ring on it.
 *
 * ### The indexed/non-indexed rule bites here harder than anywhere else
 *
 * `common.ts` warns that every part under one material must be all-indexed or
 * all-non-indexed or `mergeGeometries` silently drops the material. With
 * eighteen bins there are eighteen chances to get it wrong and the symptom is an
 * invisible shin, so the split is made structural rather than remembered:
 *
 * - **`shell` is drawn with `roundedBox()` and nothing else.** `ExtrudeGeometry`,
 *   never indexed. Every pale part of the robot is a rounded box, which is also
 *   what Optimus actually looks like — flat-sided shells with softened edges,
 *   not tubes.
 * - **`frame` is drawn with `box()` and `cylinder()` and nothing else.** Both
 *   indexed. This is why the visor is five flat boxes fitted round the head's
 *   corner rather than one moulded shell: it is drawn in the dark material, so
 *   it may not be an extrusion, so the curve has to be faceted by hand.
 *
 * Add a part and put it in the material whose primitive class it already
 * belongs to. If you cannot, you want the *other* material, and it will usually
 * turn out to look better there anyway.
 *
 * ### The joint frame
 *
 * The figure faces **−Z at yaw 0**, same as every other asset (`common.ts`), and
 * the origin is on the floor between the feet. Consequences worth writing down,
 * because getting a sign wrong here produces a robot that walks backwards
 * through its own knees:
 *
 * - **+X is the robot's right, −X its left.** Stand behind it looking the way it
 *   faces — which is looking down −Z, the three.js default view direction — and
 *   its right hand is on your right, which is +X. So `hipR` is at `x > 0`.
 * - **Every joint group's local axes are the root's axes.** No rotation is baked
 *   into a joint; a rig with every rotation at zero is a figure standing to
 *   attention. `OPTIMUS_REST` is applied on top of that, by `buildOptimus`, and
 *   is what "standing still" means to the walk cycle.
 * - **`rotation.x` is the sagittal hinge and positive swings the limb forward.**
 *   A limb hangs along −Y; `Rx(θ)` sends (0,−1,0) to (0,−cos θ,−sin θ), and −Z
 *   is the front. So `hipL.rotation.x = +0.4` is a leg reaching forward and
 *   `shoulderR.rotation.x = +0.4` is a hand swung forward.
 * - **A knee bends the other way, so `knee.rotation.x` must stay ≤ 0.** The shin
 *   folds backwards, toward +Z. Positive values hyperextend it, and because
 *   nothing clamps them, a sign slip in an animation shows up as a robot with
 *   its knees on backwards rather than as an error. This is the asymmetry to
 *   remember: hip, shoulder and elbow all bend positive; the knee is the one
 *   that bends negative.
 * - **An elbow bends positive.** The forearm folds forward, toward the chest,
 *   which is −Z. It shares its sign with the shoulder and not with the knee.
 * - **`rotation.y` is the transverse twist and positive turns left**
 *   (counter-clockwise from above — the house `Yaw` sense, unconverted). Used on
 *   `pelvis` and `torso` for the counter-rotation of a walk.
 * - **`rotation.z` is the frontal lean and positive leans left.** `Rz(θ)` sends
 *   up (0,1,0) toward −X. Used on `torso` for sway and on `shoulder` for the
 *   outward splay that keeps the hands off the hips.
 * - **`pelvis.position.y` is the body's height above the floor** and starts at
 *   `OPTIMUS.hipY`. Everything else hangs off it, feet included, so lowering the
 *   pelvis lowers the whole robot — which is what a walk's vertical oscillation
 *   wants and is why the bob is applied there and nowhere else.
 *
 * ### What it costs
 *
 * Eighteen meshes and about 7,100 triangles per figure. The mesh count is the
 * one that matters and it is fixed by the eleven bins and the two materials;
 * the triangles are almost all `roundedBox`, which is 236 of them whatever size
 * it is drawn at and however tight its bevel. That flat rate is worth knowing
 * before adding shell detail: a 14 mm finger and a 340 mm chest cost the same,
 * which is why the fingers were a quarter of the figure until somebody counted
 * (see `DIGITS`). `buildOptimus` is
 * meant to be called **once**; `cloneOptimus` gives you another figure sharing
 * every geometry and both materials, which is how four robots cost four times
 * the draw calls but one times the memory. Seventy-two draw calls for a crowd of
 * four is real money against an office that draws in about thirty, and it is the
 * number to look at first if a floor starts dropping frames.
 *
 * The `tera:robot.optimus` asset is the other end of that trade: it flattens the
 * same rig into one mesh per material, so a pack that wants a robot standing
 * still gets two draw calls and no articulation. `furnishings.ts` instances per
 * mesh, so twenty static robots in a pack still cost those two.
 */

import * as THREE from "three";
import { mergeGeometries } from "three/examples/jsm/utils/BufferGeometryUtils.js";
import { defineAsset, type AssetContext } from "../kit.ts";
import type { SurfaceMaterial } from "../materials.ts";
import { MeshBin, type PartBin } from "../parts.ts";

/**
 * Every dimension the figure and the walk cycle both have to agree on, in
 * metres above the sole, in the rest pose.
 *
 * Exported because `robots.ts` needs `hipY` to bob the pelvis around and would
 * otherwise carry a second copy of a number that must not drift. Everything
 * else — shell widths, bevel radii, how far a finger sticks out — is local to
 * the emitters below and deliberately not part of the contract.
 */
export const OPTIMUS = {
  /** Sole to crown. Gen 3 is quoted at about 1.73 m. */
  height: 1.73,
  ankleY: 0.085,
  kneeY: 0.5,
  hipY: 0.92,
  /** Top of the pelvis shell; the waist column starts here and the torso pivots here. */
  waistY: 1.05,
  /** Bottom of the chest shell. The 0.10 m between this and `waistY` is the slim bit. */
  chestY: 1.15,
  elbowY: 1.12,
  wristY: 0.865,
  shoulderY: 1.4,
  /** Top of the chest shell, where the neck column starts. */
  neckY: 1.43,
  /** The head's pivot: the top of the neck, so a nod hinges where a neck does. */
  headY: 1.505,
  /** Half the distance between the two hip pivots. A narrow stance; Optimus has one. */
  hipHalf: 0.085,
  /** Half the distance between the two shoulder pivots. */
  shoulderHalf: 0.175,
} as const;

/**
 * What "standing still" is, in radians, applied by `buildOptimus` and used as
 * the base every animated angle is added to.
 *
 * These are not zero because a figure with every joint at zero stands with its
 * arms welded to its hips and its legs locked straight, which looks like a
 * mannequin rather than like a machine that is idling. A few degrees of elbow
 * and a few of outward arm splay is the whole difference.
 *
 * `shoulderZ` is a magnitude: it is applied as `+` on the right shoulder and `−`
 * on the left, because positive `rotation.z` swings a hanging arm toward +X and
 * +X is the robot's right — so the same sign splays one arm out and tucks the
 * other one in.
 *
 * `hipX` and `kneeX` cancel, and that is not a coincidence: nothing here moves
 * the ankle, so the foot's pitch is the *sum* of the two, and any sum but zero
 * stands the robot on its heels or its toes. The first draft had a 0.01 hip and
 * a −0.05 knee and buried six millimetres of toe in the carpet. Change one of
 * these and change the other. The residue is that a bent leg is fractionally
 * shorter than a straight one, so the soles float about a millimetre — which is
 * a millimetre, and cheaper than a third joint to correct it.
 */
export const OPTIMUS_REST = {
  hipX: 0.04,
  /** Negative, and exactly −`hipX`. A knee only bends one way; see above. */
  kneeX: -0.04,
  shoulderX: 0.03,
  shoulderZ: 0.075,
  elbowX: 0.16,
} as const;

/**
 * The joints a walk cycle drives, resolved once so an animation never has to
 * search the scene graph per frame.
 *
 * `pelvis` carries the legs and the body's height; `torso` is a child of
 * `pelvis` and carries everything above the waist, which is what lets the
 * shoulders counter-rotate against the hips without the legs coming with them.
 */
export interface OptimusJoints {
  /** Body height and hip twist. Pivots at `OPTIMUS.hipY`; the legs hang off it. */
  pelvis: THREE.Group;
  /** Sway, lean and shoulder counter-twist. Pivots at the waist. */
  torso: THREE.Group;
  head: THREE.Group;
  hipL: THREE.Group;
  hipR: THREE.Group;
  kneeL: THREE.Group;
  kneeR: THREE.Group;
  shoulderL: THREE.Group;
  shoulderR: THREE.Group;
  elbowL: THREE.Group;
  elbowR: THREE.Group;
}

export interface OptimusRig {
  /** Origin on the floor between the feet, facing −Z at `rotation.y === 0`. */
  root: THREE.Group;
  joints: OptimusJoints;
}

/** The two materials the whole figure is drawn in. See the header for the borrow. */
interface Skin {
  shell: SurfaceMaterial;
  frame: SurfaceMaterial;
  /**
   * The joint barrels, and nothing else. Kept separate from `frame` so that
   * widening its use is a deliberate act with a visible cost in the mesh count
   * rather than a one-character change — see the header.
   */
  metal: SurfaceMaterial;
}

/** A point in whichever joint frame the emitter is drawing into. */
interface Anchor {
  x: number;
  y: number;
  z: number;
}

const JOINT_NAMES = [
  "pelvis",
  "torso",
  "head",
  "hipL",
  "hipR",
  "kneeL",
  "kneeR",
  "shoulderL",
  "shoulderR",
  "elbowL",
  "elbowR",
] as const;

// ---- Emitters -------------------------------------------------------------
//
// Each of these draws one body part into a bin, in that bin's own frame, with
// `a` naming the joint centre the part hangs from. They are written this way so
// that the rig and the flattened asset are the *same* geometry rather than two
// bodies that will drift apart the first time somebody widens a shin.

/**
 * A dark joint barrel lying across the body, centred on `a`.
 *
 * A unit cylinder stands along +Y with its base at the placement point, so a
 * roll of +π/2 sends its length along −X — which means the placement has to sit
 * half a length to the *right* of where the barrel should end up. Getting that
 * offset wrong puts every joint on the robot half a barrel off-centre, which is
 * subtle enough to survive a first look and obvious enough to ruin the second.
 */
function barrel(
  bin: MeshBin,
  P: PartBin,
  material: SurfaceMaterial,
  a: Anchor,
  diameter: number,
  length: number,
): void {
  bin.add(P.cylinder(12), material, {
    x: a.x + length / 2,
    y: a.y,
    z: a.z,
    size: [diameter, length, diameter],
    roll: Math.PI / 2,
  });
}

/** The pelvis block, and the hip axle the legs turn on. Drawn in the pelvis frame. */
function emitPelvis(bin: MeshBin, P: PartBin, s: Skin): void {
  // Straddles the hip line: down to 0.855 m and up to the waist at 1.05 m. The
  // width is what the slim waist above it is measured against.
  bin.add(P.roundedBox(0.07), s.shell, { y: -0.065, size: [0.28, 0.195, 0.19] });
  // The axle has to be longer than the pelvis is wide or none of it is visible,
  // and for a long time none of it was: at `2·hipHalf + 0.05` it stopped at
  // ±0.11 inside a shell that reaches ±0.14, and the comment on this line
  // claimed it "runs right through and out both sides" on the strength of
  // nobody having measured it. `+ 0.14` puts it at ±0.155: 15 mm of dark disc
  // clear of the pelvis and 2.5 mm clear of the outer face of each thigh, which
  // is the gap rule 4 in the header is about and the one the hip did not have.
  // Shorten it again and the hip goes back to one unbroken pale mass from the
  // waist to the knee, which is what a mannequin looks like.
  barrel(bin, P, s.metal, { x: 0, y: 0, z: 0 }, 0.115, 2 * OPTIMUS.hipHalf + 0.14);
}

/**
 * Waist, chest, shoulder caps and neck. Drawn in the torso frame, whose origin
 * is the waist at `OPTIMUS.waistY`.
 *
 * The shoulder caps live here rather than on the arms on purpose, and it is not
 * only a draw-call saving: on the real machine the cap is bodywork bolted to the
 * torso and the arm swings inside it. Putting the cap on the arm makes the whole
 * shoulder rotate when an arm swings, which reads as a shrug.
 */
function emitTorso(bin: MeshBin, P: PartBin, s: Skin): void {
  const waist = OPTIMUS.chestY - OPTIMUS.waistY;
  const chest = OPTIMUS.neckY - OPTIMUS.chestY;

  // The waist. 0.15 m across between a 0.28 m pelvis and a 0.34 m chest, and it
  // is drawn in the dark material so the gap reads as structure rather than as
  // a robot that skipped lunch.
  bin.add(P.box(), s.frame, { y: -0.02, size: [0.15, waist + 0.04, 0.135] });

  // Chest: a shell, then a plate a few millimetres proud of it. The plate is
  // what catches the light and gives the chest an edge at ten metres; without it
  // the torso is one flat pale slab.
  bin.add(P.roundedBox(0.06), s.shell, {
    y: waist,
    size: [0.34, chest, 0.205],
  });
  bin.add(P.roundedBox(0.055), s.shell, {
    y: waist + 0.045,
    z: -0.098,
    size: [0.245, chest - 0.09, 0.022],
  });

  // The neck column, dark, running from the top of the chest into the head.
  bin.add(P.cylinder(10), s.frame, {
    y: OPTIMUS.neckY - OPTIMUS.waistY,
    size: [0.072, OPTIMUS.headY - OPTIMUS.neckY + 0.012, 0.072],
  });

  const shoulderY = OPTIMUS.shoulderY - OPTIMUS.waistY;
  for (const side of [-1, 1]) {
    // The cap. Rounded hard, because a square shoulder is the other thing that
    // makes a humanoid read as a costume.
    bin.add(P.roundedBox(0.09), s.shell, {
      x: side * OPTIMUS.shoulderHalf,
      y: shoulderY - 0.078,
      size: [0.13, 0.156, 0.15],
    });
    // The drum the arm turns in. 0.135 across and 0.17 long against a cap that
    // is 0.13 wide, 0.156 tall and 0.15 deep, and every one of those numbers is
    // chosen against the cap's: the drum is buried inside the cap in Y and Z
    // and clears it only in X, so what shows is 20 mm of dark disc past the
    // outboard face of the cap and, inboard, a nub of the same 20 mm in the
    // 37 mm between the top of the chest and the top of the drum. The nub is
    // wanted — it is the only thing between the neck and the shoulder — but it
    // is the reason the drum's diameter is not free: take it past 0.16 and it
    // breaks out under the cap as well, and the shoulder turns into a dark
    // wheel with a pale plate stuck on it.
    //
    // That disc is the *only* dark structure at the shoulder, and it is here
    // rather than on the arm for a reason worth stating, because the obvious
    // fix looks better and costs more than it is worth. Rule 4 in the header
    // wants a pale shell stopping short of the joint with dark showing in the
    // gap; the knee and the elbow both do it. Doing it at the shoulder means a
    // dark part in the *shoulder* bin, which today draws in `paper` alone — so
    // it would go from one mesh to two, twice, on every figure: 20 meshes a
    // robot and 80 draw calls for a crowd of four instead of 72. Eight draw
    // calls for a 24 mm band of dark under a cap that already reads as a
    // separate piece is not the trade. A fatter drum is free.
    barrel(bin, P, s.metal, { x: side * OPTIMUS.shoulderHalf, y: shoulderY, z: 0 }, 0.135, 0.17);
  }
}

/**
 * The head shell's front-right corner, as fractions of its half-width and
 * half-depth, walking round from the middle of the face toward the temple.
 *
 * These are **measured off the built shell**, by casting rays at it, not
 * derived. They have to be, because the surface they describe is the offset
 * outline of a quadratic-Bezier corner that `ExtrudeGeometry` then bevels, and
 * writing that arithmetic out here would be a worse lie than a measurement:
 * it would look authoritative and it would be a re-implementation of three.js
 * internals. The visor is fitted to *these* numbers, so `x: 1` really is the
 * widest point of the head and `z: 1` really is the flat front of the face.
 *
 * **They are only valid for `roundedBox(0.08)`.** The radius is what sets where
 * the flat front stops — at 0.08 it stops 53% of the way out, and the corner
 * from there to the temple is a four-segment polyline, of which the visor uses
 * the first two. Change the head's bevel and these are wrong, and the symptom
 * is a visor whose outer corners hang in the air beside the head rather than
 * an error. Re-measure it: cast a ray down +Z at the shell at a series of x,
 * at a y inside the shell's straight middle band, and read off where it hits.
 * (An earlier visor was three hand-placed boxes and its outer pieces stood
 * 19 mm proud of a 165 mm-wide head, which is what that failure looks like.)
 *
 * Scaling with the head rather than in metres so that the two survive being
 * resized together; they do **not** survive being reproportioned, because a
 * corner is not similar to itself under a non-uniform scale.
 */
const FACE_FLAT = { x: 0.5312, z: 1.0 } as const;
const FACE_MID = { x: 0.7146, z: 0.9738 } as const;
const FACE_TEMPLE = { x: 0.8766, z: 0.8766 } as const;

/** Head shell, metres. Named because the visor is fitted to all three. */
const HEAD_SIZE = { width: 0.165, height: 0.215, depth: 0.185 } as const;

/**
 * How far the visor's outer face stands off the shell it is fitted to, and how
 * thick the panel is.
 *
 * 1.5 mm is deliberately below anything that can resolve — at ten metres it is
 * a seventh of a pixel — and it is not there to be seen. It is there so the
 * sign can never go the other way. A visor flush with the shell z-fights it
 * along the whole seam; a visor a millimetre *inside* it disappears in patches
 * as the figure turns, which reads as a face flickering on and off and is by
 * some distance the ugliest thing this asset has ever done.
 */
const VISOR_PROUD = 0.0015;
const VISOR_THICK = 0.016;

/**
 * One panel of the visor, laid flat on the chord between two points of the
 * head's front corner and pushed out until its outer face clears the shell.
 *
 * `from` and `to` are `FACE_*` fractions on the robot's right; `side` mirrors
 * the whole thing for the left. Two things in here are easy to get wrong and
 * neither of them shows up as an error:
 *
 * - **The chord is the surface, not an approximation of it.** The shell's
 *   corner really is a polyline in this band — it is an extruded polygon, not
 *   a curve — so a panel laid on one of its edges is parallel to the shell for
 *   that edge's whole length and stands off it by exactly `VISOR_PROUD`. This
 *   is the entire reason the corner is stored as measured points instead of as
 *   a radius: fit an arc to it instead and the panel touches at two places and
 *   sinks in between, which is the flickering failure `VISOR_PROUD` describes.
 * - **The box is pushed *in* by half its thickness, not out.** A box's front is
 *   its −Z face, so its centre has to sit `VISOR_THICK / 2` behind where the
 *   front is wanted. Add instead of subtract and the panel floats 16 mm off the
 *   head — which is very close to what the version before this one did.
 *
 * `seam` lengthens the panel at its inner end only. The inner end runs under
 * the piece before it, which is what stops a hairline of shell showing through
 * the joint between two panels at a grazing angle; the outer end is left exact,
 * because past it the shell falls away and any overhang there is a ledge on the
 * one edge of the visor anybody can see.
 */
function emitVisorPanel(
  bin: MeshBin,
  P: PartBin,
  s: Skin,
  side: number,
  from: { x: number; z: number },
  to: { x: number; z: number },
  visorY: number,
  visorHeight: number,
  seam: number,
): void {
  const halfX = HEAD_SIZE.width / 2;
  const halfZ = HEAD_SIZE.depth / 2;
  const x0 = from.x * halfX;
  const z0 = -from.z * halfZ;
  const x1 = to.x * halfX;
  const z1 = -to.z * halfZ;

  const dx = x1 - x0;
  const dz = z1 - z0;
  const span = Math.hypot(dx, dz);
  // Outward normal of the chord: rotate its direction a quarter turn so it
  // points away from the head. On the robot's right that is front-and-right.
  const nx = dz / span;
  const nz = -dx / span;
  // Midpoint, dragged back along the chord by half the seam so the panel grows
  // inward only, then offset along the normal to put its front face proud.
  const offset = VISOR_PROUD - VISOR_THICK / 2;
  const cx = (x0 + x1) / 2 - (dx / span) * (seam / 2) + nx * offset;
  const cz = (z0 + z1) / 2 - (dz / span) * (seam / 2) + nz * offset;

  bin.add(P.box(), s.frame, {
    x: side * cx,
    y: visorY,
    z: cz,
    size: [span + seam, visorHeight, VISOR_THICK],
    // A box's front is −Z, which a yaw of θ sends to (−sin θ, −cos θ); solving
    // that for the normal is the whole of this line. Mirrored for the left,
    // because a mirrored normal is a negated yaw.
    yaw: side * Math.atan2(-nx, -nz),
  });
}

/**
 * The head: a small pale shell and a black visor. Drawn in the head frame, whose
 * origin is the top of the neck.
 *
 * The visor is five plain `box`es because the dark material is the indexed one
 * — see the primitive-class rule in the header, which is why the one part of
 * this figure most deserving of a smooth curve is a faceted approximation of
 * one. A flat plate across the middle of the face, then two panels a side
 * following the shell's corner out to the temple, each sitting `VISOR_PROUD`
 * off the surface it is laid on. Five boxes is 60 triangles; the head shell
 * beside them is 236.
 *
 * **The visor tracks the shell, it does not overhang it.** That is the property
 * to preserve if anything here is touched, and the reason the corner is stored
 * as measured points at all. The version before this one was three hand-placed
 * boxes with a hand-picked yaw: its flat middle plate stood 3.7 mm proud at the
 * corners where it left the flat of the face, and its two side pieces stood
 * **19 mm** proud of a head 165 mm wide, which is a face wearing goggles a size
 * too big. The comment beside them said 1.5 mm, and had said so since before
 * the shell's bevel was retuned from 0.12–0.22 to 0.055–0.09 and moved every
 * number it was describing. Measurements that are not re-taken are guesses with
 * a decimal point on them.
 *
 * At five to fifteen metres a robot's head is fifteen to forty-five pixels
 * tall. Nothing about the panel's *surface* survives that, and the only thing
 * that does is its outline against the pale shell. So the outline is the thing
 * that is built accurately and the rest is not built at all.
 *
 * It wraps as far as `FACE_TEMPLE` and stops, leaving about 10 mm of pale shell
 * either side in a front view. Wrapping further gets a black head; stopping at
 * the flat front gets a rectangle painted on a box.
 */
function emitHead(bin: MeshBin, P: PartBin, s: Skin): void {
  const base = OPTIMUS.height - OPTIMUS.headY - HEAD_SIZE.height;
  bin.add(P.roundedBox(0.08), s.shell, {
    y: base,
    size: [HEAD_SIZE.width, HEAD_SIZE.height, HEAD_SIZE.depth],
  });

  // Kept inside the shell's straight middle band — `roundedBox(0.08)` rounds
  // the top and bottom 17 mm of a 215 mm head away in Y, and `FACE_*` describes
  // the cross-section between those. Push the visor into either and it starts
  // sinking into a surface that is no longer where the measurements say.
  const visorY = base + 0.078;
  const visorHeight = 0.082;
  const seam = 0.005;

  // The flat middle of the face, stopping exactly where the shell stops being
  // flat so the first wrap panel meets it edge to edge rather than crossing it.
  bin.add(P.box(), s.frame, {
    y: visorY,
    z: -HEAD_SIZE.depth / 2 - VISOR_PROUD + VISOR_THICK / 2,
    size: [FACE_FLAT.x * HEAD_SIZE.width, visorHeight, VISOR_THICK],
  });

  for (const side of [-1, 1]) {
    emitVisorPanel(bin, P, s, side, FACE_FLAT, FACE_MID, visorY, visorHeight, seam);
    emitVisorPanel(bin, P, s, side, FACE_MID, FACE_TEMPLE, visorY, visorHeight, seam);
  }
}

/** A thigh. Drawn in the hip frame; the shell stops short at both ends. */
function emitThigh(bin: MeshBin, P: PartBin, s: Skin): void {
  const drop = OPTIMUS.hipY - OPTIMUS.kneeY;
  // Two stacked shells rather than one, for the taper. A parallel-sided thigh is
  // the difference between "slim humanoid" and "stilts".
  bin.add(P.roundedBox(0.07), s.shell, { y: -drop * 0.52, size: [0.135, drop * 0.46, 0.165] });
  bin.add(P.roundedBox(0.07), s.shell, { y: -drop + 0.045, size: [0.112, drop * 0.5, 0.14] });
}

/**
 * Shin, ankle and foot, plus the knee actuator. Drawn in the knee frame.
 *
 * The actuator is the one piece of detail on the legs that is worth its
 * geometry: a dark barrel across the front of the knee is the single most
 * recognisable thing about an Optimus leg, and it is one cylinder.
 *
 * ### The ankle is built from the foot upward, and that is not a style choice
 *
 * Every part below the knee used to be placed off `drop`, and the ankle block
 * came out at 0.090–0.120 m above the floor while the foot shell ended at
 * 0.066 — a 24 mm band of nothing, with the whole leg's weight visibly resting
 * on air, and `OPTIMUS.ankleY` at 0.085 sitting in the middle of the void it is
 * supposed to name. `robots.ts` measures the entire gait pendulum to that
 * landmark, so the one height on this figure that had to have geometry at it
 * was the one that had none.
 *
 * So the ankle is anchored to the top of the foot, which is a surface, rather
 * than to a landmark, which is a number. It reaches 7 mm down into the foot
 * shell and 13 mm up into the shin shell — overlapping at both ends on purpose,
 * because two parts that merely touch share a face and z-fight along it, and a
 * flickering seam is worse than a joint that is a few millimetres thicker than
 * it needs to be. What is left visible is 39 mm of dark between a pale foot and
 * a pale shin, which is rule 4 in the header and is what an ankle is for.
 */
function emitShin(bin: MeshBin, P: PartBin, s: Skin): void {
  const drop = OPTIMUS.kneeY - OPTIMUS.ankleY;
  barrel(bin, P, s.metal, { x: 0, y: 0, z: -0.012 }, 0.118, 0.125);

  bin.add(P.roundedBox(0.065), s.shell, { y: -drop + 0.02, size: [0.1, drop - 0.05, 0.118] });

  // The foot. The sole is a separate dark slab so the robot has something to
  // stand on that is not the same colour as its shins — a monochrome foot
  // dissolves into a pale floor.
  const sole = -OPTIMUS.kneeY;
  const footTop = sole + 0.066;
  bin.add(P.box(), s.frame, { y: sole, z: -0.035, size: [0.098, 0.014, 0.25] });
  bin.add(P.roundedBox(0.06), s.shell, {
    y: sole + 0.014,
    z: -0.035,
    size: [0.106, 0.052, 0.243],
  });

  // The ankle, dark, filling everything between the two of them. Narrower than
  // both in X and Z, so the gap reads as a joint rather than as a change of
  // colour partway up a column — but only just. It is 12 mm narrower than the
  // shin and 18 mm narrower than the foot, and both of those were 26 and 32 at
  // one point, which left a 16 mm slot each side of the ankle that you could
  // see the floor through. Narrow enough to read as a waisted joint, wide
  // enough not to be a window: the two constraints point opposite ways and
  // this is where they meet.
  bin.add(P.box(), s.frame, { y: footTop - 0.007, size: [0.088, 0.059, 0.096] });
}

/**
 * An upper arm. Drawn in the shoulder frame; the cap and the joint drum are
 * both on the torso, for the reasons given at each of them.
 *
 * One shell, and it reaches 17 mm *into* the elbow barrel below it rather than
 * stopping level with it. At `-drop + 0.048` the shell ended at 1.168 m and the
 * barrel reached 1.167 — a one-millimetre ring of daylight all the way round
 * the elbow, which does not read as a gap at any distance but does catch the
 * background for a frame at a time as the arm swings, and chasing that kind of
 * flicker back to a millimetre of arithmetic costs an afternoon.
 */
function emitUpperArm(bin: MeshBin, P: PartBin, s: Skin): void {
  const drop = OPTIMUS.shoulderY - OPTIMUS.elbowY;
  bin.add(P.roundedBox(0.07), s.shell, { y: -drop + 0.03, size: [0.088, drop - 0.08, 0.098] });
}

/**
 * The digits of one hand, medial first. `medial` is the offset **toward the
 * body**, not an X coordinate, so this table is the same for both hands and the
 * one sign flip that makes a left hand a left hand happens where it is placed
 * — the same discipline the thumb has always had, extended to the fingers so
 * that the two cannot disagree about which way is inward.
 *
 * ### Three, not four, and why the count is the wrong question
 *
 * The previous version drew four identical 14 mm slabs in a row with 3 mm
 * between them, on the argument that what a hand needs at ten metres is five of
 * something, separate. Half of that is right and the expensive half is not. At
 * ten metres this figure is about 170 pixels tall, so a 14 mm finger is 1.4 of
 * them and a 3 mm gap is a third of one: **the separations were never once
 * resolved on screen.** What is resolved is the shape the tips make, and four
 * equal slabs make a straight line across the bottom of the hand — a comb, not
 * a hand. So the gaps are not what the geometry is for; the *stagger* is.
 *
 * Three digits of different lengths give the curved tip line a hand has, and
 * cost three of these rather than four. That matters more than it looks:
 * `roundedBox` is 236 triangles whatever size it is drawn at, so the four
 * fingers alone were 944 triangles a hand and the two hands together were a
 * quarter of the entire figure — a quarter of the budget spent below the
 * resolution limit. Dropping one digit a hand is 6% off the whole robot.
 *
 * `curl` is a rotation about +X applied at the *tip*, because a placement
 * rotates a part about its own base and these are placed tip-down. The base is
 * therefore solved for: it is the knuckle, less the rotated length. Getting
 * that backwards pivots each finger about its knuckle-end and fans the hand out
 * like a card trick.
 */
const DIGITS = [
  { medial: 0.022, width: 0.016, depth: 0.024, length: 0.068, curl: 0.26 },
  { medial: 0.002, width: 0.018, depth: 0.025, length: 0.076, curl: 0.3 },
  { medial: -0.021, width: 0.024, depth: 0.024, length: 0.064, curl: 0.34 },
];

/**
 * Forearm, wrist and hand. Drawn in the elbow frame.
 *
 * The digits are pale rather than dark, with only the wrist block in the frame
 * material — see `DIGITS` for what a hand is actually made of at this distance.
 *
 * This is the one emitter that has to know which side it is on, because a thumb
 * is the only part of the figure that is not left-right symmetric. It goes
 * medial — toward the body — which is where a relaxed arm puts it, and which
 * means `side` flips its sign. Drawing both thumbs at a fixed `+x`, as the first
 * version did, gives a robot with two right hands and a bounding box 27 mm wider
 * on one side than the other.
 *
 * **Nothing here butts up against anything.** The wrist block straddles
 * `OPTIMUS.wristY` and overlaps the forearm shell by 6 mm; the palm reaches
 * 6 mm up into the wrist block; the digits reach 6 mm up into the palm. All
 * three of those were exact abutments and two of them were exactly coplanar,
 * which is a z-fight rather than a join — and the third was not an abutment at
 * all but an 8 mm hole between the bottom of the forearm shell at 0.885 m and
 * the top of the wrist block at 0.877, straight through the arm. Overlap costs
 * nothing: these are merged into one buffer and the interior faces are never
 * seen from outside.
 */
function emitForearm(bin: MeshBin, P: PartBin, s: Skin, side: number): void {
  const drop = OPTIMUS.elbowY - OPTIMUS.wristY;
  barrel(bin, P, s.metal, { x: 0, y: 0, z: 0 }, 0.094, 0.088);
  bin.add(P.roundedBox(0.07), s.shell, { y: -drop + 0.02, size: [0.08, drop - 0.055, 0.088] });
  // Wider than the palm below it and narrower than the forearm above, in that
  // order. At 0.062 it was narrower than both, which put a 6 mm slot of
  // daylight down each side of the wrist between two parts that are meant to
  // be joined; a joint that reads as a taper cannot do that.
  bin.add(P.box(), s.frame, { y: -drop - 0.016, size: [0.072, 0.042, 0.07] });

  const palmTop = -drop - 0.01;
  bin.add(P.roundedBox(0.075), s.shell, { y: palmTop - 0.082, size: [0.068, 0.082, 0.032] });

  const knuckle = palmTop - 0.076;
  for (const digit of DIGITS) {
    bin.add(P.roundedBox(0.08), s.shell, {
      x: -side * digit.medial,
      y: knuckle - digit.length * Math.cos(digit.curl),
      z: -digit.length * Math.sin(digit.curl),
      size: [digit.width, digit.length, digit.depth],
      pitch: digit.curl,
    });
  }

  // The thumb, also placed tip-first, and the only part of the figure that
  // needs two rotations: it runs up and *outward* from a tip set 12 mm medial
  // of the palm's edge, and forward of the palm's face, so the pad ends up
  // opposing the fingers rather than lying alongside them. The old one was a
  // single roll about its own tip, which swung the knuckle 23 mm clear of the
  // hand and left the thumb attached to the palm at one corner.
  bin.add(P.roundedBox(0.08), s.shell, {
    x: -side * 0.046,
    y: palmTop - 0.078,
    z: -0.03,
    size: [0.019, 0.058, 0.026],
    pitch: 0.33,
    roll: -side * 0.28,
  });
}

// ---- The rig --------------------------------------------------------------

/** A named, empty joint at a position in its parent's frame. */
function joint(parent: THREE.Object3D, name: string, x: number, y: number): THREE.Group {
  const group = new THREE.Group();
  group.name = name;
  group.position.set(x, y, 0);
  parent.add(group);
  return group;
}

/** Merge a joint's parts and hang them off it, if it has any. */
function attach(target: THREE.Group, bin: MeshBin): void {
  if (bin.size === 0) return;
  target.add(bin.build(`${target.name}.mesh`));
}

/**
 * Build one posable Optimus.
 *
 * Call this **once** and `cloneOptimus` for every figure after the first: a
 * clone shares every geometry and both materials, so a crowd costs draw calls
 * and nothing else. Whoever built the original disposes it with
 * `disposeOptimus`, which frees the geometry the clones are all pointing at —
 * so dispose last, and dispose exactly once.
 *
 * The materials come from `ctx.materials` and belong to the registry. Nothing
 * here disposes them.
 */
export function buildOptimus(ctx: AssetContext): OptimusRig {
  const P = ctx.parts;
  const skin: Skin = {
    shell: ctx.materials.get("paper"),
    frame: ctx.materials.get("screenBezel"),
    metal: ctx.materials.get("metalTrim"),
  };

  const root = new THREE.Group();
  root.name = "optimus";

  // Pelvis, and the legs hanging off it. The legs are children of the pelvis so
  // that dropping the pelvis drops the whole robot — see the header note on
  // `pelvis.position.y`.
  const pelvis = joint(root, "pelvis", 0, OPTIMUS.hipY);
  const pelvisBin = new MeshBin();
  emitPelvis(pelvisBin, P, skin);
  attach(pelvis, pelvisBin);

  const torso = joint(pelvis, "torso", 0, OPTIMUS.waistY - OPTIMUS.hipY);
  const torsoBin = new MeshBin();
  emitTorso(torsoBin, P, skin);
  attach(torso, torsoBin);

  const head = joint(torso, "head", 0, OPTIMUS.headY - OPTIMUS.waistY);
  const headBin = new MeshBin();
  emitHead(headBin, P, skin);
  attach(head, headBin);

  // `side` is −1 for the robot's left and +1 for its right, which is the sign of
  // X: see the joint-frame note. The two limbs are mirror images in position
  // only — the shells themselves are symmetric, so there is no mirrored
  // geometry and no wound-backwards triangles to worry about.
  for (const side of [-1, 1]) {
    const suffix = side < 0 ? "L" : "R";

    const hip = joint(pelvis, `hip${suffix}`, side * OPTIMUS.hipHalf, 0);
    const hipBin = new MeshBin();
    emitThigh(hipBin, P, skin);
    attach(hip, hipBin);

    const knee = joint(hip, `knee${suffix}`, 0, OPTIMUS.kneeY - OPTIMUS.hipY);
    const kneeBin = new MeshBin();
    emitShin(kneeBin, P, skin);
    attach(knee, kneeBin);

    const shoulder = joint(
      torso,
      `shoulder${suffix}`,
      side * OPTIMUS.shoulderHalf,
      OPTIMUS.shoulderY - OPTIMUS.waistY,
    );
    const shoulderBin = new MeshBin();
    emitUpperArm(shoulderBin, P, skin);
    attach(shoulder, shoulderBin);

    const elbow = joint(shoulder, `elbow${suffix}`, 0, OPTIMUS.elbowY - OPTIMUS.shoulderY);
    const elbowBin = new MeshBin();
    emitForearm(elbowBin, P, skin, side);
    attach(elbow, elbowBin);
  }

  const rig: OptimusRig = { root, joints: optimusJoints(root) };
  restOptimus(rig.joints);
  return rig;
}

/**
 * Resolve the joints of a rig root by name.
 *
 * This used to be exported, on the stated grounds that `Object3D.clone(true)`
 * "hands back plain `Object3D`s" and so a caller holding a clone would need to
 * re-resolve it. **That is not true of three.js and never was.** `clone` is
 * `new this.constructor().copy(this, recursive)` (`three/src/core/Object3D.js`),
 * so cloning a `Group` gives a `Group`, and a cloned rig is structurally
 * identical to the original down to the class of every node. Nothing outside
 * this file ever called it — `robots.ts` takes its joints from the `OptimusRig`
 * that `cloneOptimus` hands back — so the export was justified by a wrong claim
 * and used by nobody, and it is now private.
 *
 * The function itself is still needed, for the reason the false one was
 * standing in front of: `clone` copies the *tree*, and `OptimusJoints` is a
 * flat record of references **into** the original tree. Those references are
 * not part of the tree and are not cloned with it, so a clone's joints have to
 * be found again, and the names are the only thing that survives the copy to
 * find them by.
 *
 * Throws rather than returning null: a root with no `kneeL` in it is not a rig,
 * and the caller has nothing useful to do about that at runtime.
 */
function optimusJoints(root: THREE.Object3D): OptimusJoints {
  const found = {} as Record<(typeof JOINT_NAMES)[number], THREE.Group>;
  for (const name of JOINT_NAMES) {
    const object = root.getObjectByName(name);
    if (!object) throw new Error(`optimus: rig has no joint named "${name}"`);
    found[name] = object as THREE.Group;
  }
  return found;
}

/**
 * Put every joint back to the idle stance. Called by `buildOptimus`, and by an
 * animation that wants a figure to stop moving without writing the eleven
 * assignments out itself.
 */
export function restOptimus(j: OptimusJoints): void {
  j.pelvis.position.y = OPTIMUS.hipY;
  j.pelvis.rotation.set(0, 0, 0);
  j.torso.rotation.set(0, 0, 0);
  j.head.rotation.set(0, 0, 0);
  j.hipL.rotation.set(OPTIMUS_REST.hipX, 0, 0);
  j.hipR.rotation.set(OPTIMUS_REST.hipX, 0, 0);
  j.kneeL.rotation.set(OPTIMUS_REST.kneeX, 0, 0);
  j.kneeR.rotation.set(OPTIMUS_REST.kneeX, 0, 0);
  // The splay is mirrored: positive `rotation.z` swings a hanging arm toward
  // +X, so the right arm needs `+` and the left arm `−` to both move outward.
  j.shoulderL.rotation.set(OPTIMUS_REST.shoulderX, 0, -OPTIMUS_REST.shoulderZ);
  j.shoulderR.rotation.set(OPTIMUS_REST.shoulderX, 0, OPTIMUS_REST.shoulderZ);
  j.elbowL.rotation.set(OPTIMUS_REST.elbowX, 0, 0);
  j.elbowR.rotation.set(OPTIMUS_REST.elbowX, 0, 0);
}

/**
 * Another figure sharing the first one's geometry and materials.
 *
 * `Object3D.clone(true)` copies the hierarchy, the names and the transforms and
 * *references* geometry and material, which is exactly the sharing wanted here —
 * so do not dispose a clone. Dispose the original, once, with `disposeOptimus`.
 */
export function cloneOptimus(rig: OptimusRig): OptimusRig {
  const root = rig.root.clone(true);
  return { root, joints: optimusJoints(root) };
}

/**
 * Free a rig's geometry.
 *
 * Materials are the registry's and are left alone, exactly as every asset in
 * this directory leaves them alone. Call this on the rig `buildOptimus`
 * returned, never on a clone, and only once everything cloned from it is out of
 * the scene.
 */
export function disposeOptimus(rig: OptimusRig): void {
  rig.root.traverse((child) => {
    const mesh = child as THREE.Mesh;
    if (mesh.isMesh) mesh.geometry.dispose();
  });
}

// ---- The static asset -----------------------------------------------------

/**
 * Collapse a posed rig into one mesh per material.
 *
 * The rig's own geometries are baked at their world transforms and disposed, so
 * what comes back owns everything it points at and can go through `MeshBin`'s
 * usual life cycle. This is safe only because of the primitive-class rule in the
 * header: `paper` is all extrusions and `screenBezel` is all indexed
 * primitives, so neither merge can hit the mixed-index refusal that would drop a
 * material and leave a robot with no shells on it.
 */
function flatten(rig: OptimusRig, name: string): THREE.Group {
  rig.root.updateMatrixWorld(true);

  const byMaterial = new Map<THREE.Material, THREE.BufferGeometry[]>();
  rig.root.traverse((child) => {
    const mesh = child as THREE.Mesh;
    if (!mesh.isMesh || Array.isArray(mesh.material)) return;
    const baked = mesh.geometry.clone().applyMatrix4(mesh.matrixWorld);
    const list = byMaterial.get(mesh.material);
    if (list) list.push(baked);
    else byMaterial.set(mesh.material, [baked]);
  });
  disposeOptimus(rig);

  const group = new THREE.Group();
  group.name = name;
  for (const [material, list] of byMaterial) {
    const merged = list.length === 1 ? list[0] : mergeGeometries(list, false);
    if (list.length > 1) for (const geometry of list) geometry.dispose();
    if (!merged) continue;
    const mesh = new THREE.Mesh(merged, material);
    mesh.name = `${name}:${material.name || "material"}`;
    mesh.castShadow = true;
    mesh.receiveShadow = true;
    group.add(mesh);
  }
  return group;
}

type OptimusParams = {
  /** Sole to crown, metres. The whole figure scales; 1.73 is the real one. */
  height: number;
  /**
   * The stance to bake in. `"rest"` stands to attention, `"stride"` freezes it
   * mid-step so a pack can put one in a corridor without it looking parked.
   */
  pose: "rest" | "stride";
};

/**
 * One Optimus, standing still, merged.
 *
 * Two draw calls, no articulation, and `furnishings.ts` instances it per mesh —
 * so a pack with twenty of these still pays those two. Anything that has to walk
 * wants `buildOptimus` instead, and pays eighteen.
 *
 * There is no `colorKey` and no tintable part. A robot that comes in a team
 * colour is a mascot; this is a machine, and the two things it can be are the
 * colour of its shells and the colour of its frame, both of which belong to the
 * palette rather than to one instance.
 */
export const robotOptimus = defineAsset<OptimusParams>({
  id: "tera:robot.optimus",
  label: "Optimus humanoid robot",
  defaults: { height: OPTIMUS.height, pose: "rest" },

  footprint(p) {
    const scale = p.height / OPTIMUS.height;
    // Measured off the built figure rather than guessed, and measured off the
    // *baked vertices* rather than off `Box3.setFromObject`, which inflates the
    // box of anything rotated and had this at 0.534 for a while: 0.520 m across
    // and 0.279 m from toe to heel, rounded up. The feet are the deepest part,
    // not the chest, which is what you would reach for. The width is a near tie
    // between the shoulder drums at 0.520 and the hands at 0.516 — the arms of
    // a standing humanoid are as wide as its shoulders, which is the other
    // thing you would get wrong — so widening either one moves this number.
    return { width: 0.55 * scale, depth: 0.3 * scale, height: p.height, clearance: 0.35 };
  },

  build(p, ctx) {
    const rig = buildOptimus(ctx);
    if (p.pose === "stride") {
      // A single frame of the walk cycle, written out rather than shared with
      // `robots.ts`: this is a fixed pose for a static prop, that is a function
      // of distance travelled, and coupling them would mean a pack's decorative
      // robot changing shape whenever somebody retunes a gait.
      rig.joints.hipL.rotation.x = 0.36;
      rig.joints.hipR.rotation.x = -0.3;
      rig.joints.kneeL.rotation.x = -0.12;
      rig.joints.kneeR.rotation.x = -0.42;
      rig.joints.shoulderL.rotation.x = -0.26;
      rig.joints.shoulderR.rotation.x = 0.28;
      rig.joints.elbowL.rotation.x = 0.2;
      rig.joints.elbowR.rotation.x = 0.34;
      rig.joints.torso.rotation.y = -0.07;
      rig.joints.pelvis.rotation.y = 0.04;
      // Dropped by the amount the straddle costs, and no further: the pose is
      // hand-written, so nothing checks that the feet reach the floor except
      // measuring the result. At −0.022 the leading toe was 2 mm under it.
      rig.joints.pelvis.position.y = OPTIMUS.hipY - 0.019;
    }

    const group = flatten(rig, "robot.optimus");
    if (p.height !== OPTIMUS.height) group.scale.setScalar(p.height / OPTIMUS.height);
    return group;
  },
});
