<!-- AVATAR-FORGE · part 10 -->
 keeps its parts private, can still hand its rig to
 * the mixer by traversal. Returns {partName: group}. */
function collectParts(model) {
  const out = {};
  model.traverse(o => {
    if (o.name && o.name.startsWith('rig_') && o.parent) out[o.name.slice(4)] = o.parent;
  });
  return out;
}

/* createGestureSession({timeout, onEvent}) — the cooperative
 * protocol of the design, in miniature: a fist bump (or any paired
 * gesture) is offered under a session id, accepted or declined by
 * the partner, played, and completed — or it expires safely.
 * Proximity and facing are the caller's physics; agreement and
 * time are kept here. Stale ids are refused. */
function createGestureSession(opts = {}) {
  const timeout = opts.timeout ?? 3;
  const onEvent = opts.onEvent || (() => {});
  let state = 'idle', id = null, clock = 0;
  const go = (next, why) => { state = next; onEvent(next, { id, why }); };
  return {
    get state() { return state; },
    get id() { return id; },
    offer(sessionId) {
      if (state !== 'idle' && state !== 'done' && state !== 'cancelled' && state !== 'expired') return false;
      id = sessionId; clock = 0; go('offered'); return true;
    },
    accept(sessionId) {
      if (state !== 'offered' || sessionId !== id) return false;
      go('agreed'); return true;
    },
    decline() { if (state === 'offered' || state === 'agreed') { go('cancelled', 'declined'); return true; } return false; },
    start() { if (state !== 'agreed') return false; go('playing'); return true; },
    complete() { if (state !== 'playing') return false; go('done'); return true; },
    cancel() { if (state === 'idle') return false; go('cancelled', 'cancelled'); return true; },
    tick(dt) {
      if (state === 'offered') { clock += dt; if (clock >= timeout) go('expired', 'timeout'); }
      return state;
    }
  };
}

export { HERO_POSES, GESTURES, createHeroMotion, collectParts, createGestureSession };


===== FILE: public/avatar-forge/three.module.js =====
/**
 * @license
 * Copyright 2010-2024 Three.js Authors
 * SPDX-License-Identifier: MIT
 */
const REVISION = '165';

const MOUSE = { LEFT: 0, MIDDLE: 1, RIGHT: 2, ROTATE: 0, DOLLY: 1, PAN: 2 };
const TOUCH = { ROTATE: 0, PAN: 1, DOLLY_PAN: 2, DOLLY_ROTATE: 3 };
const CullFaceNone = 0;
const CullFaceBack = 1;
const CullFaceFront = 2;
const CullFaceFrontBack = 3;
const BasicShadowMap = 0;
const PCFShadowMap = 1;
const PCFSoftShadowMap = 2;
const VSMShadowMap = 3;
const FrontSide = 0;
const BackSide = 1;
const DoubleSide = 2;
const NoBlending = 0;
const NormalBlending = 1;
const AdditiveBlending = 2;
const SubtractiveBlending = 3;
const MultiplyBlending = 4;
const CustomBlending = 5;
const AddEquation = 100;
const SubtractEquation = 101;
const ReverseSubtractEquation = 102;
const MinEquation = 103;
const MaxEquation = 104;
const ZeroFactor = 200;
const OneFactor = 201;
const SrcColorFactor = 202;
const OneMinusSrcColorFactor = 203;
const SrcAlphaFactor = 204;
const OneMinusSrcAlphaFactor = 205;
const DstAlphaFactor = 206;
const OneMinusDstAlphaFactor = 207;
const DstColorFactor = 208;
const OneMinusDstColorFactor = 209;
const SrcAlphaSaturateFactor = 210;
const ConstantColorFactor = 211;
const OneMinusConstantColorFactor = 212;
const ConstantAlphaFactor = 213;
const OneMinusConstantAlphaFactor = 214;
const NeverDepth = 0;
const AlwaysDepth = 1;
const LessDepth = 2;
const LessEqualDepth = 3;
const EqualDepth = 4;
const GreaterEqualDepth = 5;
const GreaterDepth = 6;
const NotEqualDepth = 7;
const MultiplyOperation = 0;
const MixOperation = 1;
const AddOperation = 2;
const NoToneMapping = 0;
const LinearToneMapping = 1;
const ReinhardToneMapping = 2;
const CineonToneMapping = 3;
const ACESFilmicToneMapping = 4;
const CustomToneMapping = 5;
const AgXToneMapping = 6;
const NeutralToneMapping = 7;
const AttachedBindMode = 'attached';
const DetachedBindMode = 'detached';

const UVMapping = 300;
const CubeReflectionMapping = 301;
const CubeRefractionMapping = 302;
const EquirectangularReflectionMapping = 303;
const EquirectangularRefractionMapping = 304;
const CubeUVReflectionMapping = 306;
const RepeatWrapping = 1000;
const ClampToEdgeWrapping = 1001;
const MirroredRepeatWrapping = 1002;
const NearestFilter = 1003;
const NearestMipmapNearestFilter = 1004;
const NearestMipMapNearestFilter = 1004;
const NearestMipmapLinearFilter = 1005;
const NearestMipMapLinearFilter = 1005;
const LinearFilter = 1006;
const LinearMipmapNearestFilter = 1007;
const LinearMipMapNearestFilter = 1007;
const LinearMipmapLinearFilter = 1008;
const LinearMipMapLinearFilter = 1008;
const UnsignedByteType = 1009;
const ByteType = 1010;
const ShortType = 1011;
const UnsignedShortType = 1012;
const IntType = 1013;
const UnsignedIntType = 1014;
const FloatType = 1015;
const HalfFloatType = 1016;
const UnsignedShort4444Type = 1017;
const UnsignedShort5551Type = 1018;
const UnsignedInt248Type = 1020;
const UnsignedInt5999Type = 35902;
const AlphaFormat = 1021