import type { CanonicalBone } from "@psx/shared";
import type { Rgb } from "../image/Raster.js";
import {
  add,
  computeFlatNormals,
  createMesh,
  lerp,
  loft,
  normalise,
  orientedBox,
  scale,
  sub,
  vertexCount,
  type LoftKey,
  type MeshData,
  type Vec3Tuple,
} from "../mesh/MeshBuilder.js";
import { jointWorld, type Skeleton } from "../rig/skeleton.js";
import type { BodyStyle } from "./bodyStyle.js";
import type { HeadStyle } from "./headStyle.js";
import { PART, tagLoft, tagRange, type BodyPart } from "./coverage.js";
import { bodyMetrics, torsoPlan, type BodyMetrics } from "./metrics.js";
import type { BodyType, CreatorPalette } from "./types.js";

/**
 * Base body mesh — skin only. Every clothed character derives from this.
 *
 * Shape comes entirely from {@link bodyMetrics}; this file decides only where
 * the rings go and how many. The stylisation is in the ring counts, not in the
 * proportions — a PSX figure that is honestly proportioned and coarsely
 * tessellated reads as an adult at 240p, where a lanky one reads as a
 * marionette.
 *
 * Note that `computeFlatNormals` is a misnomer: it averages face normals into
 * shared vertices, so every loft here is smooth-shaded and the faceting you see
 * is the silhouette, not the shading. The face relies on that — eyes and mouth
 * are painted, so the head is kept as clean unbroken surface for the texture and
 * only the nose, which changes the silhouette, stays as geometry.
 *
 * Structure, root to tip:
 *   torso     crotch → glutes → hip shelf → waist → ribs → chest → girdle
 *   neck      C7 → jaw, buried at both ends
 *   head      jaw → chin → cheek → brow → cranium → crown, plus nose and ears
 *   arms      deltoid socket buried in the girdle → upper → fore → palm → thumb
 *   legs      hip socket buried in the pelvis → thigh → knee → calf → ankle
 *   feet      heel → instep → ball → toe, lofted along the foot axis
 *
 * Every piece is tagged with its region and loft parameter (see
 * `coverage.ts`), so a garment can claim the skin it covers and the assembler
 * can delete it. The loft parameters quoted in the comments below are the same
 * numbers garments cite in their coverage bands — move a key here and the
 * matching band moves with it.
 */

/** Torso reads round at 8 sides; limbs stay cheap. */
const TORSO_SIDES = 8;
const ARM_SIDES = 6;
const LEG_SIDES = 6;
const HEAD_SIDES = 8;
const FOOT_SIDES = 5;

/** Stable ring frames: rx across the body, rz front-to-back. */
const ACROSS: Vec3Tuple = [1, 0, 0];

export interface HeadLandmarks {
  head: Vec3Tuple;
  neck: Vec3Tuple;
  crown: Vec3Tuple;
  brow: Vec3Tuple;
  browFwd: Vec3Tuple;
  occiput: Vec3Tuple;
  headW: number;
  /** Half-depth of the skull, front to back. */
  headD: number;
  height: number;
}

/**
 * Skull reference points for hair and headgear.
 *
 * Body style is ignored — a hat doesn't care how heavy its wearer is — but head
 * style is not: the sliders reshape the skull these anchors sit on, so hair
 * built off a neutral head would clip through a long or square one.
 */
export function headLandmarks(
  skeleton: Skeleton,
  body: BodyType,
  headStyle: HeadStyle | null = null,
): HeadLandmarks {
  const m = bodyMetrics(skeleton, body, null, headStyle);
  const head = jointWorld(skeleton, "Head");
  const neck = jointWorld(skeleton, "Neck");
  const headW = m.headHalfW;

  const crown: Vec3Tuple = [0, m.crownY, m.headZ + headW * 0.05];
  const brow: Vec3Tuple = [0, m.browY, m.headZ + m.headHalfD * 0.5];
  const browFwd: Vec3Tuple = [0, m.browY - headW * 0.04, m.headZ + m.headHalfD * 0.92];
  const occiput: Vec3Tuple = [
    0,
    m.browY - headW * 0.12,
    m.headZ - m.headHalfD * 0.78,
  ];

  return {
    head,
    neck,
    crown,
    brow,
    browFwd,
    occiput,
    headW,
    headD: m.headHalfD,
    height: skeleton.height,
  };
}

export function buildBaseBody(
  skeleton: Skeleton,
  body: BodyType,
  palette: CreatorPalette,
  bodyStyle: BodyStyle | null = null,
  headStyle: HeadStyle | null = null,
): MeshData {
  const mesh = createMesh();
  const m = bodyMetrics(skeleton, body, bodyStyle, headStyle);
  const skin = palette.skin;

  buildTorso(mesh, m, skin);
  buildChestForms(mesh, m, skin);
  buildNeck(mesh, m, skin);
  buildHead(mesh, m, skin);
  buildArms(mesh, m, skin);
  buildLegs(mesh, m, skin);

  computeFlatNormals(mesh);
  return mesh;
}

// --- torso -----------------------------------------------------------------

function buildTorso(mesh: MeshData, m: BodyMetrics, skin: Rgb): void {
  const h = m.height;
  const hips = m.at("Hips");

  // Ring plan lives in metrics.ts so torso garments loft on the same one.
  tagLoft(
    mesh,
    loft(mesh, torsoPlan(m), skin, {
      sides: TORSO_SIDES,
      rings: 13,
      caps: true,
      startRight: ACROSS,
    }),
    PART.TORSO,
  );

  // Groin bridge — fills between the leg roots so the crotch reads solid from
  // the front and under a skirt hem.
  const legRootX = Math.abs(m.at("LeftUpLeg")[0]);
  const bridgeFrom = vertexCount(mesh);
  loft(
    mesh,
    [
      { t: 0, c: [0, m.hipY - h * 0.02, hips[2] + h * 0.012], rx: legRootX * 0.8, rz: m.hip.rz * 0.5 },
      {
        t: 0.6,
        c: [0, m.crotchY + h * 0.012, hips[2] + h * 0.014],
        rx: legRootX * 0.62,
        rz: m.hip.rz * 0.42,
      },
      { t: 1, c: [0, m.crotchY - h * 0.008, hips[2] + h * 0.008], rx: legRootX * 0.4, rz: m.hip.rz * 0.3 },
    ],
    skin,
    { sides: 5, rings: 4, caps: true, startRight: ACROSS },
  );
  // The bridge lives at the crotch, so it hides with the pelvis.
  tagRange(mesh, bridgeFrom, PART.TORSO, 0.02);
}

/** Breasts (female) or a flat pectoral shelf (male), on the front of the ribcage. */
function buildChestForms(mesh: MeshData, m: BodyMetrics, skin: Rgb): void {
  if (m.female) {
    const { r, x, y, z, out } = m.bust;
    if (r <= 0) return;
    for (const side of [-1, 1] as const) {
      const from = vertexCount(mesh);
      loft(
        mesh,
        [
          // Rooted inside the ribcage so the seam never separates.
          { t: 0, c: [side * x, y + r * 0.15, z - r * 0.5], rx: r * 0.92, rz: r * 0.92 },
          { t: 0.55, c: [side * x, y, z + out * 0.45], rx: r, rz: r },
          { t: 1, c: [side * x * 0.98, y - r * 0.22, z + out], rx: r * 0.42, rz: r * 0.42 },
        ],
        skin,
        { sides: 5, rings: 3, caps: true, startRight: ACROSS },
      );
      // Chest height on the torso loft — hides under any upper garment.
      tagRange(mesh, from, PART.TORSO, 0.7);
    }
    return;
  }

  const { rx, rz, x, y, z } = m.pec;
  // Flat chest at rest — a pec pad at neutral muscle just adds a seam to
  // shade wrong. It grows out of the chest wall only when the slider asks.
  if (rz < m.height * 0.004) return;
  for (const side of [-1, 1] as const) {
    const from = vertexCount(mesh);
    loft(
      mesh,
      [
        { t: 0, c: [side * x, y + rx * 0.42, z - rz * 1.6], rx: rx * 0.86, rz: rz * 0.7 },
        { t: 1, c: [side * x, y - rx * 0.5, z - rz * 0.2], rx, rz },
      ],
      skin,
      { sides: 5, rings: 3, caps: true, startRight: ACROSS },
    );
    tagRange(mesh, from, PART.TORSO, 0.7);
  }
}

// --- neck and head ---------------------------------------------------------

function buildNeck(mesh: MeshData, m: BodyMetrics, skin: Rgb): void {
  const h = m.height;
  const neck = m.at("Neck");
  const neckLoft = loft(
    mesh,
    [
      // Buried in the trapezius — uncapped, or the cap pokes through the collar.
      { t: 0, c: [0, m.collarY - h * 0.035, neck[2]], rx: m.neck.rx * 1.35, rz: m.neck.rz * 1.3 },
      { t: 0.4, c: [0, m.collarY + h * 0.008, neck[2] + h * 0.002], rx: m.neck.rx, rz: m.neck.rz },
      // Just under the jaw, tilted slightly forward with the cervical curve.
      { t: 1, c: [0, m.neckTopY, neck[2] + h * 0.007], rx: m.neck.rx * 0.92, rz: m.neck.rz * 0.94 },
    ],
    skin,
    { sides: 6, rings: 4, capStart: false, capEnd: true, startRight: ACROSS },
  );
  tagLoft(mesh, neckLoft, PART.NECK);
}

/**
 * Head loft, chin (t=0) to crown (t=1).
 *
 * Shape is entirely `m.cranium` — the sex face base times the three head
 * sliders. The ring plan below decides only *where* the rings sit; how wide the
 * jaw is against the cheekbones, whether the forehead stands up or slopes back,
 * and how far the brow juts are all read off the metrics, the same way the
 * torso reads its widths. Every hard-coded fraction still here is one no face
 * varies by.
 */
function buildHead(mesh: MeshData, m: BodyMetrics, skin: Rgb): void {
  const hw = m.headHalfW;
  const hd = m.headHalfD;
  const z = m.headZ;
  const cr = m.cranium;
  const mix = (a: number, b: number, t: number) => a + (b - a) * t;

  // Skull mass sits behind the face plane; the jaw is narrower and set back.
  const keys: LoftKey[] = [
    // Under the jaw, inside the neck.
    { t: 0.0, c: [0, m.chinY - hw * 0.16, z + hd * 0.04], rx: hw * cr.chinW * 0.8, rz: hd * cr.chinD * 0.58 },
    // Chin — narrow and thrust forward. This is what stops the head reading as
    // an egg: the lower face must be a wedge, not a hemisphere.
    { t: 0.12, c: [0, m.chinY, z + hd * cr.chinOut], rx: hw * cr.chinW, rz: hd * cr.chinD },
    // Jawline, halfway to the jaw angle.
    //
    // The centre sits at 0.12, not the 0.18 it used to: `rz` here is nearly the
    // jaw's, so a centre that far forward pushed this ring's front surface past
    // both the jaw above it and the chin below. Reading down the face the front
    // went 0.96 → 1.00 → 0.92, and that non-monotonic step is a shelf across the
    // lower face, not a jawline. Keep the profile falling: 0.96 → 0.94 → 0.92.
    {
      t: 0.24,
      c: [0, m.chinY + (m.jawY - m.chinY) * 0.6, z + hd * 0.12],
      rx: hw * mix(cr.chinW, cr.jawW, 0.62),
      rz: hd * mix(cr.chinD, cr.jawD, 0.71),
    },
    // Jaw angle / masseter — widest point of the lower face.
    { t: 0.38, c: [0, m.jawY, z + hd * 0.06], rx: hw * cr.jawW, rz: hd * cr.jawD },
    // Cheekbone — the head's widest ring.
    { t: 0.55, c: [0, m.mouthY + (m.browY - m.mouthY) * 0.5, z + hd * 0.06], rx: hw * cr.cheekW, rz: hd * 0.98 },
    // Brow ridge, carried by the ring itself rather than a bar glued on top.
    // A separate slab here shades as a hard step across exactly the band the
    // texture paints eyes into; rolled into the loft it stays one continuous
    // surface and the slider still reads in profile.
    {
      t: 0.68,
      c: [0, m.browY, z + hd * (0.02 + 0.012 * cr.browOut)],
      rx: hw * cr.cheekW * 0.97,
      // Spans ~0.96·hd soft to ~1.09·hd heavy, so the brow sits behind the
      // cheekbone at one end and a centimetre proud of it at the other. That is
      // the whole slider now, and it stays a swelling of the skull rather than
      // a step the texture has to paint around.
      rz: hd * (0.96 + 0.065 * cr.browOut),
    },
    // Upper cranium. On a heavy brow this drifts back over the occiput; on a
    // soft one the forehead stands up instead.
    {
      t: 0.83,
      c: [0, m.browY + (m.crownY - m.browY) * 0.52, z - hd * 0.08 * cr.foreheadSlope],
      rx: hw * 0.92,
      rz: hd * 0.94,
    },
    // Crown.
    { t: 0.95, c: [0, m.crownY - hw * 0.1, z - hd * 0.12 * cr.foreheadSlope], rx: hw * 0.56, rz: hd * 0.54 },
    { t: 1.0, c: [0, m.crownY, z - hd * 0.12 * cr.foreheadSlope], rx: hw * 0.18, rz: hd * 0.18 },
  ];
  // 13 rings, not 10: the chin juts forward of the jaw on purpose, and at 10
  // the hermite sweep resolves that turn as a hard crease across the lower face
  // — a shading break on a smooth head rather than a jawline.
  tagLoft(mesh, loft(mesh, keys, skin, { sides: HEAD_SIDES, rings: 13, caps: true, startRight: ACROSS }), PART.HEAD);

  const featuresFrom = vertexCount(mesh);

  // Nose — the one feature that stays geometry, because it is the only one that
  // changes the silhouette. Eyes and mouth are painted, so the face is left as
  // clean surface for them; a modelled brow or lip at this scale only fights
  // the texture and shades as a crease across it.
  //
  // Anchored to the face plane rather than a fixed fraction of the skull. The
  // old placement put the tip at ~0.96·hd while the surface at nose height sits
  // near 1.0·hd, so the wedge was buried and read as a flat plate.
  // Lofted rather than boxed: a box cannot taper, so it reads as a slab stuck
  // to the face from the front however well it sits in profile. Four sides and
  // three rings is 20 triangles for a wedge that narrows to the tip.
  const noseY = m.mouthY + (m.browY - m.mouthY) * 0.5;
  const faceZ = z + hd * 0.97;
  const proj = hw * 0.2 * cr.noseOut;
  const noseW = hw * cr.noseW;
  loft(
    mesh,
    [
      // Root sits behind the face plane, so the bridge never opens a gap.
      {
        t: 0,
        c: [0, m.browY - hw * 0.04 * cr.noseLen, faceZ - hw * 0.12],
        rx: noseW * 0.07,
        rz: hw * 0.05,
      },
      {
        t: 0.6,
        c: [0, noseY, faceZ + proj * 0.5],
        rx: noseW * 0.1,
        rz: hw * 0.09,
      },
      // Tip — the widest ring, and the only part clear of the face.
      {
        t: 1,
        c: [0, noseY - hw * 0.22 * cr.noseLen, faceZ + proj],
        rx: noseW * 0.12,
        rz: hw * 0.1,
      },
    ],
    skin,
    { sides: 4, rings: 3, caps: true, startRight: ACROSS },
  );

  // Ears, flat against the skull just behind the cheekbone.
  for (const side of [-1, 1] as const) {
    orientedBox(
      mesh,
      [side * hw * 0.88, m.browY - hw * 0.34, z - hd * 0.08],
      hw * 0.07,
      hw * 0.26 * cr.earSize,
      hw * 0.14 * cr.earSize,
      [side, 0, 0.25],
      skin,
    );
  }
  // Face features sit at brow height; nothing in the catalogue hides them, and
  // nothing should — an open-fronted hood over a hidden face reads decapitated.
  tagRange(mesh, featuresFrom, PART.HEAD, 0.66);
}

// --- arms ------------------------------------------------------------------

function buildArms(mesh: MeshData, m: BodyMetrics, skin: Rgb): void {
  const h = m.height;

  for (const prefix of ["Left", "Right"] as const) {
    const side = prefix === "Left" ? 1 : -1;
    const shoulder = m.at(`${prefix}Shoulder` as CanonicalBone);
    const arm = m.at(`${prefix}Arm` as CanonicalBone);
    const foreArm = m.at(`${prefix}ForeArm` as CanonicalBone);
    const hand = m.at(`${prefix}Hand` as CanonicalBone);

    const down = normalise(sub(hand, arm));
    const wristOut = normalise(sub(hand, foreArm));
    const handEnd = add(hand, scale(wristOut, m.handLength));

    const keys: LoftKey[] = [
      // Inside the girdle — no cap, so no disk floating in the chest.
      {
        t: 0.0,
        c: [side * m.clavX, shoulder[1] - h * 0.012, shoulder[2]],
        rx: m.deltoidR * 0.94,
        rz: m.deltoidR,
      },
      // Deltoid cap. This, not the trunk, sets the shoulder silhouette — but it
      // must sit *under* the trapezius line, or it reads as an epaulette.
      {
        t: 0.1,
        c: [side * m.socketX, arm[1] + h * 0.002, arm[2]],
        rx: m.deltoidR,
        rz: m.deltoidR * 1.04,
      },
      // Deltoid insertion — the arm proper takes over.
      {
        t: 0.22,
        c: add(arm, scale(down, h * 0.045)),
        rx: m.upperArm.rx * 1.06,
        rz: m.upperArm.rz * 1.06,
      },
      { t: 0.4, c: lerp(arm, foreArm, 0.62), rx: m.upperArm.rx * 0.92, rz: m.upperArm.rz * 0.92 },
      // Elbow.
      { t: 0.52, c: foreArm, rx: m.elbow.rx, rz: m.elbow.rz },
      // Forearm belly, just below the elbow.
      { t: 0.62, c: lerp(foreArm, hand, 0.2), rx: m.foreArm.rx, rz: m.foreArm.rz },
      { t: 0.8, c: lerp(foreArm, hand, 0.72), rx: m.wrist.rx * 1.18, rz: m.wrist.rz * 1.12 },
      // Wrist — the section turns flat here and stays flat through the palm.
      { t: 0.86, c: hand, rx: m.wrist.rx, rz: m.wrist.rz },
      { t: 0.93, c: lerp(hand, handEnd, 0.45), rx: m.palm.rx, rz: m.palm.rz },
      // Knuckles, then the fingers close as one paddle.
      { t: 0.97, c: lerp(hand, handEnd, 0.78), rx: m.palm.rx * 0.94, rz: m.palm.rz * 0.94 },
      { t: 1.0, c: handEnd, rx: m.palm.rx * 0.7, rz: m.palm.rz * 0.62 },
    ];
    // startRight along X puts rx across the body and rz front-to-back, so the
    // palm faces the thigh at any A-pose angle.
    tagLoft(
      mesh,
      loft(mesh, keys, skin, {
        sides: ARM_SIDES,
        rings: 11,
        capStart: false,
        capEnd: true,
        startRight: ACROSS,
      }),
      side > 0 ? PART.ARM_L : PART.ARM_R,
    );

    // Thumb — a wedge off the front-inboard corner of the palm.
    const thumbFrom = vertexCount(mesh);
    const thumbAt = lerp(hand, handEnd, 0.32);
    orientedBox(
      mesh,
      add(thumbAt, [side * -m.palm.rx * 0.5, 0, m.palm.rz * 0.75]),
      m.thumbR * 0.62,
      m.thumbR * 1.5,
      m.thumbR * 0.8,
      [side * -0.25, -0.55, 0.8],
      skin,
    );
    tagRange(mesh, thumbFrom, side > 0 ? PART.ARM_L : PART.ARM_R, 0.93);
  }
}

// --- legs and feet ---------------------------------------------------------

function buildLegs(mesh: MeshData, m: BodyMetrics, skin: Rgb): void {
  const h = m.height;
  const hips = m.at("Hips");

  for (const prefix of ["Left", "Right"] as const) {
    const side = prefix === "Left" ? 1 : -1;
    const upLeg = m.at(`${prefix}UpLeg` as CanonicalBone);
    const leg = m.at(`${prefix}Leg` as CanonicalBone);
    const foot = m.at(`${prefix}Foot` as CanonicalBone);
    const legX = Math.abs(upLeg[0]);

    const keys: LoftKey[] = [
      // Buried inside the pelvis bowl — uncapped, so the hip owns the join.
      {
        t: 0.0,
        c: [side * legX * 0.6, m.hipY + h * 0.012, hips[2] + m.buttZ * 0.5],
        rx: m.hipSocket.rx,
        rz: m.hipSocket.rz,
      },
      // Upper thigh, still wide where it meets the glute.
      {
        t: 0.1,
        c: [side * legX * 0.95, m.crotchY + h * 0.01, upLeg[2] - h * 0.004],
        rx: m.thigh.rx * 1.1,
        rz: m.thigh.rz * 1.12,
      },
      { t: 0.24, c: [side * legX, upLeg[1] - h * 0.03, upLeg[2]], rx: m.thigh.rx, rz: m.thigh.rz },
      // Above the knee the thigh narrows to the condyles.
      { t: 0.42, c: lerp(upLeg, leg, 0.78), rx: m.thigh.rx * 0.78, rz: m.thigh.rz * 0.8 },
      { t: 0.52, c: leg, rx: m.knee.rx, rz: m.knee.rz },
      // Calf belly — behind the shin axis, and high on the leg.
      {
        t: 0.65,
        c: add(lerp(leg, foot, 0.26), [0, 0, m.calfZ]),
        rx: m.calf.rx,
        rz: m.calf.rz,
      },
      { t: 0.82, c: add(lerp(leg, foot, 0.62), [0, 0, m.calfZ * 0.4]), rx: m.calf.rx * 0.72, rz: m.calf.rz * 0.7 },
      // Ankle.
      { t: 1.0, c: [foot[0], foot[1], foot[2]], rx: m.ankle.rx, rz: m.ankle.rz },
    ];
    tagLoft(
      mesh,
      loft(mesh, keys, skin, {
        sides: LEG_SIDES,
        rings: 10,
        capStart: false,
        capEnd: false,
        startRight: ACROSS,
      }),
      side > 0 ? PART.LEG_L : PART.LEG_R,
    );

    buildFoot(mesh, m, skin, foot, side > 0 ? PART.FOOT_L : PART.FOOT_R);
  }
}

/**
 * Foot lofted along its own axis (heel → toe) rather than dangling off the
 * shin, so the ring plane is vertical and `rz` is the foot's height above the
 * floor. Every section is sized to touch y=0 exactly where it should: heel and
 * ball on the ground, arch lifted.
 */
function buildFoot(
  mesh: MeshData,
  m: BodyMetrics,
  skin: Rgb,
  ankle: Vec3Tuple,
  part: BodyPart,
): void {
  const x = ankle[0];
  const z = ankle[2];
  const hw = m.footHalfW;
  const toeZ = z + m.footLength - m.heelBack;
  const heelZ = z - m.heelBack;

  const keys: LoftKey[] = [
    // Back of the heel, rounded off the floor.
    { t: 0.0, c: [x, m.ankleHeight * 0.62, heelZ], rx: hw * 0.62, rz: m.ankleHeight * 0.62 },
    // Heel pad — on the ground.
    { t: 0.16, c: [x, m.ankleHeight * 0.52, heelZ + m.footLength * 0.1], rx: hw * 0.78, rz: m.ankleHeight * 0.52 },
    // Under the ankle: tallest section, arch lifted off the floor.
    { t: 0.36, c: [x, m.ankleHeight * 0.62, z + m.footLength * 0.06], rx: hw * 0.84, rz: m.ankleHeight * 0.56 },
    // Midfoot / instep.
    { t: 0.58, c: [x, m.ankleHeight * 0.44, z + m.footLength * 0.28], rx: hw * 0.92, rz: m.ankleHeight * 0.44 },
    // Ball of the foot — widest, back on the ground.
    { t: 0.82, c: [x, m.ankleHeight * 0.34, z + m.footLength * 0.5], rx: hw, rz: m.ankleHeight * 0.34 },
    // Toes.
    { t: 1.0, c: [x, m.ankleHeight * 0.24, toeZ], rx: hw * 0.82, rz: m.ankleHeight * 0.24 },
  ];
  tagLoft(mesh, loft(mesh, keys, skin, { sides: FOOT_SIDES, rings: 7, caps: true, startRight: ACROSS }), part);

  // Ankle collar — bridges the shin ring into the top of the foot.
  const collarFrom = vertexCount(mesh);
  loft(
    mesh,
    [
      { t: 0, c: [x, ankle[1] + m.height * 0.012, z], rx: m.ankle.rx, rz: m.ankle.rz },
      { t: 1, c: [x, m.ankleHeight * 0.66, z + m.footLength * 0.04], rx: hw * 0.8, rz: m.ankleHeight * 0.6 },
    ],
    skin,
    { sides: FOOT_SIDES, rings: 3, caps: true, startRight: ACROSS },
  );
  tagRange(mesh, collarFrom, part, 0.35);
}

export const SKIN = {
  fair: { r: 220, g: 185, b: 160 } satisfies Rgb,
  light: { r: 200, g: 160, b: 130 } satisfies Rgb,
  medium: { r: 168, g: 128, b: 100 } satisfies Rgb,
  tan: { r: 145, g: 105, b: 78 } satisfies Rgb,
  deep: { r: 90, g: 62, b: 48 } satisfies Rgb,
};
