lecodes-assets 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +449 -326
  2. package/dist/cli.js +2161 -198
  3. package/dist/index.js +2268 -198
  4. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -959,15 +959,18 @@ var init_toDocument = __esm(() => {
959
959
  var exports_rig = {};
960
960
  __export(exports_rig, {
961
961
  worldOf: () => worldOf,
962
+ sameSkeleton: () => sameSkeleton,
962
963
  rotateRig: () => rotateRig,
963
964
  rigFromFbx: () => rigFromFbx,
964
965
  rigFromDocument: () => rigFromDocument,
966
+ rebindClips: () => rebindClips,
965
967
  qslerp: () => qslerp,
966
968
  qrot: () => qrot,
967
969
  qnorm: () => qnorm,
968
970
  qmul: () => qmul,
969
971
  qinv: () => qinv,
970
972
  loadRig: () => loadRig,
973
+ hasSkin: () => hasSkin,
971
974
  checkUniformScale: () => checkUniformScale,
972
975
  bindVsRest: () => bindVsRest
973
976
  });
@@ -992,7 +995,7 @@ var qmul = (a, b) => [
992
995
  const th = Math.acos(d), s = Math.sin(th);
993
996
  const wa = Math.sin((1 - t) * th) / s, wb = Math.sin(t * th) / s;
994
997
  return [a[0] * wa + e[0] * wb, a[1] * wa + e[1] * wb, a[2] * wa + e[2] * wb, a[3] * wa + e[3] * wb];
995
- }, qrot = (q, v) => {
998
+ }, sub = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]], scl = (a, k) => [a[0] * k, a[1] * k, a[2] * k], len = (v) => Math.hypot(v[0], v[1], v[2]), qrot = (q, v) => {
996
999
  const [x, y, z, w] = q;
997
1000
  const tx = 2 * (y * v[2] - z * v[1]), ty = 2 * (z * v[0] - x * v[2]), tz = 2 * (x * v[1] - y * v[0]);
998
1001
  return [
@@ -1085,16 +1088,31 @@ var qmul = (a, b) => [
1085
1088
  }
1086
1089
  const k = Math.sqrt(1 + i - a - e) * 2;
1087
1090
  return qnorm([(g + c) / k, (h + f) / k, k / 4, (b - d) / k]);
1088
- }, buildBind = (parents, order, rest, ibmByNode) => {
1091
+ }, buildBind = (parents, order, rest, ibmByNode, spaceByNode) => {
1089
1092
  const world = new Array(parents.length);
1090
1093
  const fromSkin = new Array(parents.length).fill(false);
1094
+ const restWorld = new Array(parents.length);
1091
1095
  for (const i of order) {
1092
- const ibm = ibmByNode.get(i);
1093
- if (ibm) {
1094
- world[i] = matInvRigid(ibm);
1095
- fromSkin[i] = true;
1096
- }
1096
+ const local2 = matCompose(rest.t[i], rest.r[i], rest.s[i]);
1097
+ restWorld[i] = parents[i] >= 0 ? matMul(restWorld[parents[i]], local2) : local2;
1097
1098
  }
1099
+ let rawDist = 0, foldedDist = 0;
1100
+ const raw = new Array(parents.length), folded = new Array(parents.length);
1101
+ for (const i of order) {
1102
+ const ibm = ibmByNode.get(i);
1103
+ if (!ibm)
1104
+ continue;
1105
+ raw[i] = matInvRigid(ibm);
1106
+ const space = spaceByNode.get(i);
1107
+ folded[i] = space ? matMul(space, raw[i]) : raw[i];
1108
+ fromSkin[i] = true;
1109
+ rawDist += Math.hypot(raw[i][12] - restWorld[i][12], raw[i][13] - restWorld[i][13], raw[i][14] - restWorld[i][14]);
1110
+ foldedDist += Math.hypot(folded[i][12] - restWorld[i][12], folded[i][13] - restWorld[i][13], folded[i][14] - restWorld[i][14]);
1111
+ }
1112
+ const useFolded = foldedDist <= rawDist;
1113
+ for (const i of order)
1114
+ if (fromSkin[i])
1115
+ world[i] = useFolded ? folded[i] : raw[i];
1098
1116
  const accum = new Array(parents.length).fill(1);
1099
1117
  for (const i of order)
1100
1118
  accum[i] = (parents[i] >= 0 ? accum[parents[i]] : 1) * rest.s[i][0];
@@ -1138,9 +1156,19 @@ var qmul = (a, b) => [
1138
1156
  local.r[i] = qnorm(qmul(qconj(worldRot[p]), worldRot[i]));
1139
1157
  }
1140
1158
  return { worldRot, worldPos, fromSkin, local, scale: Math.abs(scale - 1) > 0.01 ? scale : 1 };
1141
- }, finish = (label, names, parents, rest, makeClips, ibmByNode) => {
1159
+ }, restWorldOf = (parents, rest, node) => {
1160
+ const chain = [];
1161
+ for (let a = node;a >= 0; a = parents[a])
1162
+ chain.unshift(a);
1163
+ let m = null;
1164
+ for (const i of chain) {
1165
+ const local = matCompose(rest.t[i], rest.r[i], rest.s[i]);
1166
+ m = m ? matMul(m, local) : local;
1167
+ }
1168
+ return m;
1169
+ }, finish = (label, names, parents, rest, makeClips, ibmByNode, spaceByNode = new Map) => {
1142
1170
  const order = buildOrder(parents);
1143
- const bind = buildBind(parents, order, rest, ibmByNode);
1171
+ const bind = buildBind(parents, order, rest, ibmByNode, spaceByNode);
1144
1172
  const rig = {
1145
1173
  label,
1146
1174
  names,
@@ -1204,13 +1232,19 @@ var qmul = (a, b) => [
1204
1232
  s: scene.nodes.map((n) => [...n.s])
1205
1233
  };
1206
1234
  const ibmByNode = new Map;
1207
- for (const skin of scene.skins) {
1235
+ const spaceByNode = new Map;
1236
+ scene.skins.forEach((skin, si) => {
1208
1237
  const ibm = f32(scene, skin.ibm, skin.joints.length * 16);
1238
+ const meshNode = scene.nodes.findIndex((n) => n.skin === si);
1239
+ const space = meshNode >= 0 ? restWorldOf(parents, rest, meshNode) : null;
1209
1240
  skin.joints.forEach((node, j) => {
1210
- if (!ibmByNode.has(node))
1211
- ibmByNode.set(node, Array.from(ibm.subarray(j * 16, j * 16 + 16)));
1241
+ if (ibmByNode.has(node))
1242
+ return;
1243
+ ibmByNode.set(node, Array.from(ibm.subarray(j * 16, j * 16 + 16)));
1244
+ if (space)
1245
+ spaceByNode.set(node, space);
1212
1246
  });
1213
- }
1247
+ });
1214
1248
  const makeClips = (base) => scene.animations.filter((a) => a.tracks.length > 0).map((anim) => {
1215
1249
  const tracks = anim.tracks.map((tr) => {
1216
1250
  const n = tr.path === "rotation" ? 4 : 3;
@@ -1234,7 +1268,7 @@ var qmul = (a, b) => [
1234
1268
  }
1235
1269
  };
1236
1270
  });
1237
- return finish(path, names, parents, rest, makeClips, ibmByNode);
1271
+ return finish(path, names, parents, rest, makeClips, ibmByNode, spaceByNode);
1238
1272
  }, clipsFromDocument = (doc, nodes, base) => {
1239
1273
  const idx = new Map(nodes.map((n, i) => [n, i]));
1240
1274
  return doc.getRoot().listAnimations().map((anim) => {
@@ -1284,18 +1318,127 @@ var qmul = (a, b) => [
1284
1318
  s: nodes.map((n) => [...n.getScale()])
1285
1319
  };
1286
1320
  const ibmByNode = new Map;
1321
+ const spaceByNode = new Map;
1287
1322
  for (const skin of doc.getRoot().listSkins()) {
1288
1323
  const acc = skin.getInverseBindMatrices();
1289
1324
  if (!acc)
1290
1325
  continue;
1326
+ const meshNode = nodes.findIndex((n) => n.getSkin() === skin);
1327
+ const space = meshNode >= 0 ? restWorldOf(parents, rest, meshNode) : null;
1291
1328
  skin.listJoints().forEach((joint, j) => {
1292
1329
  const i = idx.get(joint);
1293
- if (i !== undefined && !ibmByNode.has(i))
1294
- ibmByNode.set(i, acc.getElement(j, []));
1330
+ if (i === undefined || ibmByNode.has(i))
1331
+ return;
1332
+ ibmByNode.set(i, acc.getElement(j, []));
1333
+ if (space)
1334
+ spaceByNode.set(i, space);
1295
1335
  });
1296
1336
  }
1297
- return { ...finish(label, names, parents, rest, (base) => clipsFromDocument(doc, nodes, base), ibmByNode), doc, nodes };
1298
- }, loadRig = async (path) => rigFromFbx(path, new Uint8Array(readFileSync3(path))), rotateRig = (rig, q) => {
1337
+ return { ...finish(label, names, parents, rest, (base) => clipsFromDocument(doc, nodes, base), ibmByNode, spaceByNode), doc, nodes };
1338
+ }, loadRig = async (path) => rigFromFbx(path, new Uint8Array(readFileSync3(path))), hasSkin = (rig) => rig.bind.fromSkin.some(Boolean), shortBoneName = (n) => n.slice(n.search(/[^:|]*$/)), sameSkeleton = (rig, source) => {
1339
+ const short = new Map;
1340
+ rig.names.forEach((n, i) => {
1341
+ const s = shortBoneName(n);
1342
+ if (!short.has(s))
1343
+ short.set(s, i);
1344
+ });
1345
+ const map = source.names.map((n) => rig.index.get(n) ?? short.get(shortBoneName(n)) ?? -1);
1346
+ const share = map.filter((j) => j >= 0).length / Math.max(1, source.names.length);
1347
+ let worstDeg = 0, worstBone = "";
1348
+ source.names.forEach((_, i) => {
1349
+ const j = map[i];
1350
+ if (j < 0)
1351
+ return;
1352
+ const ps = source.parents[i], pt = rig.parents[j];
1353
+ if (ps < 0 || pt < 0 || map[ps] !== pt)
1354
+ return;
1355
+ const a = rig.rest.t[j], b = source.rest.t[i];
1356
+ const la = Math.hypot(...a), lb = Math.hypot(...b);
1357
+ if (la < 0.05 || lb < 0.05)
1358
+ return;
1359
+ const deg = Math.acos(Math.max(-1, Math.min(1, (a[0] * b[0] + a[1] * b[1] + a[2] * b[2]) / (la * lb)))) * 180 / Math.PI;
1360
+ if (deg > worstDeg) {
1361
+ worstDeg = deg;
1362
+ worstBone = source.names[i];
1363
+ }
1364
+ });
1365
+ return { same: share >= 0.8 && worstDeg < 45, share, worstDeg, worstBone };
1366
+ }, rebindClips = (rig, source) => {
1367
+ const short = new Map;
1368
+ rig.names.forEach((n, i) => {
1369
+ const s = shortBoneName(n);
1370
+ if (!short.has(s))
1371
+ short.set(s, i);
1372
+ });
1373
+ const map = source.names.map((n) => rig.index.get(n) ?? short.get(shortBoneName(n)) ?? -1);
1374
+ const unbound = source.names.filter((_, i) => map[i] < 0);
1375
+ const bound = map.filter((j) => j >= 0).length;
1376
+ const base = rig.bind.local, srcRest = source.rest;
1377
+ const animated = source.names.map(() => false);
1378
+ for (const c of source.clips)
1379
+ for (let k = 0;k <= 8; k++) {
1380
+ const p = c.sample(k / 8 * c.duration);
1381
+ p.t.forEach((t, i) => {
1382
+ if (!animated[i] && len(sub(t, srcRest.t[i])) > 0.000001)
1383
+ animated[i] = true;
1384
+ });
1385
+ }
1386
+ const sampleAt = (c, time, scale2) => {
1387
+ const p = c.sample(time);
1388
+ const out = { t: base.t.map((v) => [...v]), r: base.r.map((q) => [...q]), s: base.s.map((v) => [...v]) };
1389
+ map.forEach((j, i) => {
1390
+ if (j < 0)
1391
+ return;
1392
+ out.r[j] = p.r[i];
1393
+ if (animated[i])
1394
+ out.t[j] = scl(p.t[i], scale2);
1395
+ });
1396
+ return out;
1397
+ };
1398
+ let sameLengths = true, num = 0, den = 0;
1399
+ map.forEach((j, i) => {
1400
+ if (j < 0 || animated[i] || source.parents[i] < 0 || rig.parents[j] < 0)
1401
+ return;
1402
+ const a = len(base.t[j]), b = len(srcRest.t[i]);
1403
+ num += a;
1404
+ den += b;
1405
+ if (Math.abs(a - b) > 0.01 * Math.max(a, b, 0.01))
1406
+ sameLengths = false;
1407
+ });
1408
+ let scale = 1;
1409
+ if (!sameLengths && source.clips.length) {
1410
+ const skinned = rig.bind.fromSkin.some(Boolean) ? rig.bind.worldPos.filter((_, i) => rig.bind.fromSkin[i]) : rig.bind.worldPos;
1411
+ const ground = Math.min(...skinned.map((p) => p[1]));
1412
+ const lowest = (s) => {
1413
+ let lo = Infinity;
1414
+ for (const c of source.clips)
1415
+ for (let k = 0;k < 16; k++) {
1416
+ const w = worldOf(rig, sampleAt(c, k / 15 * c.duration, s));
1417
+ for (let i = 0;i < w.pos.length; i++)
1418
+ if (rig.bind.fromSkin[i] || !rig.bind.fromSkin.some(Boolean))
1419
+ lo = Math.min(lo, w.pos[i][1]);
1420
+ }
1421
+ return lo;
1422
+ };
1423
+ let a = 0.5, b = 2, fa = lowest(a) - ground, fb = lowest(b) - ground;
1424
+ if (fa < 0 && fb > 0) {
1425
+ for (let it = 0;it < 24; it++) {
1426
+ const m = (a + b) / 2, fm = lowest(m) - ground;
1427
+ if (fm < 0) {
1428
+ a = m;
1429
+ fa = fm;
1430
+ } else {
1431
+ b = m;
1432
+ fb = fm;
1433
+ }
1434
+ }
1435
+ scale = (a + b) / 2;
1436
+ } else
1437
+ scale = den > 0 && num > 0 ? num / den : 1;
1438
+ }
1439
+ const clips = source.clips.map((c) => ({ name: c.name, duration: c.duration, sample: (time) => sampleAt(c, time, scale) }));
1440
+ return { rig: { ...rig, clips }, bound, total: source.names.length, unbound, scale };
1441
+ }, rotateRig = (rig, q) => {
1299
1442
  const roots = rig.parents.map((p, i) => p < 0 ? i : -1).filter((i) => i >= 0);
1300
1443
  const turn = (pose) => {
1301
1444
  const out2 = { t: pose.t.map((v) => [...v]), r: pose.r.map((v) => [...v]), s: pose.s };
@@ -1338,9 +1481,9 @@ var u322 = (b, o) => (b[o] << 24 | b[o + 1] << 16 | b[o + 2] << 8 | b[o + 3]) >>
1338
1481
  let palette = null;
1339
1482
  let trns = null;
1340
1483
  while (o + 8 <= b.length) {
1341
- const len = u322(b, o);
1484
+ const len2 = u322(b, o);
1342
1485
  const type = String.fromCharCode(b[o + 4], b[o + 5], b[o + 6], b[o + 7]);
1343
- const data = b.subarray(o + 8, o + 8 + len);
1486
+ const data = b.subarray(o + 8, o + 8 + len2);
1344
1487
  if (type === "IHDR") {
1345
1488
  width = u322(data, 0);
1346
1489
  height = u322(data, 4);
@@ -1355,7 +1498,7 @@ var u322 = (b, o) => (b[o] << 24 | b[o + 1] << 16 | b[o + 2] << 8 | b[o + 3]) >>
1355
1498
  idat.push(data);
1356
1499
  else if (type === "IEND")
1357
1500
  break;
1358
- o += 12 + len;
1501
+ o += 12 + len2;
1359
1502
  }
1360
1503
  if (!width || !height || interlace !== 0 || depth !== 8 && depth !== 16)
1361
1504
  return null;
@@ -4046,12 +4189,12 @@ var init_lightmapUv = __esm(() => {
4046
4189
  var num2 = 0;
4047
4190
  for (var i2 = 0;i2 < iovcnt; i2++) {
4048
4191
  var ptr = HEAPU32[iov >> 2];
4049
- var len = HEAPU32[iov + 4 >> 2];
4192
+ var len2 = HEAPU32[iov + 4 >> 2];
4050
4193
  iov += 8;
4051
- for (var j = 0;j < len; j++) {
4194
+ for (var j = 0;j < len2; j++) {
4052
4195
  printChar(fd, HEAPU8[ptr + j]);
4053
4196
  }
4054
- num2 += len;
4197
+ num2 += len2;
4055
4198
  }
4056
4199
  HEAPU32[pnum >> 2] = num2;
4057
4200
  return 0;
@@ -4311,6 +4454,193 @@ var init_lightmapUv2 = __esm(() => {
4311
4454
  init_lightmapUv();
4312
4455
  });
4313
4456
 
4457
+ // src/doctor/lod.ts
4458
+ import { simplifyPrimitive, weldPrimitive } from "@gltf-transform/functions";
4459
+ import { MeshoptSimplifier } from "meshoptimizer";
4460
+ function planLod(doc, opts) {
4461
+ const ratios = (opts.lod ?? []).filter((r) => r > 0 && r < 1).sort((a, b) => b - a);
4462
+ if (!ratios.length)
4463
+ return null;
4464
+ const nodes = [];
4465
+ let existing = 0;
4466
+ for (const node of doc.getRoot().listNodes()) {
4467
+ const mesh = node.getMesh();
4468
+ if (!mesh)
4469
+ continue;
4470
+ if (LOD_NAME.test(node.getName())) {
4471
+ existing++;
4472
+ continue;
4473
+ }
4474
+ const tris = meshTris(mesh);
4475
+ if (tris < LOD_MIN_TRIS)
4476
+ continue;
4477
+ nodes.push({ node, name: node.getName() || mesh.getName() || `mesh${nodes.length}`, tris, skinned: !!node.getSkin() });
4478
+ }
4479
+ return { ratios, bones: !!opts.lodBones, nodes, existing };
4480
+ }
4481
+ async function applyLod(doc, plan) {
4482
+ await MeshoptSimplifier.ready;
4483
+ const result = { levels: plan.ratios.length, nodes: 0, tris: [], bones: [], skipped: [] };
4484
+ for (const entry of plan.nodes) {
4485
+ const { node } = entry;
4486
+ const baseMesh = node.getMesh();
4487
+ const parent = node.getParentNode();
4488
+ const scenes = parent ? [] : doc.getRoot().listScenes().filter((s) => s.listChildren().includes(node));
4489
+ const baseExtras = node.getExtras() ?? {};
4490
+ const baseLecodes = baseExtras.lecodes ?? {};
4491
+ node.setExtras({ ...baseExtras, lecodes: { ...baseLecodes, lod: { level: 0, levels: plan.ratios.length + 1, tris: entry.tris } } });
4492
+ const levels = [entry.tris];
4493
+ for (let i = 0;i < plan.ratios.length; i++) {
4494
+ const level = i + 1;
4495
+ const ratio = plan.ratios[i];
4496
+ const mesh = doc.createMesh(`${baseMesh.getName() || entry.name}_LOD${level}`);
4497
+ let tris = 0;
4498
+ for (const prim of baseMesh.listPrimitives()) {
4499
+ if (prim.getMode() !== 4)
4500
+ continue;
4501
+ let copy = clonePrimitive(doc, prim);
4502
+ weldPrimitive(copy);
4503
+ copy = simplifyPrimitive(copy, { simplifier: MeshoptSimplifier, ratio, error: 0.02, lockBorder: false });
4504
+ tris += primTris(copy);
4505
+ mesh.addPrimitive(copy);
4506
+ }
4507
+ if (tris > levels[levels.length - 1] * 0.85) {
4508
+ for (const p of mesh.listPrimitives()) {
4509
+ for (const sem of p.listSemantics())
4510
+ p.getAttribute(sem)?.dispose();
4511
+ p.getIndices()?.dispose();
4512
+ p.dispose();
4513
+ }
4514
+ mesh.dispose();
4515
+ result.skipped.push({ name: entry.name, level, tris });
4516
+ break;
4517
+ }
4518
+ const lodNode = doc.createNode(`${node.getName() || entry.name}_LOD${level}`).setMesh(mesh).setTranslation(node.getTranslation()).setRotation(node.getRotation()).setScale(node.getScale());
4519
+ if (node.getSkin())
4520
+ lodNode.setSkin(node.getSkin());
4521
+ lodNode.setExtras({ lecodes: { lod: { level, ratio, tris } } });
4522
+ if (parent)
4523
+ parent.addChild(lodNode);
4524
+ else
4525
+ for (const s of scenes)
4526
+ s.addChild(lodNode);
4527
+ levels.push(tris);
4528
+ if (plan.bones && i === plan.ratios.length - 1 && node.getSkin()) {
4529
+ const r = reduceSkinForLod(doc, lodNode);
4530
+ if (r)
4531
+ result.bones.push({ name: entry.name, ...r });
4532
+ }
4533
+ }
4534
+ result.nodes++;
4535
+ result.tris.push({ name: entry.name, levels });
4536
+ }
4537
+ return result;
4538
+ }
4539
+ var LOD_MIN_TRIS = 2000, LOD_JOINT_MASK, LOD_NAME, shortName3 = (s) => {
4540
+ const p = Math.max(s.lastIndexOf(":"), s.lastIndexOf("|"));
4541
+ return p < 0 ? s : s.slice(p + 1);
4542
+ }, primTris = (p) => {
4543
+ if (p.getMode() !== 4)
4544
+ return 0;
4545
+ const idx = p.getIndices();
4546
+ const n = idx ? idx.getCount() : p.getAttribute("POSITION")?.getCount() ?? 0;
4547
+ return Math.floor(n / 3);
4548
+ }, meshTris = (m) => m.listPrimitives().reduce((s, p) => s + primTris(p), 0), clonePrimitive = (doc, prim) => {
4549
+ const buffer = doc.getRoot().listBuffers()[0] ?? doc.createBuffer();
4550
+ const cloneAcc = (a) => doc.createAccessor(a.getName(), buffer).setType(a.getType()).setNormalized(a.getNormalized()).setArray(a.getArray().slice());
4551
+ const out = doc.createPrimitive().setMode(prim.getMode()).setMaterial(prim.getMaterial());
4552
+ for (const sem of prim.listSemantics())
4553
+ out.setAttribute(sem, cloneAcc(prim.getAttribute(sem)));
4554
+ const idx = prim.getIndices();
4555
+ if (idx)
4556
+ out.setIndices(cloneAcc(idx));
4557
+ return out;
4558
+ }, reduceSkinForLod = (doc, lodNode) => {
4559
+ const skin = lodNode.getSkin();
4560
+ const mesh = lodNode.getMesh();
4561
+ if (!skin || !mesh)
4562
+ return null;
4563
+ const joints = skin.listJoints();
4564
+ const removed = new Set;
4565
+ joints.forEach((j, i) => {
4566
+ if (LOD_JOINT_MASK.test(shortName3(j.getName())))
4567
+ removed.add(i);
4568
+ });
4569
+ if (!removed.size)
4570
+ return null;
4571
+ const idxOf = new Map(joints.map((j, i) => [j, i]));
4572
+ const remap = joints.map((j, i) => {
4573
+ if (!removed.has(i))
4574
+ return i;
4575
+ for (let p = j.getParentNode();p; p = p.getParentNode()) {
4576
+ const k = idxOf.get(p);
4577
+ if (k !== undefined && !removed.has(k))
4578
+ return k;
4579
+ }
4580
+ return i;
4581
+ });
4582
+ const kept = [...new Set(remap)].sort((a, b) => a - b);
4583
+ const newIndex = new Map(kept.map((old, k) => [old, k]));
4584
+ const buffer = doc.getRoot().listBuffers()[0] ?? doc.createBuffer();
4585
+ const newSkin = doc.createSkin(`${skin.getName() || "skin"}_LOD`);
4586
+ if (skin.getSkeleton())
4587
+ newSkin.setSkeleton(skin.getSkeleton());
4588
+ for (const i of kept)
4589
+ newSkin.addJoint(joints[i]);
4590
+ const ibm = skin.getInverseBindMatrices();
4591
+ if (ibm) {
4592
+ const src = ibm.getArray();
4593
+ const out = new Float32Array(kept.length * 16);
4594
+ kept.forEach((i, k) => out.set(src.subarray(i * 16, i * 16 + 16), k * 16));
4595
+ newSkin.setInverseBindMatrices(doc.createAccessor(`${ibm.getName() || "ibm"}_LOD`, buffer).setType("MAT4").setArray(out));
4596
+ }
4597
+ const jv = [], wv = [];
4598
+ for (const prim of mesh.listPrimitives()) {
4599
+ const sets = [];
4600
+ for (let s = 0;; s++) {
4601
+ const j = prim.getAttribute(`JOINTS_${s}`), w = prim.getAttribute(`WEIGHTS_${s}`);
4602
+ if (!j || !w)
4603
+ break;
4604
+ sets.push({ j, w });
4605
+ }
4606
+ if (!sets.length)
4607
+ continue;
4608
+ const count = sets[0].j.getCount();
4609
+ const folded = new Map;
4610
+ for (let v = 0;v < count; v++) {
4611
+ folded.clear();
4612
+ for (const { j, w } of sets) {
4613
+ j.getElement(v, jv);
4614
+ w.getElement(v, wv);
4615
+ for (let k = 0;k < 4; k++) {
4616
+ const weight = wv[k] ?? 0;
4617
+ if (weight <= 0)
4618
+ continue;
4619
+ const target = newIndex.get(remap[jv[k]] ?? jv[k]) ?? 0;
4620
+ folded.set(target, (folded.get(target) ?? 0) + weight);
4621
+ }
4622
+ }
4623
+ const pairs = [...folded].sort((a, b) => b[1] - a[1]);
4624
+ let n = 0;
4625
+ for (const { j, w } of sets) {
4626
+ for (let k = 0;k < 4; k++) {
4627
+ const p = pairs[n++];
4628
+ jv[k] = p ? p[0] : 0;
4629
+ wv[k] = p ? p[1] : 0;
4630
+ }
4631
+ j.setElement(v, jv);
4632
+ w.setElement(v, wv);
4633
+ }
4634
+ }
4635
+ }
4636
+ lodNode.setSkin(newSkin);
4637
+ return { before: joints.length, after: kept.length };
4638
+ };
4639
+ var init_lod = __esm(() => {
4640
+ LOD_JOINT_MASK = /finger|thumb|index|middle|ring|pinky|toe|twist|roll|leaf|_end$/i;
4641
+ LOD_NAME = /_LOD\d+$/;
4642
+ });
4643
+
4314
4644
  // src/doctor/doctor.ts
4315
4645
  var exports_doctor = {};
4316
4646
  __export(exports_doctor, {
@@ -4358,7 +4688,8 @@ async function analyzeDoc(doc, opts = {}) {
4358
4688
  const scaleInfo = measureScale(doc);
4359
4689
  const scalePlan = planScale(scaleInfo, opts);
4360
4690
  const dupNames = duplicateNodeNames(doc);
4361
- return { skinPlans, texPlans, ktx2Plans, metallicWarn, shadingPlan, lightmapPlan, scaleInfo, scalePlan, conflicts, target, dupNames };
4691
+ const lodPlan = planLod(doc, opts);
4692
+ return { skinPlans, texPlans, ktx2Plans, metallicWarn, shadingPlan, lightmapPlan, scaleInfo, scalePlan, lodPlan, conflicts, target, dupNames };
4362
4693
  }
4363
4694
  async function dumpTextures(doc, dir, log = console.log) {
4364
4695
  if (!existsSync8(dir))
@@ -4510,6 +4841,7 @@ async function applyPlan(doc, plan, opts = {}) {
4510
4841
  }
4511
4842
  const shading = applyShading(doc, plan.shadingPlan);
4512
4843
  const lightmapUv = plan.lightmapPlan.enabled ? await applyLightmapUv(doc, plan.lightmapPlan) : null;
4844
+ const lod = plan.lodPlan ? await applyLod(doc, plan.lodPlan) : null;
4513
4845
  let ktx2Encoded = 0;
4514
4846
  if (opts.ktx2) {
4515
4847
  const toktx = opts.toktx ?? findToktx();
@@ -4533,7 +4865,7 @@ async function applyPlan(doc, plan, opts = {}) {
4533
4865
  ext.dispose();
4534
4866
  }
4535
4867
  }
4536
- return { metallicZeroed, ktx2Encoded, scaled: plan.scalePlan?.factor ?? 1, unlit: shading.unlit, emissiveCleared: shading.emissiveCleared, lightmapUv };
4868
+ return { metallicZeroed, ktx2Encoded, scaled: plan.scalePlan?.factor ?? 1, unlit: shading.unlit, emissiveCleared: shading.emissiveCleared, lightmapUv, lod };
4537
4869
  }
4538
4870
  function outputFor(input, oVal, batch, ktx2) {
4539
4871
  const derived = input.replace(/\.glb$/i, ktx2 ? ".ktx2.glb" : ".fixed.glb");
@@ -4556,6 +4888,7 @@ var init_doctor = __esm(() => {
4556
4888
  init_normalizeScale();
4557
4889
  init_shading();
4558
4890
  init_lightmapUv2();
4891
+ init_lod();
4559
4892
  init_mergeSkins();
4560
4893
  init_normalizeScale();
4561
4894
  EXT_BY_MIME = { "image/png": "png", "image/jpeg": "jpg", "image/webp": "webp", "image/ktx2": "ktx2" };
@@ -5001,7 +5334,26 @@ function retarget(target, donorIn, map, rootBone, opts = {}) {
5001
5334
  if (rootBone !== null && rootIdx < 0)
5002
5335
  warn(`target has no root bone "${rootBone}" — no root motion`);
5003
5336
  const donorRoot = rootIdx >= 0 ? byTarget.get(rootIdx) : undefined;
5337
+ const rootTranslateOnly = rootIdx >= 0 && donorRoot !== undefined && pairs.some((p) => p.d !== rootIdx && p.s === donorRoot);
5004
5338
  const legs = opts.ik === "none" ? [] : detectLegs(target, turned, byTarget, target.bind.worldPos, opts.legs, warn);
5339
+ const hum = opts.humanoid;
5340
+ const humIdx = (av, k) => {
5341
+ const n = av.bones[k];
5342
+ return n === undefined ? undefined : target.index.get(n);
5343
+ };
5344
+ const hipsIdx = hum ? humIdx(hum.target, "Hips") ?? -1 : -1;
5345
+ if (hum)
5346
+ for (const leg of legs)
5347
+ for (const s of ["Left", "Right"]) {
5348
+ const foot = humIdx(hum.target, `${s}Foot`), toes = humIdx(hum.target, `${s}Toes`);
5349
+ if (leg.effector === foot && toes !== undefined && byTarget.has(toes) && target.parents[toes] === foot) {
5350
+ leg.contact = toes;
5351
+ leg.donorContact = byTarget.get(toes);
5352
+ const tf = len2(sub2(target.bind.worldPos[toes], target.bind.worldPos[foot])), df = len2(sub2(turned.bind.worldPos[leg.donorContact], turned.bind.worldPos[leg.donorEffector]));
5353
+ leg.contactRatio = tf > 0 && df > 0 ? tf / df : 1;
5354
+ }
5355
+ }
5356
+ const donorReach = (leg, pos) => add(scl2(sub2(pos[leg.donorEffector], pos[leg.donorHip]), leg.ratio), scl2(sub2(pos[leg.donorContact], pos[leg.donorEffector]), leg.contactRatio));
5005
5357
  const wanted = opts.clips?.length ? new Set(opts.clips) : null;
5006
5358
  const clips = turned.clips.filter((c) => !wanted || wanted.has(c.name));
5007
5359
  let donorGround = 0;
@@ -5013,7 +5365,7 @@ function retarget(target, donorIn, map, rootBone, opts = {}) {
5013
5365
  for (let f = 0;f < frames; f++) {
5014
5366
  const dw = worldOf(turned, clip.sample(f / (frames - 1) * clip.duration));
5015
5367
  for (const l of legs)
5016
- l.baseDonor = Math.min(l.baseDonor, dw.pos[l.donorEffector][1]);
5368
+ l.baseDonor = Math.min(l.baseDonor, dw.pos[l.donorContact][1]);
5017
5369
  }
5018
5370
  }
5019
5371
  donorGround = Math.min(...legs.map((l) => l.baseDonor));
@@ -5038,8 +5390,8 @@ function retarget(target, donorIn, map, rootBone, opts = {}) {
5038
5390
  })());
5039
5391
  if (kid === undefined || !byTarget.has(kid) || !byTarget.has(i))
5040
5392
  continue;
5041
- const ut = unit(sub(target.bind.worldPos[kid], target.bind.worldPos[i]));
5042
- const us = unit(sub(turned.bind.worldPos[byTarget.get(kid)], turned.bind.worldPos[byTarget.get(i)]));
5393
+ const ut = unit(sub2(target.bind.worldPos[kid], target.bind.worldPos[i]));
5394
+ const us = unit(sub2(turned.bind.worldPos[byTarget.get(kid)], turned.bind.worldPos[byTarget.get(i)]));
5043
5395
  if (!ut || !us)
5044
5396
  continue;
5045
5397
  legDisagreement += Math.acos(Math.max(-1, Math.min(1, ut[0] * us[0] + ut[1] * us[1] + ut[2] * us[2]))) * DEG;
@@ -5048,15 +5400,54 @@ function retarget(target, donorIn, map, rootBone, opts = {}) {
5048
5400
  if (n)
5049
5401
  legDisagreement /= n;
5050
5402
  }
5051
- const donorRefMode = opts.donorRef ?? (legs.length && legDisagreement > 12 ? "auto" : "bind");
5403
+ const donorRefMode = opts.donorRef ?? (opts.humanoid ? "bind" : legs.length && legDisagreement > 12 ? "auto" : "bind");
5052
5404
  const { rig: donor, from: donorRefFrom } = donorReference(turned, donorRefMode, fps);
5053
5405
  for (const l of legs)
5054
- l.baseDonor = Math.min(l.baseDonor, donor.bind.worldPos[l.donorEffector][1]);
5406
+ l.baseDonor = opts.humanoid ? donor.bind.worldPos[l.donorContact][1] : Math.min(l.baseDonor, donor.bind.worldPos[l.donorContact][1]);
5055
5407
  if (legs.length)
5056
5408
  donorGround = Math.min(...legs.map((l) => l.baseDonor));
5057
5409
  for (const l of legs)
5058
- l.stanceDonor = sub(donor.bind.worldPos[l.donorEffector], donor.bind.worldPos[l.donorHip]);
5410
+ l.stanceDonor = donorReach(l, donor.bind.worldPos);
5059
5411
  const policies = { ...opts.alignTo ?? {}, ...opts.alignBones ?? {} };
5412
+ const forced = new Set;
5413
+ if (hum?.target.transfer?.align)
5414
+ for (const [k, v] of Object.entries(hum.target.transfer.align)) {
5415
+ const n = hum.target.bones[k];
5416
+ if (!n || n in policies)
5417
+ continue;
5418
+ const i = target.index.get(n);
5419
+ if (i === undefined || !byTarget.has(i))
5420
+ continue;
5421
+ if (v === "keep")
5422
+ policies[n] = "keep";
5423
+ else
5424
+ forced.add(i);
5425
+ }
5426
+ if (hum) {
5427
+ const T = hum.target;
5428
+ const set = (k, pol) => {
5429
+ const n = T.bones[k];
5430
+ if (n !== undefined && !(n in policies) && target.index.has(n) && byTarget.has(target.index.get(n)))
5431
+ policies[n] = pol;
5432
+ };
5433
+ for (const k of ["Hips", "Spine", "Chest", "UpperChest", "Neck", "Head", "Jaw", "LeftEye", "RightEye"])
5434
+ set(k, "keep");
5435
+ const refTo = (k, ...tos) => {
5436
+ const names = tos.map((t) => T.bones[t]).filter((n) => !!n);
5437
+ set(k, names.length ? names : "direction");
5438
+ };
5439
+ for (const s of ["Left", "Right"]) {
5440
+ refTo(`${s}Shoulder`, `${s}UpperArm`);
5441
+ refTo(`${s}UpperArm`, `${s}LowerArm`);
5442
+ refTo(`${s}LowerArm`, `${s}Hand`);
5443
+ refTo(`${s}Hand`, `${s} Index Proximal`, `${s} Middle Proximal`, `${s} Ring Proximal`, `${s} Little Proximal`);
5444
+ for (const f of ["Thumb", "Index", "Middle", "Ring", "Little"]) {
5445
+ refTo(`${s} ${f} Proximal`, `${s} ${f} Intermediate`);
5446
+ refTo(`${s} ${f} Intermediate`, `${s} ${f} Distal`);
5447
+ set(`${s} ${f} Distal`, "direction");
5448
+ }
5449
+ }
5450
+ }
5060
5451
  let stance = legs.length > 0;
5061
5452
  if (legs.length) {
5062
5453
  let lo = Infinity, hi = -Infinity;
@@ -5066,10 +5457,10 @@ function retarget(target, donorIn, map, rootBone, opts = {}) {
5066
5457
  hi = Math.max(hi, target.bind.worldPos[i][1]);
5067
5458
  }
5068
5459
  const height = hi - lo || 1;
5069
- const footYs = legs.map((l) => target.bind.worldPos[l.effector][1]);
5460
+ const footYs = legs.map((l) => target.bind.worldPos[l.contact][1]);
5070
5461
  const lowestFoot = Math.min(...footYs);
5071
5462
  for (const leg of legs) {
5072
- const hipY = target.bind.worldPos[leg.chain[0]][1], footY = target.bind.worldPos[leg.effector][1];
5463
+ const hipY = target.bind.worldPos[leg.chain[0]][1], footY = target.bind.worldPos[leg.contact][1];
5073
5464
  const L = leg.lengths.reduce((a, b) => a + b, 0);
5074
5465
  if ((hipY - footY) / L < 0.5)
5075
5466
  stance = false;
@@ -5090,61 +5481,166 @@ function retarget(target, donorIn, map, rootBone, opts = {}) {
5090
5481
  if (!(name in policies))
5091
5482
  policies[name] = "direction";
5092
5483
  }
5093
- const { ref: ref2, A, log: alignLog, kept } = alignReference(target, donor, byTarget, rootIdx, opts.align ?? "bone-direction", policies);
5484
+ for (const i of forced)
5485
+ if (policies[target.names[i]] === "keep")
5486
+ policies[target.names[i]] = "direction";
5487
+ const { ref: ref2, A, log: alignLog, kept, under: alignUnder } = alignReference(target, donor, byTarget, rootIdx, opts.align ?? "bone-direction", policies, hum ? 8 : 0, forced);
5488
+ if (opts.ik === "legs+hands" && hum) {
5489
+ const handNames = ["LeftHand", "RightHand"].map((k) => hum.target.bones[k]).filter((n) => !!n && byTarget.has(target.index.get(n)));
5490
+ for (const h of detectLegs(target, turned, byTarget, target.bind.worldPos, handNames, warn, 2)) {
5491
+ h.isFoot = false;
5492
+ h.baseDonor = donor.bind.worldPos[h.donorContact][1];
5493
+ h.stanceDonor = donorReach(h, donor.bind.worldPos);
5494
+ legs.push(h);
5495
+ }
5496
+ }
5094
5497
  const bridges = findBridges(target, byTarget);
5498
+ if (hum) {
5499
+ const hands = new Set([humIdx(hum.target, "LeftHand"), humIdx(hum.target, "RightHand")].filter((i) => i !== undefined));
5500
+ for (const b of bridges.values())
5501
+ if (hands.has(b.above))
5502
+ b.u = 0;
5503
+ }
5095
5504
  for (const [i, b] of bridges) {
5096
5505
  const a = qslerp(A[b.above], A[b.below], b.u);
5097
5506
  A[i] = a;
5098
5507
  ref2[i] = qnorm(qmul(a, target.bind.worldRot[i]));
5099
5508
  }
5509
+ const rollLog = [];
5510
+ if (hum && (opts.align ?? "bone-direction") !== "none") {
5511
+ const pt = posesFrom(target, ref2);
5512
+ const { donor: D, target: T } = hum;
5513
+ const dIdx = (k) => {
5514
+ const n = D.bones[k];
5515
+ return n === undefined ? undefined : donor.index.get(n);
5516
+ };
5517
+ const children2 = childrenOf(target);
5518
+ for (const s of ["Left", "Right"]) {
5519
+ const tLower = humIdx(T, `${s}LowerArm`), tHand = humIdx(T, `${s}Hand`), dLower = dIdx(`${s}LowerArm`), dHand = dIdx(`${s}Hand`);
5520
+ if (tLower === undefined || tHand === undefined || dLower === undefined || dHand === undefined)
5521
+ continue;
5522
+ const axisT = unit(sub2(pt[tHand], pt[tLower])), axisD = unit(sub2(donor.bind.worldPos[dHand], donor.bind.worldPos[dLower]));
5523
+ if (!axisT || !axisD)
5524
+ continue;
5525
+ const line = (av, pos, idx, hand) => {
5526
+ const i = idx(`${s} Index Proximal`), l = idx(`${s} Little Proximal`), th = idx(`${s} Thumb Proximal`);
5527
+ if (i !== undefined && l !== undefined)
5528
+ return { v: sub2(pos[i], pos[l]), via: "knuckle line" };
5529
+ if (th !== undefined)
5530
+ return { v: sub2(pos[th], pos[hand]), via: "thumb base" };
5531
+ return null;
5532
+ };
5533
+ const kt = line(T, pt, (k) => humIdx(T, k), tHand), kd = line(D, donor.bind.worldPos, dIdx, dHand);
5534
+ if (!kt || !kd) {
5535
+ warn(`${s.toLowerCase()} arm: no fingers on the ${kt ? "donor" : "target"} — palm roll not corrected`);
5536
+ continue;
5537
+ }
5538
+ const perp = (v, ax) => {
5539
+ const d = v[0] * ax[0] + v[1] * ax[1] + v[2] * ax[2];
5540
+ return unit([v[0] - ax[0] * d, v[1] - ax[1] * d, v[2] - ax[2] * d]);
5541
+ };
5542
+ const a = perp(kt.v, axisD), b = perp(kd.v, axisD);
5543
+ if (!a || !b)
5544
+ continue;
5545
+ const cross2 = [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
5546
+ const ang = Math.atan2(cross2[0] * axisD[0] + cross2[1] * axisD[1] + cross2[2] * axisD[2], a[0] * b[0] + a[1] * b[1] + a[2] * b[2]) + (hum.target.transfer?.roll?.[s] ?? 0) / DEG;
5547
+ const deg = Math.abs(ang) * DEG;
5548
+ if (deg < 1) {
5549
+ rollLog.push({ side: s, degrees: 0, via: kt.via });
5550
+ continue;
5551
+ }
5552
+ const R = qnorm([axisD[0] * Math.sin(ang / 2), axisD[1] * Math.sin(ang / 2), axisD[2] * Math.sin(ang / 2), Math.cos(ang / 2)]);
5553
+ const stack = [tLower];
5554
+ while (stack.length) {
5555
+ const i = stack.pop();
5556
+ ref2[i] = qnorm(qmul(R, ref2[i]));
5557
+ A[i] = qnorm(qmul(ref2[i], qinv(target.bind.worldRot[i])));
5558
+ stack.push(...children2[i]);
5559
+ }
5560
+ rollLog.push({ side: s, degrees: deg, via: `${kt.via} vs ${kd.via}` });
5561
+ }
5562
+ }
5100
5563
  const refPos = posesFrom(target, ref2);
5101
5564
  const skinned = target.bind.fromSkin.map((v, i) => v ? i : -1).filter((i) => i >= 0);
5102
5565
  const lowestBind = Math.min(...skinned.map((i) => target.bind.worldPos[i][1]));
5566
+ const donorChildren = childrenOf(donor);
5567
+ const donorSkinned = donor.bind.fromSkin.some(Boolean) ? donor.bind.fromSkin.map((v, i) => v ? i : -1).filter((i) => i >= 0) : donor.names.map((_, i) => i);
5568
+ const donorLowestBind = Math.min(...donorSkinned.map((i) => donor.bind.worldPos[i][1]));
5103
5569
  const ground = legs.length ? { target: lowestBind, donor: donorGround } : null;
5104
5570
  const feet = legs.filter((l) => l.isFoot);
5105
- const lowestOf = feet.length ? feet.map((l) => l.effector) : skinned;
5106
- let groundOffset = lowestBind - Math.min(...lowestOf.map((i) => refPos[i][1]));
5107
- for (const leg of feet) {
5108
- const hipY = refPos[leg.chain[0]][1] + groundOffset;
5109
- const reach = 0.98 * leg.lengths.reduce((a, b) => a + b, 0);
5110
- if (hipY - lowestBind > reach)
5111
- groundOffset -= hipY - lowestBind - reach;
5112
- }
5571
+ const restAbove = (leg) => stance ? target.bind.worldPos[leg.contact][1] - lowestBind : 0;
5572
+ let groundOffset = feet.length ? lowestBind - Math.min(...feet.map((l) => refPos[l.contact][1] - restAbove(l))) : lowestBind - Math.min(...skinned.map((i) => refPos[i][1]));
5573
+ if (!stance)
5574
+ for (const leg of feet) {
5575
+ const hipY = refPos[leg.chain[0]][1] + groundOffset;
5576
+ const reach = 0.98 * leg.lengths.reduce((a, b) => a + b, 0);
5577
+ const floor = lowestBind + restAbove(leg);
5578
+ if (hipY - floor > reach)
5579
+ groundOffset -= hipY - floor - reach;
5580
+ }
5113
5581
  const refPosGrounded = refPos.map((p) => [p[0], p[1] + groundOffset, p[2]]);
5114
5582
  const localOff = target.names.map((_, i) => {
5115
5583
  const p = target.parents[i];
5116
5584
  if (p < 0)
5117
5585
  return [0, 0, 0];
5118
- return qrot(qinv(target.bind.worldRot[p]), sub(target.bind.worldPos[i], target.bind.worldPos[p]));
5586
+ return qrot(qinv(target.bind.worldRot[p]), sub2(target.bind.worldPos[i], target.bind.worldPos[p]));
5119
5587
  });
5120
5588
  const children = childrenOf(target);
5121
- const placeLeg = (leg, goal, rot, pos) => {
5589
+ const donorBend = (leg, dpos) => {
5590
+ const hip = dpos[leg.donorHip], end = dpos[leg.donorEffector];
5591
+ const axis = unit(sub2(end, hip));
5592
+ if (!axis)
5593
+ return null;
5594
+ const acc = [0, 0, 0];
5595
+ let n = 0;
5596
+ for (let k = 1;k < leg.chain.length; k++) {
5597
+ const d = byTarget.get(leg.chain[k]);
5598
+ if (d === undefined)
5599
+ continue;
5600
+ const v = sub2(dpos[d], hip), t = v[0] * axis[0] + v[1] * axis[1] + v[2] * axis[2];
5601
+ acc[0] += v[0] - axis[0] * t;
5602
+ acc[1] += v[1] - axis[1] * t;
5603
+ acc[2] += v[2] - axis[2] * t;
5604
+ n++;
5605
+ }
5606
+ const dir = n ? unit(acc) : null;
5607
+ return dir ? { dir, off: len2(acc) / n / donorLegLen(leg) } : null;
5608
+ };
5609
+ const placeLeg = (leg, goal, rot, pos, bend = null) => {
5122
5610
  const full = [...leg.chain, leg.effector];
5123
5611
  const hip = pos[leg.chain[0]];
5124
5612
  const pts = full.map((i) => [...pos[i]]);
5125
5613
  const total = leg.lengths.reduce((a, b) => a + b, 0);
5126
- const short = len(sub(goal, hip)) - total;
5127
- const axis = unit(sub(pts[pts.length - 1], hip));
5614
+ const short = len2(sub2(goal, hip)) - total;
5615
+ const axis = unit(sub2(pts[pts.length - 1], hip));
5128
5616
  if (axis && short < 0) {
5129
- let flat = true;
5130
- for (let k = 1;k < pts.length - 1 && flat; k++) {
5131
- const v = sub(pts[k], hip), d = v[0] * axis[0] + v[1] * axis[1] + v[2] * axis[2];
5132
- if (len([v[0] - axis[0] * d, v[1] - axis[1] * d, v[2] - axis[2] * d]) > 0.0001 * total)
5133
- flat = false;
5134
- }
5135
- if (flat) {
5136
- const hipTurn = qmul(rot[leg.chain[0]], qinv(target.bind.worldRot[leg.chain[0]]));
5137
- const nudge = leg.pole ? qrot(hipTurn, leg.pole) : [0, 0, 1];
5138
- const dist = len(sub(goal, hip));
5139
- const sag = Math.max(0.001 * total, 0.5 * Math.sqrt(Math.max(0, total * total - dist * dist)));
5140
- for (let k = 1;k < pts.length - 1; k++)
5141
- pts[k] = add(pts[k], scl(nudge, sag));
5617
+ const hipTurn = qmul(rot[leg.chain[0]], qinv(target.bind.worldRot[leg.chain[0]]));
5618
+ const pole = leg.pole ? qrot(hipTurn, leg.pole) : [0, 0, 1];
5619
+ const acc = [0, 0, 0];
5620
+ for (let k = 1;k < pts.length - 1; k++) {
5621
+ const v = sub2(pts[k], hip), d = v[0] * axis[0] + v[1] * axis[1] + v[2] * axis[2];
5622
+ acc[0] += v[0] - axis[0] * d;
5623
+ acc[1] += v[1] - axis[1] * d;
5624
+ acc[2] += v[2] - axis[2] * d;
5625
+ }
5626
+ const off = bend ? bend.off * total : len2(acc) / (pts.length - 2);
5627
+ const cur = bend ? bend.dir : unit(acc);
5628
+ const against = cur ? cur[0] * pole[0] + cur[1] * pole[1] + cur[2] * pole[2] < 0 : false;
5629
+ const w = cur && !against ? Math.min(1, off / (BEND_TRUST * total)) : 0;
5630
+ if (w < 1) {
5631
+ const dir = unit(add(scl2(pole, 1 - w), scl2(cur ?? pole, w))) ?? pole;
5632
+ const dist = len2(sub2(goal, hip));
5633
+ const sag = Math.max(off, 0.001 * total, 0.5 * Math.sqrt(Math.max(0, total * total - dist * dist)));
5634
+ for (let k = 1;k < pts.length - 1; k++) {
5635
+ const v = sub2(pts[k], hip), d = v[0] * axis[0] + v[1] * axis[1] + v[2] * axis[2];
5636
+ pts[k] = add(add(hip, scl2(axis, d)), scl2(dir, sag));
5637
+ }
5142
5638
  }
5143
5639
  }
5144
5640
  fabrik(pts, leg.lengths, goal);
5145
5641
  for (let k = 0;k < leg.chain.length; k++) {
5146
5642
  const i = leg.chain[k], next = full[k + 1];
5147
- const cur = unit(qrot(rot[i], localOff[next])), want = unit(sub(pts[k + 1], pos[i]));
5643
+ const cur = unit(qrot(rot[i], localOff[next])), want = unit(sub2(pts[k + 1], pos[i]));
5148
5644
  if (cur && want)
5149
5645
  rot[i] = qnorm(qmul(arc(cur, want), rot[i]));
5150
5646
  pos[next] = add(pos[i], qrot(rot[i], localOff[next]));
@@ -5160,17 +5656,70 @@ function retarget(target, donorIn, map, rootBone, opts = {}) {
5160
5656
  return short;
5161
5657
  };
5162
5658
  for (const leg of feet) {
5163
- const foot = refPosGrounded[leg.effector];
5164
- if (Math.abs(foot[1] - lowestBind) < 0.000001)
5659
+ const c = refPosGrounded[leg.contact], floor = lowestBind + restAbove(leg);
5660
+ if (Math.abs(c[1] - floor) < 0.000001)
5165
5661
  continue;
5166
- placeLeg(leg, [foot[0], lowestBind, foot[2]], ref2, refPosGrounded);
5662
+ const e = refPosGrounded[leg.effector];
5663
+ placeLeg(leg, [e[0], e[1] + floor - c[1], e[2]], ref2, refPosGrounded);
5167
5664
  }
5168
5665
  for (const leg of legs) {
5169
- leg.baseTarget = refPosGrounded[leg.effector][1];
5170
- leg.stanceTarget = sub(refPosGrounded[leg.effector], refPosGrounded[leg.chain[0]]);
5666
+ leg.baseTarget = refPosGrounded[leg.contact][1];
5667
+ leg.stanceTarget = sub2(refPosGrounded[leg.contact], refPosGrounded[leg.chain[0]]);
5668
+ leg.refReach = len2(sub2(refPosGrounded[leg.effector], refPosGrounded[leg.chain[0]]));
5171
5669
  }
5172
5670
  for (const i of byTarget.keys())
5173
5671
  A[i] = qnorm(qmul(ref2[i], qinv(target.bind.worldRot[i])));
5672
+ const twists = [];
5673
+ const twistFraction = opts.twist === false ? 0 : opts.twist ?? 0.5;
5674
+ if (hum && twistFraction > 0) {
5675
+ const NEXT = {
5676
+ LeftUpperArm: "LeftLowerArm",
5677
+ LeftLowerArm: "LeftHand",
5678
+ RightUpperArm: "RightLowerArm",
5679
+ RightLowerArm: "RightHand",
5680
+ LeftUpperLeg: "LeftLowerLeg",
5681
+ LeftLowerLeg: "LeftFoot",
5682
+ RightUpperLeg: "RightLowerLeg",
5683
+ RightLowerLeg: "RightFoot"
5684
+ };
5685
+ for (const [name, of] of Object.entries(hum.target.twist)) {
5686
+ const h = target.index.get(name), p = humIdx(hum.target, of), nxt = NEXT[of], c = nxt ? humIdx(hum.target, nxt) : undefined;
5687
+ if (h === undefined || p === undefined || c === undefined || target.parents[h] !== p || !byTarget.has(p) || !byTarget.has(c))
5688
+ continue;
5689
+ let g = target.parents[p];
5690
+ while (g >= 0 && !byTarget.has(g))
5691
+ g = target.parents[g];
5692
+ if (g < 0)
5693
+ continue;
5694
+ const axis = unit(sub2(target.bind.worldPos[c], target.bind.worldPos[p]));
5695
+ if (!axis)
5696
+ continue;
5697
+ const along = sub2(target.bind.worldPos[h], target.bind.worldPos[p]);
5698
+ const t = Math.max(0, Math.min(1, (along[0] * axis[0] + along[1] * axis[1] + along[2] * axis[2]) / len2(sub2(target.bind.worldPos[c], target.bind.worldPos[p]))));
5699
+ twists.push({ bone: h, parent: p, grand: g, child: c, t, axisLocal: qrot(qinv(target.bind.worldRot[p]), axis), name, of });
5700
+ }
5701
+ }
5702
+ const separateRoot = rootIdx >= 0 && hipsIdx >= 0 && hipsIdx !== rootIdx && donorRoot !== undefined;
5703
+ const rootMotion = (opts.rootMotion ?? "auto") === "auto" ? separateRoot ? "root" : "hips" : opts.rootMotion;
5704
+ const donorHipsForYaw = hum ? (() => {
5705
+ const n = hum.donor.bones.Hips;
5706
+ return n === undefined ? undefined : donor.index.get(n);
5707
+ })() : undefined;
5708
+ const rootRotation = (opts.rootRotation ?? "none") === "yaw" && rootTranslateOnly && donorHipsForYaw !== undefined ? "yaw" : "none";
5709
+ if ((opts.rootRotation ?? "none") === "yaw" && rootRotation === "none")
5710
+ warn(`rootRotation 'yaw' needs a target root above the hips mapped to the donor's hips${hum ? "" : " (humanoid avatars)"} — the root keeps its bind heading`);
5711
+ const hipsHeading = (dwRot) => {
5712
+ const q = qmul(dwRot[donorHipsForYaw], qinv(donor.bind.worldRot[donorHipsForYaw]));
5713
+ const f = qrot(q, [0, 0, 1]);
5714
+ return Math.atan2(f[0], f[2]);
5715
+ };
5716
+ const wrapRad = (a) => Math.atan2(Math.sin(a), Math.cos(a));
5717
+ const donorHips = hum ? (() => {
5718
+ const n = hum.donor.bones.Hips;
5719
+ return n === undefined ? undefined : donor.index.get(n);
5720
+ })() : undefined;
5721
+ const donorBob = separateRoot && donorHips !== undefined && donorHips !== donorRoot && donor.parents[donorHips] >= 0;
5722
+ const hipsParent = hipsIdx >= 0 ? target.parents[hipsIdx] : -1;
5174
5723
  let rootScaleFrom = "none";
5175
5724
  const rootScale = (() => {
5176
5725
  if (opts.rootScale !== undefined) {
@@ -5198,20 +5747,32 @@ function retarget(target, donorIn, map, rootBone, opts = {}) {
5198
5747
  legDisagreement,
5199
5748
  stance,
5200
5749
  align: alignLog,
5750
+ underThreshold: alignUnder,
5201
5751
  kept,
5202
5752
  bridges: [...bridges.keys()].map((i) => target.names[i]),
5203
5753
  groundOffset,
5204
5754
  ground,
5205
- legs: legs.map((l) => ({ effector: l.name, donorEffector: donor.names[l.donorEffector], chain: l.chain.map((i) => target.names[i]), ratio: l.ratio, shortfall: 0 })),
5755
+ legs: legs.map((l) => ({ effector: l.name, donorEffector: donor.names[l.donorEffector], contact: target.names[l.contact], chain: l.chain.map((i) => target.names[i]), ratio: l.ratio, shortfall: 0, isFoot: l.isFoot, planted: 0 })),
5206
5756
  clips: [],
5207
5757
  mapped: pairs.map((p) => ({ target: p.target, donor: p.donor })),
5208
5758
  unmappedTarget: target.names.filter((n, i) => !byTarget.has(i) && !bridges.has(i) && target.nodes[i].getMesh() === null),
5209
5759
  rootScale,
5210
5760
  rootScaleFrom,
5211
- root: rootIdx >= 0 && donorRoot !== undefined ? { target: target.names[rootIdx], donor: donor.names[donorRoot] } : null
5761
+ root: rootIdx >= 0 && donorRoot !== undefined ? { target: target.names[rootIdx], donor: donor.names[donorRoot] } : null,
5762
+ rootTranslateOnly,
5763
+ roll: rollLog,
5764
+ rootMotion,
5765
+ rootRotation,
5766
+ hipsBob: separateRoot ? donorBob ? "donor" : rootMotion === "hips" ? null : "root" : null,
5767
+ twist: twists.map((t) => ({ bone: t.name, of: t.of, at: t.t }))
5212
5768
  };
5213
- const animated = new Set([...byTarget.keys(), ...bridges.keys()]);
5769
+ const animated = new Set([...byTarget.keys(), ...bridges.keys(), ...twists.map((t) => t.bone)]);
5214
5770
  const rootRefPos = rootIdx >= 0 ? refPosGrounded[rootIdx] : null;
5771
+ const plantOn = opts.plant !== false;
5772
+ const donorLegLen = (l) => len2(sub2(donor.bind.worldPos[l.donorEffector], donor.bind.worldPos[l.donorHip])) || 1;
5773
+ const PLANT_FADE = 4;
5774
+ const driven = new Set([...byTarget.keys(), ...bridges.keys()]);
5775
+ const hipsParentScale = hipsParent >= 0 ? restWorld.scale[hipsParent] : 1;
5215
5776
  for (const clip of clips) {
5216
5777
  const frames = Math.max(2, Math.round(clip.duration * fps) + 1);
5217
5778
  const channels = new Map;
@@ -5223,47 +5784,306 @@ function retarget(target, donorIn, map, rootBone, opts = {}) {
5223
5784
  }
5224
5785
  return c;
5225
5786
  };
5787
+ const events = [];
5788
+ const open = new Map;
5789
+ const mark = (kind, bone, time, value, at) => {
5790
+ const key = `${kind}:${bone}`;
5791
+ const e = open.get(key);
5792
+ if (e) {
5793
+ e.to = time;
5794
+ if (value !== undefined)
5795
+ e.value = Math.max(e.value ?? 0, value);
5796
+ } else {
5797
+ const n = { kind, bone, from: time, to: time, ...value !== undefined ? { value } : {}, ...at ? { at } : {} };
5798
+ open.set(key, n);
5799
+ events.push(n);
5800
+ }
5801
+ };
5802
+ const rootXZ = () => rootIdx >= 0 ? wpos[rootIdx] : hipsIdx >= 0 ? wpos[hipsIdx] : [0, 0, 0];
5803
+ let rootFirst = [0, 0, 0], rootLast = [0, 0, 0];
5804
+ const rootTrack = [];
5805
+ const contactTrack = new Map;
5226
5806
  const wrot = new Array(target.names.length);
5227
5807
  const wpos = new Array(target.names.length);
5228
5808
  const delta = new Array(target.names.length);
5229
- for (let f = 0;f < frames; f++) {
5230
- const time = f / (frames - 1) * clip.duration;
5231
- const donorPose = clip.sample(time);
5809
+ const timeOf = (f) => f / (frames - 1) * clip.duration;
5810
+ const yaw0 = rootRotation === "yaw" ? hipsHeading(worldOf(donor, clip.sample(0)).rot) : 0;
5811
+ const turn = rootRotation === "yaw" ? wrapRad(hipsHeading(worldOf(donor, clip.sample(clip.duration)).rot) - yaw0) * DEG : 0;
5812
+ const frameFK = (f) => {
5813
+ const donorPose = clip.sample(timeOf(f));
5232
5814
  const dw = worldOf(donor, donorPose);
5233
5815
  for (const i of byTarget.keys()) {
5234
5816
  const s = byTarget.get(i);
5235
5817
  delta[i] = qmul(dw.rot[s], qinv(donor.bind.worldRot[s]));
5236
5818
  }
5819
+ if (rootTranslateOnly) {
5820
+ if (rootRotation === "yaw") {
5821
+ const y = wrapRad(hipsHeading(dw.rot) - yaw0);
5822
+ delta[rootIdx] = [0, Math.sin(y / 2), 0, Math.cos(y / 2)];
5823
+ } else
5824
+ delta[rootIdx] = IDENT;
5825
+ }
5237
5826
  for (const [i, b] of bridges)
5238
5827
  delta[i] = qslerp(delta[b.above], delta[b.below], b.u);
5828
+ let bob = null;
5829
+ if (donorBob && donorRoot !== undefined && donorHips !== undefined) {
5830
+ const v = sub2(dw.pos[donorHips], dw.pos[donorRoot]);
5831
+ const v0 = sub2(donor.bind.worldPos[donorHips], donor.bind.worldPos[donorRoot]);
5832
+ const dRoot = qmul(dw.rot[donorRoot], qinv(donor.bind.worldRot[donorRoot]));
5833
+ bob = scl2(sub2(v, qrot(dRoot, v0)), rootScale);
5834
+ }
5239
5835
  for (const i of target.order) {
5240
5836
  const p = target.parents[i];
5241
- if (animated.has(i))
5837
+ if (driven.has(i))
5242
5838
  wrot[i] = qnorm(qmul(delta[i], ref2[i]));
5243
5839
  else
5244
5840
  wrot[i] = p >= 0 ? qnorm(qmul(wrot[p], target.rest.r[i])) : qnorm(target.rest.r[i]);
5245
5841
  if (i === rootIdx && rootRefPos && donorRoot !== undefined) {
5246
5842
  const d = dw.pos[donorRoot], r = donor.bind.worldPos[donorRoot];
5247
- wpos[i] = add(rootRefPos, scl(sub(d, r), rootScale));
5843
+ wpos[i] = add(rootRefPos, scl2(sub2(d, r), rootScale));
5248
5844
  } else if (p < 0)
5249
5845
  wpos[i] = target.rest.t[i];
5250
5846
  else
5251
5847
  wpos[i] = add(wpos[p], qrot(wrot[p], localOff[i]));
5848
+ if (i === hipsIdx && bob)
5849
+ wpos[i] = add(wpos[i], bob);
5252
5850
  }
5851
+ return { dw };
5852
+ };
5853
+ const goalOf = (leg, dw) => {
5854
+ const hip = wpos[leg.chain[0]];
5855
+ const off = sub2(wpos[leg.contact], wpos[leg.effector]);
5856
+ const a = sub2(donorReach(leg, dw.pos), off);
5857
+ const c = [0, 1, 2].map((i) => leg.stanceTarget[i] - leg.stanceDonor[i]);
5858
+ const R = 0.995 * leg.lengths.reduce((x, y) => x + y, 0);
5859
+ let k = 1;
5860
+ if (len2(add(a, c)) > R) {
5861
+ const cc = c[0] * c[0] + c[1] * c[1] + c[2] * c[2], ac = a[0] * c[0] + a[1] * c[1] + a[2] * c[2], aa = a[0] * a[0] + a[1] * a[1] + a[2] * a[2] - R * R;
5862
+ const disc = ac * ac - cc * aa;
5863
+ k = cc > 0.000000000001 && disc >= 0 ? Math.max(0, Math.min(1, (-ac + Math.sqrt(disc)) / cc)) : 0;
5864
+ }
5865
+ let effector = [0, 1, 2].map((i) => hip[i] + a[i] + c[i] * k);
5866
+ if (leg.isFoot && leg.refReach > 0) {
5867
+ const ankle = add(scl2(sub2(dw.pos[leg.donorEffector], dw.pos[leg.donorHip]), leg.ratio), c);
5868
+ const dir = unit(ankle);
5869
+ const ext = len2(sub2(dw.pos[leg.donorEffector], dw.pos[leg.donorHip])) / donorLegLen(leg);
5870
+ if (dir)
5871
+ effector = add(hip, scl2(dir, Math.min(R / 0.995, ext * leg.refReach)));
5872
+ }
5873
+ return { effector, contact: add(effector, off) };
5874
+ };
5875
+ const spans = [];
5876
+ if (plantOn && legs.some((l) => l.isFoot)) {
5877
+ const feetIdx = legs.map((l, i) => l.isFoot ? i : -1).filter((i) => i >= 0);
5878
+ const goals = feetIdx.map(() => new Array(frames));
5879
+ const rel = feetIdx.map(() => new Array(frames));
5880
+ const still = feetIdx.map(() => new Array(frames).fill(false));
5881
+ let prev = null;
5882
+ for (let f = 0;f < frames; f++) {
5883
+ const { dw } = frameFK(f);
5884
+ feetIdx.forEach((li, k) => {
5885
+ const leg = legs[li], L = donorLegLen(leg), dFoot = dw.pos[leg.donorContact];
5886
+ goals[k][f] = goalOf(leg, dw).contact;
5887
+ rel[k][f] = sub2(dFoot, dw.pos[leg.donorHip]);
5888
+ const speed = prev ? len2(sub2(dFoot, prev[k])) * fps : Infinity;
5889
+ still[k][f] = speed < 0.15 * L && dFoot[1] - leg.baseDonor < 0.06 * L;
5890
+ });
5891
+ prev = feetIdx.map((li) => dw.pos[legs[li].donorContact]);
5892
+ }
5893
+ feetIdx.forEach((li, k) => {
5894
+ const s = still[k];
5895
+ const L = donorLegLen(legs[li]);
5896
+ const cyclic = len2(sub2(rel[k][0], rel[k][frames - 1])) < 0.02 && s[frames - 1];
5897
+ if (cyclic)
5898
+ s[0] = true;
5899
+ const runs = [];
5900
+ for (let i = 0;i < frames; i++) {
5901
+ if (!s[i])
5902
+ continue;
5903
+ const last = runs[runs.length - 1];
5904
+ if (last && last.start + last.length === i)
5905
+ last.length++;
5906
+ else
5907
+ runs.push({ start: i, length: 1 });
5908
+ }
5909
+ const travel2 = sub2(goals[k][frames - 1], goals[k][0]);
5910
+ const head = runs[0], tail = runs[runs.length - 1];
5911
+ const seam = cyclic && runs.length >= 2 && head.start === 0 && tail.start + tail.length === frames;
5912
+ const meanOf = (list) => {
5913
+ const acc = [0, 0, 0];
5914
+ let n = 0;
5915
+ for (const r of list)
5916
+ for (let i = r.start;i < r.start + r.length; i++) {
5917
+ const g = add(goals[k][i], r.shift);
5918
+ acc[0] += g[0];
5919
+ acc[1] += g[1];
5920
+ acc[2] += g[2];
5921
+ n++;
5922
+ }
5923
+ return { anchor: scl2(acc, 1 / Math.max(1, n)), n };
5924
+ };
5925
+ const fits = (r, anchor) => {
5926
+ let worst = 0;
5927
+ for (let i = r.start;i < r.start + r.length; i++)
5928
+ worst = Math.max(worst, Math.hypot(goals[k][i][0] - anchor[0], goals[k][i][2] - anchor[2]));
5929
+ return worst <= 0.05 * L;
5930
+ };
5931
+ for (const r of runs) {
5932
+ if (seam && (r === head || r === tail))
5933
+ continue;
5934
+ if (r.length < 3)
5935
+ continue;
5936
+ const { anchor } = meanOf([{ ...r, shift: [0, 0, 0] }]);
5937
+ if (fits(r, anchor))
5938
+ spans.push({ leg: li, start: r.start, length: r.length, anchor });
5939
+ }
5940
+ if (seam && head.length + tail.length >= 3) {
5941
+ const { anchor } = meanOf([{ ...head, shift: [0, 0, 0] }, { ...tail, shift: scl2(travel2, -1) }]);
5942
+ const tailAnchor = add(anchor, travel2);
5943
+ if (fits(head, anchor) && fits(tail, tailAnchor)) {
5944
+ spans.push({ leg: li, start: head.start, length: head.length, anchor, openStart: true });
5945
+ spans.push({ leg: li, start: tail.start, length: tail.length, anchor: tailAnchor, openEnd: true });
5946
+ }
5947
+ }
5948
+ });
5949
+ }
5950
+ const plantAt = (li, f) => {
5951
+ for (const s of spans) {
5952
+ if (s.leg !== li)
5953
+ continue;
5954
+ const n = f - s.start;
5955
+ if (n < 0 || n >= s.length)
5956
+ continue;
5957
+ const w = Math.min(1, s.openStart ? 1 : (n + 1) / PLANT_FADE, s.openEnd ? 1 : (s.length - n) / PLANT_FADE);
5958
+ return { anchor: s.anchor, w };
5959
+ }
5960
+ return null;
5961
+ };
5962
+ for (const s of spans)
5963
+ report.legs[s.leg].planted += s.length;
5964
+ for (let f = 0;f < frames; f++) {
5965
+ const time = timeOf(f);
5966
+ const { dw } = frameFK(f);
5967
+ const active = new Set;
5968
+ if (f === 0)
5969
+ rootFirst = [...rootXZ()];
5970
+ if (f === frames - 1)
5971
+ rootLast = [...rootXZ()];
5253
5972
  if (legs.length) {
5254
5973
  for (const [li, leg] of legs.entries()) {
5255
- const dHip = dw.pos[leg.donorHip], dFoot = dw.pos[leg.donorEffector];
5256
- const hip = wpos[leg.chain[0]];
5257
- const goal = [
5258
- hip[0] + leg.stanceTarget[0] + (dFoot[0] - dHip[0] - leg.stanceDonor[0]) * leg.ratio,
5259
- leg.baseTarget + (dFoot[1] - leg.baseDonor) * leg.ratio,
5260
- hip[2] + leg.stanceTarget[2] + (dFoot[2] - dHip[2] - leg.stanceDonor[2]) * leg.ratio
5261
- ];
5262
- const short = placeLeg(leg, goal, wrot, wpos);
5974
+ const g = goalOf(leg, dw), goal = g.effector;
5975
+ const pl = plantAt(li, f);
5976
+ if (pl) {
5977
+ goal[0] += (pl.anchor[0] - g.contact[0]) * pl.w;
5978
+ goal[2] += (pl.anchor[2] - g.contact[2]) * pl.w;
5979
+ }
5980
+ const slack = len2(sub2(scl2(sub2(dw.pos[leg.donorContact], dw.pos[leg.donorEffector]), leg.contactRatio), sub2(wpos[leg.contact], wpos[leg.effector])));
5981
+ const short = placeLeg(leg, goal, wrot, wpos, donorBend(leg, dw.pos)) - slack;
5982
+ if (pl) {
5983
+ const held = target.names[leg.contact];
5984
+ const r = rootXZ(), c = wpos[leg.contact];
5985
+ mark("plant", held, time, undefined, [c[0] - r[0], c[1], c[2] - r[2]]);
5986
+ active.add(`plant:${held}`);
5987
+ }
5263
5988
  if (short > report.legs[li].shortfall)
5264
5989
  report.legs[li].shortfall = short;
5990
+ if (short > 0.005) {
5991
+ mark("reach", leg.name, time, short);
5992
+ active.add(`reach:${leg.name}`);
5993
+ }
5994
+ if (leg.isFoot) {
5995
+ const deepest = () => {
5996
+ let worst = { bone: -1, dip: 0 };
5997
+ const below = [...children[leg.effector]];
5998
+ while (below.length) {
5999
+ const c = below.pop();
6000
+ const dip = lowestBind - wpos[c][1];
6001
+ if (dip > worst.dip)
6002
+ worst = { bone: c, dip };
6003
+ below.push(...children[c]);
6004
+ }
6005
+ return worst;
6006
+ };
6007
+ const d = deepest();
6008
+ let donorDip = 0;
6009
+ {
6010
+ const stack = [...donorChildren[leg.donorEffector]];
6011
+ let low = Infinity;
6012
+ while (stack.length) {
6013
+ const c = stack.pop();
6014
+ low = Math.min(low, dw.pos[c][1]);
6015
+ stack.push(...donorChildren[c]);
6016
+ }
6017
+ if (low < Infinity)
6018
+ donorDip = Math.max(0, donorLowestBind - low) * leg.ratio;
6019
+ }
6020
+ d.dip -= donorDip;
6021
+ if (d.dip > 0.002) {
6022
+ const v = sub2(wpos[d.bone], wpos[leg.effector]), r = len2(v);
6023
+ const axis = unit(cross(v, [0, 1, 0]));
6024
+ if (axis && r > 0.0001) {
6025
+ const want = Math.min(1, (v[1] + d.dip) / r), have = v[1] / r;
6026
+ let ang = Math.asin(want) - Math.asin(Math.max(-1, Math.min(1, have)));
6027
+ ang = Math.max(-FOOT_PITCH_CAP, Math.min(FOOT_PITCH_CAP, ang));
6028
+ const tryPitch = (a) => {
6029
+ const q2 = axisAngle(axis, a);
6030
+ const p = qrot(q2, v);
6031
+ return p[1] - v[1];
6032
+ };
6033
+ if (tryPitch(ang) < tryPitch(-ang))
6034
+ ang = -ang;
6035
+ const q = axisAngle(axis, ang);
6036
+ const stack = [leg.effector];
6037
+ while (stack.length) {
6038
+ const i = stack.pop();
6039
+ wrot[i] = qnorm(qmul(q, wrot[i]));
6040
+ for (const c of children[i]) {
6041
+ wpos[c] = add(wpos[i], qrot(wrot[i], localOff[c]));
6042
+ stack.push(c);
6043
+ }
6044
+ }
6045
+ }
6046
+ }
6047
+ const left = deepest();
6048
+ if (left.dip > 0.01) {
6049
+ mark("below", target.names[left.bone], time, left.dip);
6050
+ active.add(`below:${target.names[left.bone]}`);
6051
+ }
6052
+ }
6053
+ }
6054
+ }
6055
+ for (const k of [...open.keys()])
6056
+ if (!active.has(k))
6057
+ open.delete(k);
6058
+ for (const tw of twists) {
6059
+ const aRef = qrot(ref2[tw.parent], tw.axisLocal);
6060
+ const dP = qmul(wrot[tw.parent], qinv(ref2[tw.parent])), dG = qmul(wrot[tw.grand], qinv(ref2[tw.grand])), dC = qmul(wrot[tw.child], qinv(ref2[tw.child]));
6061
+ const twistIn = twistAbout(qmul(qinv(dG), dP), aRef), twistOut = twistAbout(qmul(qinv(dP), dC), aRef);
6062
+ const ang = twistFraction * (tw.t * twistOut - (1 - tw.t) * twistIn);
6063
+ const aNow = qrot(wrot[tw.parent], tw.axisLocal);
6064
+ wrot[tw.bone] = qnorm(qmul(axisAngle(aNow, ang), qmul(wrot[tw.parent], target.rest.r[tw.bone])));
6065
+ const stack = [tw.bone];
6066
+ while (stack.length) {
6067
+ const i = stack.pop();
6068
+ for (const c of children[i]) {
6069
+ if (!driven.has(c))
6070
+ wrot[c] = qnorm(qmul(wrot[i], target.rest.r[c]));
6071
+ wpos[c] = add(wpos[i], qrot(wrot[i], localOff[c]));
6072
+ stack.push(c);
6073
+ }
5265
6074
  }
5266
6075
  }
6076
+ rootTrack.push([...rootXZ()]);
6077
+ for (const [li, leg] of legs.entries()) {
6078
+ if (!leg.isFoot)
6079
+ continue;
6080
+ let tr = contactTrack.get(li);
6081
+ if (!tr) {
6082
+ tr = [];
6083
+ contactTrack.set(li, tr);
6084
+ }
6085
+ tr.push([...wpos[leg.contact]]);
6086
+ }
5267
6087
  for (const i of animated) {
5268
6088
  const p = target.parents[i];
5269
6089
  const local = p >= 0 ? qnorm(qmul(qinv(wrot[p]), wrot[i])) : wrot[i];
@@ -5274,9 +6094,28 @@ function retarget(target, donorIn, map, rootBone, opts = {}) {
5274
6094
  }
5275
6095
  }
5276
6096
  if (rootIdx >= 0 && rootRefPos && donorRoot !== undefined) {
5277
- const rel = sub(wpos[rootIdx], parentPos);
6097
+ const travel2 = sub2(wpos[rootIdx], rootRefPos);
6098
+ let strip = [0, 0, 0], keep = [0, 0, 0];
6099
+ if (separateRoot) {
6100
+ if (rootMotion === "root") {
6101
+ strip = [0, travel2[1], 0];
6102
+ keep = strip;
6103
+ } else if (rootMotion === "none") {
6104
+ strip = travel2;
6105
+ keep = [0, travel2[1], 0];
6106
+ } else {
6107
+ strip = travel2;
6108
+ keep = travel2;
6109
+ }
6110
+ } else if (rootMotion === "none")
6111
+ strip = [travel2[0], 0, travel2[2]];
6112
+ const rel = sub2(sub2(wpos[rootIdx], strip), parentPos);
5278
6113
  const [x, y, z] = qrot(qinv(parentRot), rel);
5279
6114
  (chan(rootIdx).pos ??= []).push([x / parentScale, y / parentScale, z / parentScale]);
6115
+ if (separateRoot && hipsParent >= 0) {
6116
+ const [hx, hy, hz] = qrot(qinv(wrot[hipsParent]), add(sub2(wpos[hipsIdx], wpos[hipsParent]), keep));
6117
+ (chan(hipsIdx).pos ??= []).push([hx / hipsParentScale, hy / hipsParentScale, hz / hipsParentScale]);
6118
+ }
5280
6119
  }
5281
6120
  }
5282
6121
  for (const c of channels.values())
@@ -5296,7 +6135,54 @@ function retarget(target, donorIn, map, rootBone, opts = {}) {
5296
6135
  keys += c.times.length;
5297
6136
  }
5298
6137
  }
5299
- report.clips.push({ name: anim.getName(), duration: clip.duration, frames, keys });
6138
+ const sideOf = (li) => {
6139
+ const n = `${legs[li].name} ${target.names[legs[li].contact]}`;
6140
+ if (/left|(^|[_.\s])l([_.\s]|$)/i.test(n))
6141
+ return "l";
6142
+ if (/right|(^|[_.\s])r([_.\s]|$)/i.test(n))
6143
+ return "r";
6144
+ return null;
6145
+ };
6146
+ const contacts = { feet: { l: null, r: null }, l: [], r: [], first: null };
6147
+ const dtF = frames > 1 ? clip.duration / (frames - 1) : 1;
6148
+ for (const [li, leg] of legs.entries()) {
6149
+ if (!leg.isFoot)
6150
+ continue;
6151
+ const side = sideOf(li);
6152
+ const tr = contactTrack.get(li);
6153
+ if (!side || !tr || tr.length < 2)
6154
+ continue;
6155
+ contacts.feet[side] = target.names[leg.contact];
6156
+ const n = tr.length;
6157
+ const sp = tr.map((_, k2) => {
6158
+ const a = tr[Math.max(0, k2 - 1)], b = tr[Math.min(n - 1, k2 + 1)];
6159
+ const dt = (Math.min(n - 1, k2 + 1) - Math.max(0, k2 - 1)) * dtF;
6160
+ return Math.hypot(b[0] - a[0], b[2] - a[2]) / (dt || dtF);
6161
+ });
6162
+ const hs = tr.map((p) => p[1]);
6163
+ const lo = Math.min(...sp), hi = Math.max(...sp), hLo = Math.min(...hs), hHi = Math.max(...hs);
6164
+ const still = hi - lo < 0.05;
6165
+ const down2 = tr.map((_, k2) => still || sp[k2] < lo + 0.2 * (hi - lo) && hs[k2] < hLo + 0.5 * (hHi - hLo));
6166
+ let k = 0;
6167
+ while (k < n) {
6168
+ if (!down2[k]) {
6169
+ k++;
6170
+ continue;
6171
+ }
6172
+ const start = k;
6173
+ while (k < n && down2[k])
6174
+ k++;
6175
+ if (k - start < 2 && !still)
6176
+ continue;
6177
+ const r = rootTrack[start], c = tr[start];
6178
+ contacts[side].push({ from: start * dtF, to: (k - 1) * dtF, at: [c[0] - r[0], c[1], c[2] - r[2]] });
6179
+ }
6180
+ }
6181
+ const down = (side) => contacts[side].some((s) => s.from <= 0.000001);
6182
+ contacts.first = down("l") && down("r") ? "both" : down("l") ? "l" : down("r") ? "r" : null;
6183
+ const travel = [rootLast[0] - rootFirst[0], rootLast[2] - rootFirst[2]];
6184
+ anim.setExtras({ ...anim.getExtras(), lecodes: { contacts, travel } });
6185
+ report.clips.push({ name: anim.getName(), duration: clip.duration, frames, keys, events, turn, contacts, travel });
5300
6186
  }
5301
6187
  return report;
5302
6188
  }
@@ -5313,9 +6199,9 @@ var DEG, IDENT, analyzeFacing = (rig, boneNames) => {
5313
6199
  hi[i] = Math.max(hi[i], p[i]);
5314
6200
  }
5315
6201
  const fwd = head && tail ? [head[0] - tail[0], 0, head[2] - tail[2]] : [0, 0, 1];
5316
- const len = Math.hypot(...fwd) || 1;
6202
+ const len2 = Math.hypot(...fwd) || 1;
5317
6203
  return {
5318
- forward: [fwd[0] / len, fwd[1] / len, fwd[2] / len],
6204
+ forward: [fwd[0] / len2, fwd[1] / len2, fwd[2] / len2],
5319
6205
  up: [0, 1, 0],
5320
6206
  height: hi[1] - lo[1],
5321
6207
  length: Math.max(hi[0] - lo[0], hi[1] - lo[1], hi[2] - lo[2])
@@ -5357,8 +6243,8 @@ var DEG, IDENT, analyzeFacing = (rig, boneNames) => {
5357
6243
  ch.rot = keepR;
5358
6244
  if (keepP)
5359
6245
  ch.pos = keepP;
5360
- }, sub = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]], add = (a, b) => [a[0] + b[0], a[1] + b[1], a[2] + b[2]], scl = (a, k) => [a[0] * k, a[1] * k, a[2] * k], len = (v) => Math.hypot(v[0], v[1], v[2]), unit = (v) => {
5361
- const l = len(v);
6246
+ }, sub2 = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]], add = (a, b) => [a[0] + b[0], a[1] + b[1], a[2] + b[2]], scl2 = (a, k) => [a[0] * k, a[1] * k, a[2] * k], len2 = (v) => Math.hypot(v[0], v[1], v[2]), unit = (v) => {
6247
+ const l = len2(v);
5362
6248
  return l > 0.000000000001 ? [v[0] / l, v[1] / l, v[2] / l] : null;
5363
6249
  }, arc = (from, to) => {
5364
6250
  const d = from[0] * to[0] + from[1] * to[1] + from[2] * to[2];
@@ -5379,15 +6265,16 @@ var DEG, IDENT, analyzeFacing = (rig, boneNames) => {
5379
6265
  children[p].push(i);
5380
6266
  });
5381
6267
  return children;
5382
- }, alignReference = (target, donor, byTarget, rootIdx, align, policies) => {
6268
+ }, alignReference = (target, donor, byTarget, rootIdx, align, policies, minDeg = 0, force = new Set) => {
5383
6269
  const ref2 = target.bind.worldRot.map((q) => [...q]);
5384
6270
  const A = target.names.map(() => IDENT);
5385
6271
  const log = [];
6272
+ const under = [];
5386
6273
  const kept = [];
5387
6274
  if (align === "none") {
5388
6275
  for (const i of byTarget.keys())
5389
6276
  kept.push(target.names[i]);
5390
- return { ref: ref2, A, log, kept };
6277
+ return { ref: ref2, A, log, kept, under };
5391
6278
  }
5392
6279
  const children = childrenOf(target);
5393
6280
  const mappedChildCount = (i) => {
@@ -5440,7 +6327,7 @@ var DEG, IDENT, analyzeFacing = (rig, boneNames) => {
5440
6327
  const acc = [0, 0, 0];
5441
6328
  let n = 0;
5442
6329
  for (const to of tos) {
5443
- const u = unit(sub(to, from));
6330
+ const u = unit(sub2(to, from));
5444
6331
  if (!u)
5445
6332
  continue;
5446
6333
  acc[0] += u[0];
@@ -5474,15 +6361,24 @@ var DEG, IDENT, analyzeFacing = (rig, boneNames) => {
5474
6361
  } else if (children[i].length) {
5475
6362
  const donorKids = donor.parents.map((p, k) => [p, k]).filter(([p]) => p === s).map(([, k]) => k);
5476
6363
  if (donorKids.length) {
5477
- ut = unit(sub(target.bind.worldPos[children[i][0]], target.bind.worldPos[i]));
5478
- us = unit(sub(donor.bind.worldPos[donorKids[0]], donor.bind.worldPos[s]));
6364
+ ut = unit(sub2(target.bind.worldPos[children[i][0]], target.bind.worldPos[i]));
6365
+ us = unit(sub2(donor.bind.worldPos[donorKids[0]], donor.bind.worldPos[s]));
5479
6366
  via = `${target.names[children[i][0]]} (unmapped child)`;
5480
6367
  }
5481
6368
  }
5482
6369
  let a;
5483
- if (ut && us)
6370
+ if (ut && us) {
5484
6371
  a = arc(ut, us);
5485
- else {
6372
+ const off = 2 * Math.acos(Math.min(1, Math.abs(a[3]))) * DEG;
6373
+ if (minDeg > 0 && off < minDeg && !force.has(i)) {
6374
+ if (off > 0.5)
6375
+ under.push({ bone: target.names[i], degrees: off, via });
6376
+ a = IDENT;
6377
+ kept.push(target.names[i]);
6378
+ A[i] = a;
6379
+ continue;
6380
+ }
6381
+ } else {
5486
6382
  a = A[target.parents[i]] ?? IDENT;
5487
6383
  via = "inherited from parent";
5488
6384
  }
@@ -5493,7 +6389,8 @@ var DEG, IDENT, analyzeFacing = (rig, boneNames) => {
5493
6389
  log.push({ bone: target.names[i], degrees: deg, via });
5494
6390
  }
5495
6391
  log.sort((a, b) => b.degrees - a.degrees);
5496
- return { ref: ref2, A, log, kept };
6392
+ under.sort((a, b) => b.degrees - a.degrees);
6393
+ return { ref: ref2, A, log, kept, under };
5497
6394
  }, findBridges = (target, byTarget) => {
5498
6395
  const children = childrenOf(target);
5499
6396
  const subtreeSize = new Array(target.names.length).fill(1);
@@ -5534,7 +6431,7 @@ var DEG, IDENT, analyzeFacing = (rig, boneNames) => {
5534
6431
  out.set(i, { above: a, below: b.idx, u: up / (up + b.depth) });
5535
6432
  }
5536
6433
  return out;
5537
- }, detectLegs = (target, donor, byTarget, refPos, explicit, warn) => {
6434
+ }, BEND_TRUST = 0.05, FOOT_PITCH_CAP, cross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]], detectLegs = (target, donor, byTarget, refPos, explicit, warn, maxAbove = 4) => {
5538
6435
  const children = childrenOf(target);
5539
6436
  const mappedKids = (i) => {
5540
6437
  const out = [];
@@ -5607,11 +6504,11 @@ var DEG, IDENT, analyzeFacing = (rig, boneNames) => {
5607
6504
  const path = [];
5608
6505
  for (let b = target.parents[e];b >= 0 && b !== target.parents[hipMapped]; b = target.parents[b])
5609
6506
  path.unshift(b);
5610
- const chain = path.slice(Math.max(0, path.length - 4));
6507
+ const chain = path.slice(Math.max(0, path.length - maxAbove));
5611
6508
  const legFull = [...chain, e];
5612
6509
  const lengths = [];
5613
6510
  for (let j = 1;j < legFull.length; j++)
5614
- lengths.push(len(sub(target.bind.worldPos[legFull[j]], target.bind.worldPos[legFull[j - 1]])));
6511
+ lengths.push(len2(sub2(target.bind.worldPos[legFull[j]], target.bind.worldPos[legFull[j - 1]])));
5615
6512
  let hipRef = chain[0];
5616
6513
  while (hipRef >= 0 && !byTarget.has(hipRef))
5617
6514
  hipRef = target.parents[hipRef];
@@ -5623,7 +6520,7 @@ var DEG, IDENT, analyzeFacing = (rig, boneNames) => {
5623
6520
  dlen = 0;
5624
6521
  break;
5625
6522
  }
5626
- dlen += len(sub(donor.bind.worldPos[b], donor.bind.worldPos[p]));
6523
+ dlen += len2(sub2(donor.bind.worldPos[b], donor.bind.worldPos[p]));
5627
6524
  }
5628
6525
  const dfull = [donorHip, donorEffector];
5629
6526
  const tlen = lengths.reduce((a, b) => a + b, 0);
@@ -5632,17 +6529,17 @@ var DEG, IDENT, analyzeFacing = (rig, boneNames) => {
5632
6529
  let pole = null;
5633
6530
  {
5634
6531
  const hip = target.bind.worldPos[legFull[0]], foot = target.bind.worldPos[legFull[legFull.length - 1]];
5635
- const axis = unit(sub(foot, hip));
6532
+ const axis = unit(sub2(foot, hip));
5636
6533
  if (axis) {
5637
6534
  const acc = [0, 0, 0];
5638
6535
  for (let j = 1;j < legFull.length - 1; j++) {
5639
- const v = sub(target.bind.worldPos[legFull[j]], hip);
6536
+ const v = sub2(target.bind.worldPos[legFull[j]], hip);
5640
6537
  const d = v[0] * axis[0] + v[1] * axis[1] + v[2] * axis[2];
5641
6538
  acc[0] += v[0] - axis[0] * d;
5642
6539
  acc[1] += v[1] - axis[1] * d;
5643
6540
  acc[2] += v[2] - axis[2] * d;
5644
6541
  }
5645
- if (len(acc) > 0.001 * tlen)
6542
+ if (len2(acc) > 0.001 * tlen)
5646
6543
  pole = unit(acc);
5647
6544
  }
5648
6545
  }
@@ -5651,6 +6548,9 @@ var DEG, IDENT, analyzeFacing = (rig, boneNames) => {
5651
6548
  effector: e,
5652
6549
  donorHip: dfull[0],
5653
6550
  donorEffector: dfull[dfull.length - 1],
6551
+ contact: e,
6552
+ donorContact: dfull[dfull.length - 1],
6553
+ contactRatio: 1,
5654
6554
  lengths,
5655
6555
  pole,
5656
6556
  ratio: tlen / dlen,
@@ -5659,34 +6559,42 @@ var DEG, IDENT, analyzeFacing = (rig, boneNames) => {
5659
6559
  baseTarget: 0,
5660
6560
  baseDonor: 0,
5661
6561
  stanceTarget: [0, 0, 0],
5662
- stanceDonor: [0, 0, 0]
6562
+ stanceDonor: [0, 0, 0],
6563
+ refReach: 0
5663
6564
  });
5664
6565
  }
5665
6566
  return legs;
6567
+ }, twistAbout = (q, a) => {
6568
+ const d = q[0] * a[0] + q[1] * a[1] + q[2] * a[2];
6569
+ const ang = 2 * Math.atan2(d, q[3]);
6570
+ return ang > Math.PI ? ang - 2 * Math.PI : ang < -Math.PI ? ang + 2 * Math.PI : ang;
6571
+ }, axisAngle = (a, ang) => {
6572
+ const s = Math.sin(ang / 2);
6573
+ return [a[0] * s, a[1] * s, a[2] * s, Math.cos(ang / 2)];
5666
6574
  }, fabrik = (p, seg, goal, iterations = 48) => {
5667
6575
  const n = p.length;
5668
6576
  const root = p[0];
5669
6577
  const total = seg.reduce((a, b) => a + b, 0);
5670
- const toGoal = sub(goal, root);
5671
- const dist = len(toGoal);
6578
+ const toGoal = sub2(goal, root);
6579
+ const dist = len2(toGoal);
5672
6580
  if (dist >= total * 0.9995) {
5673
6581
  const d = unit(toGoal) ?? [0, -1, 0];
5674
6582
  for (let i = 1;i < n; i++)
5675
- p[i] = add(p[i - 1], scl(d, seg[i - 1] * 0.9995));
6583
+ p[i] = add(p[i - 1], scl2(d, seg[i - 1] * 0.9995));
5676
6584
  return;
5677
6585
  }
5678
6586
  for (let it = 0;it < iterations; it++) {
5679
6587
  p[n - 1] = goal;
5680
6588
  for (let i = n - 2;i >= 0; i--) {
5681
- const d = unit(sub(p[i], p[i + 1])) ?? [0, 1, 0];
5682
- p[i] = add(p[i + 1], scl(d, seg[i]));
6589
+ const d = unit(sub2(p[i], p[i + 1])) ?? [0, 1, 0];
6590
+ p[i] = add(p[i + 1], scl2(d, seg[i]));
5683
6591
  }
5684
6592
  p[0] = root;
5685
6593
  for (let i = 1;i < n; i++) {
5686
- const d = unit(sub(p[i], p[i - 1])) ?? [0, -1, 0];
5687
- p[i] = add(p[i - 1], scl(d, seg[i - 1]));
6594
+ const d = unit(sub2(p[i], p[i - 1])) ?? [0, -1, 0];
6595
+ p[i] = add(p[i - 1], scl2(d, seg[i - 1]));
5688
6596
  }
5689
- if (len(sub(p[n - 1], goal)) < total * 0.000001)
6597
+ if (len2(sub2(p[n - 1], goal)) < total * 0.000001)
5690
6598
  break;
5691
6599
  }
5692
6600
  }, donorReference = (donor, ref2, fps) => {
@@ -5732,7 +6640,7 @@ var DEG, IDENT, analyzeFacing = (rig, boneNames) => {
5732
6640
  pos[i] = target.bind.worldPos[i];
5733
6641
  continue;
5734
6642
  }
5735
- const off = sub(target.bind.worldPos[i], target.bind.worldPos[p]);
6643
+ const off = sub2(target.bind.worldPos[i], target.bind.worldPos[p]);
5736
6644
  const local = qrot(qinv(target.bind.worldRot[p]), off);
5737
6645
  pos[i] = add(pos[p], qrot(worldRot[p], local));
5738
6646
  }
@@ -5747,6 +6655,7 @@ var init_retarget = __esm(() => {
5747
6655
  init_rig();
5748
6656
  DEG = 180 / Math.PI;
5749
6657
  IDENT = [0, 0, 0, 1];
6658
+ FOOT_PITCH_CAP = 20 * Math.PI / 180;
5750
6659
  });
5751
6660
 
5752
6661
  // src/retarget/unityAnim.ts
@@ -5854,9 +6763,9 @@ function attachUnityClips(rig, clips) {
5854
6763
  }
5855
6764
  return res;
5856
6765
  };
5857
- const rot = bind(uc.rotation, 4), pos = bind(uc.position, 3), scl2 = bind(uc.scale, 3);
6766
+ const rot = bind(uc.rotation, 4), pos = bind(uc.position, 3), scl3 = bind(uc.scale, 3);
5858
6767
  const total = uc.rotation.length + uc.position.length + uc.scale.length;
5859
- const bound = rot.length + pos.length + scl2.length;
6768
+ const bound = rot.length + pos.length + scl3.length;
5860
6769
  const unbound = [...uc.rotation, ...uc.position, ...uc.scale].filter((c) => !rig.index.has(c.bone)).map((c) => c.bone);
5861
6770
  report.push({ name: uc.name, bound, total, unbound: [...new Set(unbound)] });
5862
6771
  const base = rig.bind.local;
@@ -5869,7 +6778,7 @@ function attachUnityClips(rig, clips) {
5869
6778
  pose.r[node] = qnorm(sample(c, time, 4));
5870
6779
  for (const { node, c } of pos)
5871
6780
  pose.t[node] = sample(c, time, 3);
5872
- for (const { node, c } of scl2)
6781
+ for (const { node, c } of scl3)
5873
6782
  pose.s[node] = sample(c, time, 3);
5874
6783
  return pose;
5875
6784
  }
@@ -5906,6 +6815,625 @@ var init_unityAnim = __esm(() => {
5906
6815
  };
5907
6816
  });
5908
6817
 
6818
+ // src/retarget/humanoid.ts
6819
+ var exports_humanoid = {};
6820
+ __export(exports_humanoid, {
6821
+ parseUnityAvatar: () => parseUnityAvatar,
6822
+ mapAvatars: () => mapAvatars,
6823
+ isHumanBone: () => isHumanBone,
6824
+ isBiped: () => isBiped,
6825
+ describeHumanoidMap: () => describeHumanoidMap,
6826
+ describeBiped: () => describeBiped,
6827
+ classifyBiped: () => classifyBiped,
6828
+ avatarToFile: () => avatarToFile,
6829
+ avatarFromUnity: () => avatarFromUnity,
6830
+ avatarFromRig: () => avatarFromRig,
6831
+ avatarFromFile: () => avatarFromFile,
6832
+ avatarFromBiped: () => avatarFromBiped,
6833
+ SIDES: () => SIDES,
6834
+ PHALANGES: () => PHALANGES,
6835
+ HUMAN_BONES: () => HUMAN_BONES,
6836
+ FINGERS: () => FINGERS
6837
+ });
6838
+ var SIDES, FINGERS, PHALANGES, BODY, LIMB, HUMAN_BONES, HUMAN_SET, isHumanBone = (s) => HUMAN_SET.has(s), parseUnityAvatar = (yaml) => {
6839
+ const bones = {};
6840
+ const unknown = [];
6841
+ const unq = (s) => s.trim().replace(/^'(.*)'$/, "$1").replace(/^"(.*)"$/, "$1");
6842
+ const re = /boneName:\s*([^\n]+)\n\s*humanName:\s*([^\n]+)/g;
6843
+ for (let m = re.exec(yaml);m; m = re.exec(yaml)) {
6844
+ const bone = unq(m[1]), human = unq(m[2]);
6845
+ if (isHumanBone(human))
6846
+ bones[human] = bone;
6847
+ else
6848
+ unknown.push(human);
6849
+ }
6850
+ const rm = /rootMotionBoneName:\s*([^\n]*)/.exec(yaml);
6851
+ const rootMotionBone = rm && unq(rm[1]) ? unq(rm[1]) : null;
6852
+ return { bones, rootMotionBone, unknown };
6853
+ }, sub3 = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]], dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2], len3 = (v) => Math.hypot(v[0], v[1], v[2]), norm = (v) => {
6854
+ const l = len3(v) || 1;
6855
+ return [v[0] / l, v[1] / l, v[2] / l];
6856
+ }, cross2 = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]], angleDeg = (a, b) => Math.acos(Math.max(-1, Math.min(1, dot(norm(a), norm(b))))) * 180 / Math.PI, TOKEN, classifyBiped = (rig) => {
6857
+ const notes = [];
6858
+ const N = rig.names.length;
6859
+ const pos = rig.bind.worldPos;
6860
+ const skinned = (i) => rig.bind.fromSkin[i];
6861
+ const children = rig.names.map(() => []);
6862
+ rig.parents.forEach((p, i) => {
6863
+ if (p >= 0)
6864
+ children[p].push(i);
6865
+ });
6866
+ const skids = (i) => {
6867
+ const out = [];
6868
+ const q = [...children[i]];
6869
+ while (q.length) {
6870
+ const c = q.shift();
6871
+ if (skinned(c))
6872
+ out.push(c);
6873
+ else
6874
+ q.push(...children[c]);
6875
+ }
6876
+ return out;
6877
+ };
6878
+ const sparent = (i) => {
6879
+ let p = rig.parents[i];
6880
+ while (p >= 0 && !skinned(p))
6881
+ p = rig.parents[p];
6882
+ return p;
6883
+ };
6884
+ const joints = rig.order.filter(skinned);
6885
+ if (joints.length < 12)
6886
+ throw new Error(`${rig.label}: ${joints.length} skinned joints — not a humanoid`);
6887
+ const subtree = (i) => {
6888
+ const out = [];
6889
+ const q = [i];
6890
+ while (q.length) {
6891
+ const j = q.shift();
6892
+ out.push(j);
6893
+ q.push(...skids(j));
6894
+ }
6895
+ return out;
6896
+ };
6897
+ const minY = (i) => Math.min(...subtree(i).map((j) => pos[j][1]));
6898
+ const maxY = (i) => Math.max(...subtree(i).map((j) => pos[j][1]));
6899
+ const extent = (i) => {
6900
+ const s = subtree(i);
6901
+ let e = 0;
6902
+ for (const j of s)
6903
+ e = Math.max(e, len3(sub3(pos[j], pos[i])));
6904
+ return e;
6905
+ };
6906
+ let lo = Infinity, hi = -Infinity;
6907
+ for (const i of joints) {
6908
+ lo = Math.min(lo, pos[i][1]);
6909
+ hi = Math.max(hi, pos[i][1]);
6910
+ }
6911
+ const height = hi - lo || 1, ground = lo;
6912
+ const up = [0, 1, 0];
6913
+ const horiz = (v) => [v[0], 0, v[2]];
6914
+ const descend = (from) => {
6915
+ const chain = [from];
6916
+ for (let guard = 0;guard < 64; guard++) {
6917
+ const ks = skids(chain[chain.length - 1]);
6918
+ if (!ks.length)
6919
+ break;
6920
+ chain.push(ks.sort((a, b) => minY(a) - minY(b))[0]);
6921
+ }
6922
+ return chain;
6923
+ };
6924
+ let hips = -1;
6925
+ let legChains = [];
6926
+ for (const h2 of joints) {
6927
+ const cands = skids(h2).map((c) => descend(c)).filter((ch) => ch.length >= 3 && minY(ch[0]) < ground + 0.12 * height && len3(horiz(sub3(pos[ch[0]], pos[h2]))) > 0.02 * height);
6928
+ let best = null, bestScore = -Infinity;
6929
+ for (let a = 0;a < cands.length; a++)
6930
+ for (let b = a + 1;b < cands.length; b++) {
6931
+ const oa = horiz(sub3(pos[cands[a][0]], pos[h2])), ob = horiz(sub3(pos[cands[b][0]], pos[h2]));
6932
+ if (dot(oa, ob) >= 0)
6933
+ continue;
6934
+ const score = cands[a].length + cands[b].length + len3(sub3(oa, ob)) / height * 10;
6935
+ if (score > bestScore) {
6936
+ bestScore = score;
6937
+ best = [cands[a], cands[b]];
6938
+ }
6939
+ }
6940
+ if (!best)
6941
+ continue;
6942
+ if (hips < 0 || pos[h2][1] < pos[hips][1]) {
6943
+ hips = h2;
6944
+ legChains = best;
6945
+ }
6946
+ }
6947
+ if (hips < 0)
6948
+ throw new Error(`${rig.label}: no pelvis with two legs reaching the ground — not a biped`);
6949
+ const legJoints = new Set(legChains.flat());
6950
+ const centreX = pos[hips];
6951
+ const handScore = (i) => {
6952
+ if (legJoints.has(i) || i === hips)
6953
+ return -1;
6954
+ const ks = skids(i).filter((c) => subtree(c).length <= 6 && extent(c) < 0.12 * height);
6955
+ if (ks.length < 3)
6956
+ return -1;
6957
+ const lat = len3(horiz(sub3(pos[i], centreX)));
6958
+ return lat > 0.12 * height ? lat : -1;
6959
+ };
6960
+ const handCands = joints.filter((i) => handScore(i) > 0);
6961
+ const ancestors = (i) => {
6962
+ const out = [];
6963
+ for (let a = i;a >= 0; a = sparent(a))
6964
+ out.push(a);
6965
+ return out;
6966
+ };
6967
+ const lca = (list) => {
6968
+ let common = ancestors(list[0]);
6969
+ for (const i of list.slice(1)) {
6970
+ const set = new Set(ancestors(i));
6971
+ common = common.filter((a) => set.has(a));
6972
+ }
6973
+ return common[0];
6974
+ };
6975
+ const footOf = (chain) => {
6976
+ const last = chain[chain.length - 1];
6977
+ if (chain.length >= 5) {
6978
+ const ball = chain[chain.length - 2], ankle = chain[chain.length - 3];
6979
+ const endSite = /(_end|end|nub|tip)$/i.test(rig.names[last]) && children[last].length === 0;
6980
+ if ((endSite || pos[ball][1] - ground < 0.03 * height) && pos[ball][1] - ground < 0.06 * height && pos[ankle][1] - ground < 0.14 * height)
6981
+ return { foot: ankle, toes: ball };
6982
+ }
6983
+ if (chain.length >= 4 && pos[last][1] - ground < 0.06 * height && pos[chain[chain.length - 2]][1] - ground < 0.14 * height)
6984
+ return { foot: chain[chain.length - 2], toes: last };
6985
+ return { foot: last, toes: null };
6986
+ };
6987
+ let forward = [0, 0, 1];
6988
+ {
6989
+ const dirs = legChains.map(footOf).filter((f) => f.toes !== null).map((f) => horiz(sub3(pos[f.toes], pos[f.foot])));
6990
+ if (dirs.length)
6991
+ forward = norm(dirs.reduce((a, d) => [a[0] + d[0], 0, a[2] + d[2]], [0, 0, 0]));
6992
+ else
6993
+ notes.push("no toe bones — facing assumed +Z");
6994
+ }
6995
+ let lateral = norm(cross2(up, forward));
6996
+ const latOf = (i) => dot(sub3(pos[i], pos[hips]), lateral);
6997
+ const handOn = (sign) => {
6998
+ const side = handCands.filter((i) => Math.sign(latOf(i)) === sign).sort((a, b) => Math.abs(latOf(b)) - Math.abs(latOf(a)));
6999
+ if (side.length)
7000
+ return side[0];
7001
+ const leaves = joints.filter((i) => !legJoints.has(i) && skids(i).length === 0 && Math.sign(latOf(i)) === sign && Math.abs(latOf(i)) > 0.15 * height && pos[i][1] > pos[hips][1]).sort((a, b) => Math.abs(latOf(b)) - Math.abs(latOf(a)));
7002
+ if (leaves.length) {
7003
+ notes.push(`no finger chains on the ${sign > 0 ? "left" : "right"} — hand = furthest lateral joint ${rig.names[leaves[0]]}`);
7004
+ return leaves[0];
7005
+ }
7006
+ return null;
7007
+ };
7008
+ const handL = handOn(1), handR = handOn(-1);
7009
+ if (handL === null || handR === null)
7010
+ throw new Error(`${rig.label}: ${handL === null ? "left" : "right"} hand not found — not a biped`);
7011
+ const upTo = (from, stop) => {
7012
+ const out = [];
7013
+ for (let a = from;a >= 0 && !stop(a); a = sparent(a))
7014
+ out.unshift(a);
7015
+ return out;
7016
+ };
7017
+ const chest = lca([handL, handR]);
7018
+ if (chest === hips || !ancestors(chest).includes(hips))
7019
+ throw new Error(`${rig.label}: arms meet at ${rig.names[chest]}, not above the pelvis — not a biped`);
7020
+ const spine = upTo(chest, (j) => j === hips);
7021
+ const spineSet = new Set(spine);
7022
+ const armOf = (hand, side) => {
7023
+ const path2 = upTo(hand, (j) => spineSet.has(j));
7024
+ const attach = sparent(path2[0]);
7025
+ const links = path2.slice(0, -1);
7026
+ if (links.length < 2)
7027
+ notes.push(`${side} arm has ${links.length} link(s) between ${rig.names[attach]} and the hand — check the map`);
7028
+ const lower = links[links.length - 1], upper = links[links.length - 2] ?? lower;
7029
+ const shoulder = links.length >= 3 ? links[0] : null;
7030
+ const extra = links.length > 3 ? links.slice(1, -2) : [];
7031
+ return { side, attach, shoulder, extra, upper, lower, hand, fingers: {}, metacarpals: [] };
7032
+ };
7033
+ const arms = [armOf(handL, "Left"), armOf(handR, "Right")];
7034
+ const armJoints = new Set(arms.flatMap((a) => [a.shoulder ?? -1, ...a.extra, a.upper, a.lower, a.hand].filter((i) => i >= 0)));
7035
+ const centred = (i) => Math.abs(latOf(i)) < 0.05 * height;
7036
+ const path = [];
7037
+ {
7038
+ let cur = chest;
7039
+ for (let guard = 0;guard < 64; guard++) {
7040
+ const ks = skids(cur).filter((c) => !armJoints.has(c) && !legJoints.has(c) && centred(c) && pos[c][1] > pos[cur][1] - 0.01 * height);
7041
+ if (!ks.length)
7042
+ break;
7043
+ cur = ks.sort((a, b) => maxY(b) - maxY(a))[0];
7044
+ path.push(cur);
7045
+ }
7046
+ }
7047
+ if (!path.length)
7048
+ throw new Error(`${rig.label}: nothing above ${rig.names[chest]} — no neck/head`);
7049
+ let h = path.length - 1;
7050
+ while (h > 0 && skids(path[h]).length < 2)
7051
+ h--;
7052
+ if (skids(path[h]).length < 2)
7053
+ h = path.length - 1;
7054
+ const named = path.findIndex((i) => TOKEN.head.test(rig.names[i]));
7055
+ if (named >= 0 && named !== h) {
7056
+ notes.push(`head: shape picked ${rig.names[path[h]]}, name says ${rig.names[path[named]]} — using the name`);
7057
+ h = named;
7058
+ }
7059
+ const head = path[h];
7060
+ const neck = path.slice(0, h);
7061
+ let eyes = null, jaw = null;
7062
+ {
7063
+ const kids = skids(head);
7064
+ const pair = kids.filter((c) => Math.abs(latOf(c) - latOf(head)) > 0.01 * height);
7065
+ const fwdOf = (c) => dot(sub3(pos[c], pos[head]), forward);
7066
+ let best = -Infinity;
7067
+ for (const a of pair)
7068
+ for (const b of pair) {
7069
+ if (latOf(a) - latOf(head) <= 0 || latOf(b) - latOf(head) >= 0)
7070
+ continue;
7071
+ if (Math.abs(pos[a][1] - pos[b][1]) > 0.03 * height)
7072
+ continue;
7073
+ const score = fwdOf(a) + fwdOf(b) + (TOKEN.eye.test(rig.names[a]) && TOKEN.eye.test(rig.names[b]) ? height : 0);
7074
+ if (score > best) {
7075
+ best = score;
7076
+ eyes = [a, b];
7077
+ }
7078
+ }
7079
+ if (eyes && best < 0.005 * height)
7080
+ eyes = null;
7081
+ const eyeSet = new Set(eyes ?? []);
7082
+ const byName = kids.find((c) => !eyeSet.has(c) && TOKEN.jaw.test(rig.names[c]));
7083
+ const j = byName ?? kids.filter((c) => !eyeSet.has(c) && Math.abs(latOf(c) - latOf(head)) < 0.03 * height && fwdOf(c) > 0.01 * height && pos[c][1] < pos[head][1] + 0.03 * height).sort((a, b) => pos[a][1] - pos[b][1])[0];
7084
+ if (j !== undefined)
7085
+ jaw = j;
7086
+ }
7087
+ const legs = legChains.map((chain) => {
7088
+ const { foot, toes } = footOf(chain);
7089
+ const body = chain.slice(0, chain.indexOf(foot));
7090
+ const side = latOf(chain[0]) > 0 ? "Left" : "Right";
7091
+ if (body.length < 2) {
7092
+ notes.push(`${side} leg has ${body.length} link(s) above the foot — check the map`);
7093
+ return { side, upper: body[0] ?? foot, extra: [], lower: body[body.length - 1] ?? foot, foot, toes, chain };
7094
+ }
7095
+ return { side, upper: body[0], extra: body.slice(1, -1), lower: body[body.length - 1], foot, toes, chain };
7096
+ }).sort((a, b) => (a.side === "Left" ? 0 : 1) - (b.side === "Left" ? 0 : 1));
7097
+ if (legs[0].side === legs[1].side)
7098
+ notes.push("both legs on the same side — facing is wrong");
7099
+ const rootAbove = ancestors(hips).slice(1);
7100
+ const root = rootAbove.length ? rootAbove[rootAbove.length - 1] : null;
7101
+ const fingerChain = (from) => {
7102
+ const chain = [from];
7103
+ for (let guard = 0;guard < 8; guard++) {
7104
+ const cur = chain[chain.length - 1];
7105
+ const ks = skids(cur);
7106
+ if (ks.length) {
7107
+ chain.push(ks.sort((a, b) => extent(b) - extent(a))[0]);
7108
+ continue;
7109
+ }
7110
+ const raw = children[cur].filter((c) => !skinned(c));
7111
+ if (chain.length < 3 && raw.length === 1 && !/end|nub|tip|leaf/i.test(rig.names[raw[0]]))
7112
+ chain.push(raw[0]);
7113
+ break;
7114
+ }
7115
+ return chain;
7116
+ };
7117
+ for (const arm of arms) {
7118
+ let chains = skids(arm.hand).filter((c) => subtree(c).length <= 6 && extent(c) < 0.12 * height).map(fingerChain);
7119
+ if (!chains.length)
7120
+ continue;
7121
+ const FINGER_TOKENS = ["thumb", "index", "middle", "ring", "little"];
7122
+ const nameOf = (c) => FINGER_TOKENS.find((t) => c.some((i) => TOKEN[t].test(rig.names[i])));
7123
+ const named2 = chains.map(nameOf);
7124
+ const namedSet = new Set(named2.filter(Boolean));
7125
+ let useNames = false;
7126
+ if (namedSet.size === 5 && named2.filter(Boolean).length === 5) {
7127
+ chains = chains.filter((_, k) => named2[k]);
7128
+ useNames = true;
7129
+ } else if (chains.length > 5) {
7130
+ chains = [...chains].sort((a, b) => b.length - a.length || extent(b[0]) - extent(a[0])).slice(0, 5);
7131
+ }
7132
+ const meta = [];
7133
+ const phal = chains.map((c) => c.length >= 4 ? (meta.push(c[0]), c.slice(1, 4)) : c.slice(0, 3));
7134
+ arm.metacarpals = meta;
7135
+ const byName = chains.map(nameOf);
7136
+ const assigned = {};
7137
+ if (useNames) {
7138
+ byName.forEach((t, k) => {
7139
+ assigned[t.charAt(0).toUpperCase() + t.slice(1)] = phal[k];
7140
+ });
7141
+ } else {
7142
+ const order = phal.map((c, k) => ({ k, d: len3(sub3(pos[c[0]], pos[arm.hand])) })).sort((a, b) => a.d - b.d);
7143
+ const thumb = order[0].k;
7144
+ const rest = phal.map((_, k) => k).filter((k) => k !== thumb);
7145
+ const mean = rest.reduce((a, k) => [a[0] + pos[phal[k][0]][0], a[1] + pos[phal[k][0]][1], a[2] + pos[phal[k][0]][2]], [0, 0, 0]).map((v) => v / (rest.length || 1));
7146
+ const axis = norm(sub3(pos[phal[thumb][0]], mean));
7147
+ rest.sort((a, b) => dot(sub3(pos[phal[b][0]], mean), axis) - dot(sub3(pos[phal[a][0]], mean), axis));
7148
+ assigned.Thumb = phal[thumb];
7149
+ const names = ["Index", "Middle", "Ring", "Little"];
7150
+ rest.slice(0, 4).forEach((k, n) => {
7151
+ assigned[names[n]] = phal[k];
7152
+ });
7153
+ if (phal.length !== 5)
7154
+ notes.push(`${arm.side} hand has ${phal.length} finger chains — identities by position`);
7155
+ else
7156
+ notes.push(`${arm.side} fingers identified by position (no finger names)`);
7157
+ }
7158
+ arm.fingers = assigned;
7159
+ }
7160
+ const twist = [];
7161
+ const limbLinks = [];
7162
+ for (const l of legs) {
7163
+ const seq = [l.upper, ...l.extra, l.lower, l.foot];
7164
+ for (let k = 0;k + 1 < seq.length; k++)
7165
+ limbLinks.push([seq[k], seq[k + 1]]);
7166
+ }
7167
+ for (const a of arms) {
7168
+ const seq = [...a.shoulder !== null ? [a.shoulder] : [], ...a.extra, a.upper, a.lower, a.hand];
7169
+ for (let k = 0;k + 1 < seq.length; k++)
7170
+ limbLinks.push([seq[k], seq[k + 1]]);
7171
+ }
7172
+ const structural = new Set([hips, ...spine, ...neck, head, ...jaw !== null ? [jaw] : [], ...eyes ?? [], ...legs.flatMap((l) => l.chain), ...armJoints, ...arms.flatMap((a) => [...a.metacarpals, ...Object.values(a.fingers).flat()]), ...root !== null ? [root] : []]);
7173
+ for (const [b, c] of limbLinks) {
7174
+ const dir = sub3(pos[c], pos[b]);
7175
+ for (const t of skids(b)) {
7176
+ if (structural.has(t) || subtree(t).length > 2)
7177
+ continue;
7178
+ const d = sub3(pos[t], pos[b]);
7179
+ const onAxis = len3(d) < 0.000001 || angleDeg(d, dir) < 25;
7180
+ if (onAxis && (TOKEN.twist.test(rig.names[t]) || subtree(t).length === 1)) {
7181
+ twist.push({ bone: t, of: b });
7182
+ structural.add(t);
7183
+ }
7184
+ }
7185
+ }
7186
+ const helpers = joints.filter((i) => !structural.has(i));
7187
+ if (eyes) {
7188
+ const eyeFwd = horiz(sub3([(pos[eyes[0]][0] + pos[eyes[1]][0]) / 2, 0, (pos[eyes[0]][2] + pos[eyes[1]][2]) / 2], pos[head]));
7189
+ if (len3(eyeFwd) > 0.005 * height && angleDeg(eyeFwd, forward) > 90)
7190
+ notes.push("eyes look the other way than the toes point — check facing");
7191
+ }
7192
+ lateral = norm(cross2(up, forward));
7193
+ return { root, hips, forward, lateral, up, height, ground, spine, chest, neck, head, jaw, eyes, legs, arms, twist, helpers, notes };
7194
+ }, isBiped = (rig) => {
7195
+ try {
7196
+ classifyBiped(rig);
7197
+ return true;
7198
+ } catch {
7199
+ return false;
7200
+ }
7201
+ }, avatarFromBiped = (rig, b) => {
7202
+ const n = (i) => rig.names[i];
7203
+ const bones = {};
7204
+ const bridged = [];
7205
+ bones.Hips = n(b.hips);
7206
+ const s = b.spine;
7207
+ if (s.length === 1)
7208
+ bones.Spine = n(s[0]);
7209
+ else if (s.length === 2) {
7210
+ bones.Spine = n(s[0]);
7211
+ bones.Chest = n(s[1]);
7212
+ } else if (s.length === 3) {
7213
+ bones.Spine = n(s[0]);
7214
+ bones.Chest = n(s[1]);
7215
+ bones.UpperChest = n(s[2]);
7216
+ } else if (s.length > 3) {
7217
+ const pos = rig.bind.worldPos;
7218
+ let acc = 0;
7219
+ const arc2 = s.map((i, k) => acc += len3(sub3(pos[i], pos[k ? s[k - 1] : b.hips])));
7220
+ const mid = arc2.slice(1, -1).map((v, k) => ({ k: k + 1, d: Math.abs(v - acc / 2) })).sort((a, c) => a.d - c.d)[0].k;
7221
+ bones.Spine = n(s[0]);
7222
+ bones.Chest = n(s[mid]);
7223
+ bones.UpperChest = n(s[s.length - 1]);
7224
+ s.forEach((i, k) => {
7225
+ if (k !== 0 && k !== mid && k !== s.length - 1)
7226
+ bridged.push(n(i));
7227
+ });
7228
+ }
7229
+ if (b.neck.length) {
7230
+ bones.Neck = n(b.neck[0]);
7231
+ bridged.push(...b.neck.slice(1).map(n));
7232
+ }
7233
+ bones.Head = n(b.head);
7234
+ if (b.jaw !== null)
7235
+ bones.Jaw = n(b.jaw);
7236
+ if (b.eyes) {
7237
+ bones.LeftEye = n(b.eyes[0]);
7238
+ bones.RightEye = n(b.eyes[1]);
7239
+ }
7240
+ for (const l of b.legs) {
7241
+ bones[`${l.side}UpperLeg`] = n(l.upper);
7242
+ bones[`${l.side}LowerLeg`] = n(l.lower);
7243
+ bones[`${l.side}Foot`] = n(l.foot);
7244
+ if (l.toes !== null)
7245
+ bones[`${l.side}Toes`] = n(l.toes);
7246
+ bridged.push(...l.extra.map(n));
7247
+ }
7248
+ for (const a of b.arms) {
7249
+ if (a.shoulder !== null)
7250
+ bones[`${a.side}Shoulder`] = n(a.shoulder);
7251
+ bones[`${a.side}UpperArm`] = n(a.upper);
7252
+ bones[`${a.side}LowerArm`] = n(a.lower);
7253
+ bones[`${a.side}Hand`] = n(a.hand);
7254
+ bridged.push(...a.extra.map(n), ...a.metacarpals.map(n));
7255
+ for (const f of FINGERS) {
7256
+ const chain = a.fingers[f];
7257
+ if (!chain)
7258
+ continue;
7259
+ chain.forEach((i, k) => {
7260
+ if (k < 3)
7261
+ bones[`${a.side} ${f} ${PHALANGES[k]}`] = n(i);
7262
+ });
7263
+ }
7264
+ }
7265
+ const byIndex = new Map;
7266
+ for (const [k, v] of Object.entries(bones))
7267
+ byIndex.set(rig.index.get(v), k);
7268
+ const twist = {};
7269
+ for (const t of b.twist) {
7270
+ const of = byIndex.get(t.of);
7271
+ if (of)
7272
+ twist[n(t.bone)] = of;
7273
+ }
7274
+ return { root: b.root !== null ? n(b.root) : n(b.hips), bones, twist, bridged, notes: [...b.notes] };
7275
+ }, avatarFromRig = (rig) => avatarFromBiped(rig, classifyBiped(rig)), avatarFromUnity = (rig, unity) => {
7276
+ const notes = [];
7277
+ const bones = {};
7278
+ for (const [k, v] of Object.entries(unity.bones)) {
7279
+ if (rig.index.has(v))
7280
+ bones[k] = v;
7281
+ else
7282
+ notes.push(`.meta maps ${k} to '${v}', which the rig does not have — dropped`);
7283
+ }
7284
+ if (unity.unknown.length)
7285
+ notes.push(`.meta human names not in the canon: ${unity.unknown.join(", ")}`);
7286
+ if (!bones.Hips)
7287
+ throw new Error(`${rig.label}: the .meta avatar has no Hips`);
7288
+ let shape = null;
7289
+ try {
7290
+ shape = classifyBiped(rig);
7291
+ } catch (e) {
7292
+ notes.push(`shape classification failed (${e.message}) — no twist helpers, root = skinned ancestor of Hips`);
7293
+ }
7294
+ const hips = rig.index.get(bones.Hips);
7295
+ let root = bones.Hips;
7296
+ if (unity.rootMotionBone && rig.index.has(unity.rootMotionBone))
7297
+ root = unity.rootMotionBone;
7298
+ else {
7299
+ let top = -1;
7300
+ for (let a = rig.parents[hips];a >= 0; a = rig.parents[a])
7301
+ if (rig.bind.fromSkin[a])
7302
+ top = a;
7303
+ if (top >= 0)
7304
+ root = rig.names[top];
7305
+ }
7306
+ const twist = {};
7307
+ if (shape) {
7308
+ const byIndex = new Map;
7309
+ for (const [k, v] of Object.entries(bones))
7310
+ byIndex.set(rig.index.get(v), k);
7311
+ for (const t of shape.twist) {
7312
+ const of = byIndex.get(t.of);
7313
+ if (of)
7314
+ twist[rig.names[t.bone]] = of;
7315
+ }
7316
+ }
7317
+ return { root, bones, twist, bridged: [], notes };
7318
+ }, avatarFromFile = (rig, file) => {
7319
+ const bones = {};
7320
+ const notes = [];
7321
+ for (const [k, v] of Object.entries(file.bones)) {
7322
+ if (!isHumanBone(k)) {
7323
+ notes.push(`'${k}' is not a canonical humanoid bone — ignored`);
7324
+ continue;
7325
+ }
7326
+ if (!rig.index.has(v)) {
7327
+ notes.push(`${k} → '${v}': the rig has no such bone — dropped`);
7328
+ continue;
7329
+ }
7330
+ bones[k] = v;
7331
+ }
7332
+ if (!bones.Hips)
7333
+ throw new Error(`${rig.label}: avatar has no Hips`);
7334
+ const twist = {};
7335
+ for (const [t, of] of Object.entries(file.twist ?? {}))
7336
+ if (isHumanBone(of) && rig.index.has(t))
7337
+ twist[t] = of;
7338
+ let transfer;
7339
+ if (file.transfer && typeof file.transfer === "object") {
7340
+ transfer = { ...file.transfer };
7341
+ if (transfer.align) {
7342
+ const align = {};
7343
+ for (const [k, v] of Object.entries(transfer.align)) {
7344
+ if (!isHumanBone(k)) {
7345
+ notes.push(`transfer.align: '${k}' is not a canonical bone — ignored`);
7346
+ continue;
7347
+ }
7348
+ if (v === "keep" || v === "align")
7349
+ align[k] = v;
7350
+ }
7351
+ transfer.align = align;
7352
+ }
7353
+ }
7354
+ return { root: file.root && rig.index.has(file.root) ? file.root : bones.Hips, bones, twist, bridged: [], notes, ...transfer ? { transfer } : {} };
7355
+ }, avatarToFile = (a) => ({ root: a.root, bones: { ...a.bones }, twist: Object.keys(a.twist).length ? { ...a.twist } : undefined, ...a.transfer && Object.keys(a.transfer).length ? { transfer: { ...a.transfer } } : {} }), mapAvatars = (target, donor) => {
7356
+ const map = {};
7357
+ const shared = [], targetOnly = [], donorOnly = [];
7358
+ for (const k of HUMAN_BONES) {
7359
+ const t = target.bones[k], d = donor.bones[k];
7360
+ if (t && d) {
7361
+ map[t] = d;
7362
+ shared.push(k);
7363
+ } else if (t)
7364
+ targetOnly.push(k);
7365
+ else if (d)
7366
+ donorOnly.push(k);
7367
+ }
7368
+ map[target.root] = donor.root;
7369
+ if (target.root !== target.bones.Hips && donor.root === donor.bones.Hips)
7370
+ map[target.bones.Hips] = donor.bones.Hips;
7371
+ const legs = SIDES.map((s) => target.bones[`${s}Foot`]).filter((x) => !!x);
7372
+ return { map, root: target.root, legs, shared, targetOnly, donorOnly };
7373
+ }, describeBiped = (rig, b) => {
7374
+ const n = (i) => i === null ? "-" : rig.names[i];
7375
+ const fingers = b.arms.map((a) => Object.keys(a.fingers).length).join("/");
7376
+ return `hips ${n(b.hips)}${b.root !== null ? ` (root ${n(b.root)})` : ""} · spine ${b.spine.length} · neck ${b.neck.length} · head ${n(b.head)} · jaw ${n(b.jaw)} · eyes ${b.eyes ? `${n(b.eyes[0])}/${n(b.eyes[1])}` : "-"} · arms ${b.arms.map((a) => `${a.side[0]}:${(a.shoulder !== null ? 1 : 0) + 2 + a.extra.length}`).join(" ")} · fingers ${fingers} · legs ${b.legs.map((l) => `${l.side[0]}:${2 + l.extra.length}${l.toes !== null ? "+toes" : ""}`).join(" ")} · twist ${b.twist.length} · helpers ${b.helpers.length} · facing [${b.forward.map((x) => x.toFixed(2)).join(",")}]`;
7377
+ }, describeHumanoidMap = (target, donor, hm) => {
7378
+ const lines = [];
7379
+ const pair = (k) => `${target.bones[k]}←${donor.bones[k]}`;
7380
+ const group = (label, keys) => {
7381
+ const have = keys.filter((k) => hm.shared.includes(k));
7382
+ if (have.length)
7383
+ lines.push(` ${label}: ${have.map(pair).join(", ")}`);
7384
+ };
7385
+ lines.push(` root: ${target.root}←${donor.root}`);
7386
+ group("torso", ["Hips", "Spine", "Chest", "UpperChest", "Neck", "Head", "Jaw", "LeftEye", "RightEye"]);
7387
+ for (const s of SIDES)
7388
+ group(`${s.toLowerCase()} arm`, [`${s}Shoulder`, `${s}UpperArm`, `${s}LowerArm`, `${s}Hand`]);
7389
+ for (const s of SIDES)
7390
+ group(`${s.toLowerCase()} leg`, [`${s}UpperLeg`, `${s}LowerLeg`, `${s}Foot`, `${s}Toes`]);
7391
+ for (const s of SIDES) {
7392
+ const fs = FINGERS.filter((f) => hm.shared.includes(`${s} ${f} Proximal`));
7393
+ if (fs.length)
7394
+ lines.push(` ${s.toLowerCase()} fingers: ${fs.map((f) => `${f.toLowerCase()} ×${PHALANGES.filter((p) => hm.shared.includes(`${s} ${f} ${p}`)).length}`).join(", ")}`);
7395
+ }
7396
+ if (hm.targetOnly.length)
7397
+ lines.push(` target only (kept at bind): ${hm.targetOnly.join(", ")}`);
7398
+ if (hm.donorOnly.length)
7399
+ lines.push(` donor only (dropped): ${hm.donorOnly.join(", ")}`);
7400
+ const tw = Object.entries(target.twist);
7401
+ if (tw.length)
7402
+ lines.push(` target twist helpers: ${tw.map(([b, of]) => `${b}→${of}`).join(", ")}`);
7403
+ if (target.bridged.length)
7404
+ lines.push(` target bridged: ${target.bridged.join(", ")}`);
7405
+ for (const n of target.notes)
7406
+ lines.push(` ⚠ target: ${n}`);
7407
+ for (const n of donor.notes)
7408
+ lines.push(` ⚠ donor: ${n}`);
7409
+ return lines;
7410
+ };
7411
+ var init_humanoid = __esm(() => {
7412
+ SIDES = ["Left", "Right"];
7413
+ FINGERS = ["Thumb", "Index", "Middle", "Ring", "Little"];
7414
+ PHALANGES = ["Proximal", "Intermediate", "Distal"];
7415
+ BODY = ["Hips", "Spine", "Chest", "UpperChest", "Neck", "Head", "Jaw", "LeftEye", "RightEye"];
7416
+ LIMB = ["Shoulder", "UpperArm", "LowerArm", "Hand", "UpperLeg", "LowerLeg", "Foot", "Toes"];
7417
+ HUMAN_BONES = [
7418
+ ...BODY,
7419
+ ...SIDES.flatMap((s) => LIMB.map((l) => `${s}${l}`)),
7420
+ ...SIDES.flatMap((s) => FINGERS.flatMap((f) => PHALANGES.map((p) => `${s} ${f} ${p}`)))
7421
+ ];
7422
+ HUMAN_SET = new Set(HUMAN_BONES);
7423
+ TOKEN = {
7424
+ head: /head/i,
7425
+ neck: /neck/i,
7426
+ jaw: /jaw/i,
7427
+ eye: /eye(?!.*(brow|lash|lid))/i,
7428
+ thumb: /thumb/i,
7429
+ index: /index/i,
7430
+ middle: /middle/i,
7431
+ ring: /ring(?!.*(ear|fore))/i,
7432
+ little: /pinky|little/i,
7433
+ twist: /twist|roll/i
7434
+ };
7435
+ });
7436
+
5909
7437
  // src/retarget/automap.ts
5910
7438
  var exports_automap = {};
5911
7439
  __export(exports_automap, {
@@ -5913,7 +7441,7 @@ __export(exports_automap, {
5913
7441
  classify: () => classify,
5914
7442
  autoMap: () => autoMap
5915
7443
  });
5916
- var sub2 = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]], dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2], len2 = (v) => Math.hypot(v[0], v[1], v[2]), classify = (rig) => {
7444
+ var sub4 = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]], dot2 = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2], len4 = (v) => Math.hypot(v[0], v[1], v[2]), classify = (rig) => {
5917
7445
  const notes = [];
5918
7446
  const skinned = (i) => rig.bind.fromSkin[i];
5919
7447
  const pos = rig.bind.worldPos;
@@ -5963,12 +7491,12 @@ var sub2 = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]], dot = (a, b) => a[
5963
7491
  const theta = 0.5 * Math.atan2(2 * sxz, sxx - szz);
5964
7492
  let forward = [Math.cos(theta), 0, Math.sin(theta)];
5965
7493
  const top = [...joints].sort((a, b) => pos[b][1] - pos[a][1]).slice(0, Math.max(1, Math.ceil(joints.length * 0.1)));
5966
- const topProj = top.reduce((a, i) => a + dot(sub2(pos[i], [cx, 0, cz]), forward), 0) / top.length;
7494
+ const topProj = top.reduce((a, i) => a + dot2(sub4(pos[i], [cx, 0, cz]), forward), 0) / top.length;
5967
7495
  if (topProj < 0)
5968
7496
  forward = [-forward[0], 0, -forward[2]];
5969
7497
  const lateral = [forward[2], 0, -forward[0]];
5970
- const fwd = (i) => dot(sub2(pos[i], pos[root]), forward);
5971
- const lat = (i) => dot(sub2(pos[i], pos[root]), lateral);
7498
+ const fwd = (i) => dot2(sub4(pos[i], pos[root]), forward);
7499
+ const lat = (i) => dot2(sub4(pos[i], pos[root]), lateral);
5972
7500
  const centre = (i) => Math.abs(lat(i)) < 0.03 * size;
5973
7501
  const legs = [];
5974
7502
  {
@@ -6113,7 +7641,7 @@ var sub2 = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]], dot = (a, b) => a[
6113
7641
  const out = [];
6114
7642
  let acc = 0, prev = anchor;
6115
7643
  for (const c of chain) {
6116
- acc += len2(sub2(rig.bind.worldPos[c], rig.bind.worldPos[prev]));
7644
+ acc += len4(sub4(rig.bind.worldPos[c], rig.bind.worldPos[prev]));
6117
7645
  out.push(acc);
6118
7646
  prev = c;
6119
7647
  }
@@ -6559,16 +8087,16 @@ var matMul2 = (a, b) => {
6559
8087
  lowfront: { r: [1, 0, 0], u: [0, 0.87, -0.5], d: [0, 0.5, -0.87] }
6560
8088
  };
6561
8089
  const B = bases[VIEW] ?? bases.side;
6562
- const dot2 = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
8090
+ const dot3 = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
6563
8091
  const corners = [];
6564
8092
  for (let i = 0;i < 8; i++)
6565
8093
  corners.push([i & 1 ? hi[0] : lo[0], i & 2 ? hi[1] : lo[1], i & 4 ? hi[2] : lo[2]]);
6566
- const ru = corners.map((c) => [dot2(B.r, c), dot2(B.u, c)]);
8094
+ const ru = corners.map((c) => [dot3(B.r, c), dot3(B.u, c)]);
6567
8095
  const rlo = [Math.min(...ru.map((p) => p[0])), Math.min(...ru.map((p) => p[1]))];
6568
8096
  const rhi = [Math.max(...ru.map((p) => p[0])), Math.max(...ru.map((p) => p[1]))];
6569
8097
  const k = (CELL - 2 * PAD) / Math.max(rhi[0] - rlo[0], rhi[1] - rlo[1], 0.000000001);
6570
- const sx = (ox, q) => ox + PAD + (dot2(B.r, q) - rlo[0]) * k + (CELL - 2 * PAD - (rhi[0] - rlo[0]) * k) / 2;
6571
- const sy = (q) => HEAD + CELL - PAD - (dot2(B.u, q) - rlo[1]) * k;
8098
+ const sx = (ox, q) => ox + PAD + (dot3(B.r, q) - rlo[0]) * k + (CELL - 2 * PAD - (rhi[0] - rlo[0]) * k) / 2;
8099
+ const sy = (q) => HEAD + CELL - PAD - (dot3(B.u, q) - rlo[1]) * k;
6572
8100
  const W = CELL * NF, H = CELL + HEAD;
6573
8101
  const img = new Uint8Array(W * H * 4);
6574
8102
  for (let i = 0;i < W * H; i++) {
@@ -6594,7 +8122,7 @@ var matMul2 = (a, b) => {
6594
8122
  const nl = Math.hypot(nx, ny, nz) || 1;
6595
8123
  const shade = 0.22 + 0.78 * Math.abs(nx / nl * light[0] + ny / nl * light[1] + nz / nl * light[2]);
6596
8124
  const col = [Math.round(base[0] * shade), Math.round(base[1] * shade), Math.round(base[2] * shade)];
6597
- const sp = p.map((q) => [sx(ox, q), sy(q), -dot2(B.d, q)]);
8125
+ const sp = p.map((q) => [sx(ox, q), sy(q), -dot3(B.d, q)]);
6598
8126
  const minX = Math.max(ox, Math.floor(Math.min(sp[0][0], sp[1][0], sp[2][0]))), maxX = Math.min(ox + CELL - 1, Math.ceil(Math.max(sp[0][0], sp[1][0], sp[2][0])));
6599
8127
  const minY = Math.max(HEAD, Math.floor(Math.min(sp[0][1], sp[1][1], sp[2][1]))), maxY = Math.min(H - 1, Math.ceil(Math.max(sp[0][1], sp[1][1], sp[2][1])));
6600
8128
  const d = (sp[1][0] - sp[0][0]) * (sp[2][1] - sp[0][1]) - (sp[2][0] - sp[0][0]) * (sp[1][1] - sp[0][1]);
@@ -6665,8 +8193,146 @@ var init_filmstrip = __esm(() => {
6665
8193
  };
6666
8194
  });
6667
8195
 
8196
+ // src/terrainPack.ts
8197
+ var exports_terrainPack = {};
8198
+ __export(exports_terrainPack, {
8199
+ resolveTerrainLayer: () => resolveTerrainLayer,
8200
+ packTerrainTextures: () => packTerrainTextures
8201
+ });
8202
+ import { existsSync as existsSync10, mkdirSync as mkdirSync4, readdirSync as readdirSync5, readFileSync as readFileSync11, statSync as statSync6, unlinkSync } from "node:fs";
8203
+ import { basename as basename6, extname as extname4, join as join9, resolve as resolve5 } from "node:path";
8204
+ import { spawnSync as spawnSync4 } from "node:child_process";
8205
+ var IMAGE_EXT, resolveTerrainLayer = (input, normal, name) => {
8206
+ const p = resolve5(input);
8207
+ if (!existsSync10(p))
8208
+ throw new Error(`not found: ${input}`);
8209
+ if (statSync6(p).isDirectory()) {
8210
+ const files = readdirSync5(p).filter((f) => IMAGE_EXT.test(f));
8211
+ const albedo = files.find((f) => /albedo/i.test(f) && !/smooth/i.test(f)) ?? files.find((f) => /albedo|basecolor|base_color|diffuse|_d\./i.test(f));
8212
+ if (!albedo)
8213
+ throw new Error(`${input}: no albedo image (a file named *Albedo* / *BaseColor* / *Diffuse*)`);
8214
+ const nrm2 = normal ?? files.find((f) => /normal|_n\./i.test(f) && !/normalmap_?dx/i.test(f));
8215
+ return { name: name ?? cleanName(basename6(p)), albedo: join9(p, albedo), normal: nrm2 ? normal ? resolve5(normal) : join9(p, nrm2) : undefined };
8216
+ }
8217
+ if (!IMAGE_EXT.test(p))
8218
+ throw new Error(`${input}: not an image (png/jpg/webp/tiff/tga/bmp)`);
8219
+ let nrm = normal ? resolve5(normal) : undefined;
8220
+ if (!nrm) {
8221
+ const stem = p.slice(0, -extname4(p).length);
8222
+ for (const cand of [stem.replace(/albedo/i, "Normal"), stem.replace(/albedo/i, "normal"), `${stem}_Normal`, `${stem}_normal`, stem.replace(/_?(basecolor|diffuse|_d)$/i, "_Normal")]) {
8223
+ if (cand === stem)
8224
+ continue;
8225
+ const hit = [".png", ".jpg", ".jpeg", ".tga", ".webp", ".tif", ".tiff", ".bmp"].map((e) => cand + e).find((f) => existsSync10(f));
8226
+ if (hit) {
8227
+ nrm = hit;
8228
+ break;
8229
+ }
8230
+ }
8231
+ }
8232
+ return { name: name ?? cleanName(basename6(p, extname4(p)).replace(/[_-]?(albedo|basecolor|base_color|diffuse)$/i, "")), albedo: p, normal: nrm };
8233
+ }, cleanName = (s) => s.trim().replace(/\s+/g, "_").replace(/[^A-Za-z0-9_-]/g, "").toLowerCase() || "layer", loadRgba = async (path, size) => {
8234
+ const sharp3 = (await import("sharp")).default;
8235
+ let bytes2 = new Uint8Array(readFileSync11(path));
8236
+ const mime = sniffMime(bytes2, path);
8237
+ if (mime === "image/x-tga" || mime === "image/bmp") {
8238
+ const rgba = mime === "image/x-tga" ? decodeTga(bytes2) : decodeBmp(bytes2);
8239
+ if (!rgba)
8240
+ throw new Error(`${path}: could not decode the ${mime === "image/bmp" ? "BMP" : "TGA"}`);
8241
+ bytes2 = encodePng(rgba);
8242
+ }
8243
+ const img = sharp3(Buffer.from(bytes2));
8244
+ const meta = await img.metadata();
8245
+ const { data, info } = await img.resize(size, size, { fit: "fill", kernel: "lanczos3" }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
8246
+ return { data, width: info.width, height: info.height, sourceSize: [meta.width ?? 0, meta.height ?? 0] };
8247
+ }, encodeKtx2 = (toktx, png, ktx2, srgb, quality) => {
8248
+ const args = ["--t2", "--encode", "uastc", "--uastc_quality", String(quality), "--zcmp", "18", "--genmipmap", "--assign_oetf", srgb ? "srgb" : "linear", ktx2, png];
8249
+ const p = spawnSync4(toktx, args, { encoding: "utf8" });
8250
+ if (p.status !== 0)
8251
+ throw new Error(`toktx failed (${p.status ?? p.error?.message ?? "?"}) on ${basename6(png)}:
8252
+ ${(p.stderr || p.stdout || "").trim()}`);
8253
+ }, packTerrainTextures = async (layers, opts) => {
8254
+ if (layers.length === 0)
8255
+ throw new Error("no layers");
8256
+ if (layers.length > 4)
8257
+ throw new Error(`${layers.length} layers — a terrain material takes at most four`);
8258
+ const size = opts.size ?? 1024;
8259
+ if (!(size >= 16 && size <= 8192) || (size & size - 1) !== 0)
8260
+ throw new Error(`--size wants a power of two in 16..8192, got ${size}`);
8261
+ const log = opts.log ?? (() => {});
8262
+ const sharp3 = (await import("sharp")).default;
8263
+ const toktx = opts.png ? null : findToktx();
8264
+ if (toktx && !checkToktx(toktx)) {
8265
+ throw new Error("KTX-Software's toktx is needed for KTX2 output — install it (https://github.com/KhronosGroup/KTX-Software/releases, on PATH or KTX_TOKTX=<path>) or pass --png");
8266
+ }
8267
+ const seen = new Set;
8268
+ for (const l of layers) {
8269
+ if (seen.has(l.name))
8270
+ throw new Error(`two layers named "${l.name}" — pass --names`);
8271
+ seen.add(l.name);
8272
+ }
8273
+ mkdirSync4(opts.outDir, { recursive: true });
8274
+ const ext = opts.png ? ".png" : ".ktx2";
8275
+ const writeImage = async (raw, channels, out, srgb) => {
8276
+ let pipe = sharp3(raw.data, { raw: { width: raw.width, height: raw.height, channels: 4 } });
8277
+ if (channels === 3)
8278
+ pipe = pipe.removeAlpha();
8279
+ const png = opts.png ? out : out.slice(0, -ext.length) + ".png";
8280
+ await pipe.png({ compressionLevel: opts.png ? 9 : 1 }).toFile(png);
8281
+ if (toktx) {
8282
+ encodeKtx2(toktx, png, out, srgb, opts.quality ?? 1);
8283
+ unlinkSync(png);
8284
+ }
8285
+ };
8286
+ const result = { layers: [], normals: [], snippet: "" };
8287
+ const normalRaws = [];
8288
+ for (const l of layers) {
8289
+ const alb = await loadRgba(l.albedo, size);
8290
+ const file = join9(opts.outDir, l.name + ext);
8291
+ await writeImage(alb, 3, file, true);
8292
+ let nrm = null;
8293
+ if (l.normal) {
8294
+ if (!existsSync10(l.normal))
8295
+ throw new Error(`${l.name}: normal map not found: ${l.normal}`);
8296
+ nrm = await loadRgba(l.normal, size);
8297
+ }
8298
+ normalRaws.push(nrm);
8299
+ result.layers.push({ name: l.name, file, albedo: l.albedo, normal: l.normal ?? null, sourceSize: alb.sourceSize });
8300
+ log(` ${l.name.padEnd(16)} ${alb.sourceSize[0]}×${alb.sourceSize[1]} → ${size}² ${nrm ? `+ normal ${nrm.sourceSize[0]}×${nrm.sourceSize[1]}` : "(flat normal)"}`);
8301
+ }
8302
+ const packCount = layers.length > 2 ? 2 : 1;
8303
+ for (let k = 0;k < packCount; k++) {
8304
+ const a = normalRaws[k * 2] ?? null, b = normalRaws[k * 2 + 1] ?? null;
8305
+ const n = size * size;
8306
+ const out = Buffer.alloc(n * 4);
8307
+ for (let i = 0;i < n; i++) {
8308
+ const o = i * 4;
8309
+ out[o] = a ? a.data[o] : 128;
8310
+ out[o + 1] = a ? opts.flipY ? 255 - a.data[o + 1] : a.data[o + 1] : 128;
8311
+ out[o + 2] = b ? b.data[o] : 128;
8312
+ out[o + 3] = b ? opts.flipY ? 255 - b.data[o + 1] : b.data[o + 1] : 128;
8313
+ }
8314
+ const file = join9(opts.outDir, `normals${k * 2}${k * 2 + 1}${ext}`);
8315
+ await writeImage({ data: out, width: size, height: size }, 4, file, false);
8316
+ result.normals.push(file);
8317
+ log(` normals${k * 2}${k * 2 + 1}${"".padEnd(7)} ${a ? layers[k * 2].name : "flat"} + ${b ? layers[k * 2 + 1].name : layers[k * 2 + 1] ? "flat" : "—"}`);
8318
+ }
8319
+ const rel = (f) => "./" + basename6(f);
8320
+ result.snippet = [
8321
+ "layers: [",
8322
+ ...result.layers.map((l, i) => ` { albedo: asset('${rel(l.file)}'), tiling: 4${normalRaws[i] ? ", normalScale: 1" : ""} },`),
8323
+ `], normals: [ ${result.normals.map((f) => `asset('${rel(f)}')`).join(", ")} ]`
8324
+ ].join(`
8325
+ `);
8326
+ return result;
8327
+ };
8328
+ var init_terrainPack = __esm(() => {
8329
+ init_resolveImage();
8330
+ init_transcodeKtx2();
8331
+ IMAGE_EXT = /\.(png|jpe?g|webp|tiff?|tga|bmp)$/i;
8332
+ });
8333
+
6668
8334
  // src/cli.ts
6669
- import { existsSync as existsSync10, statSync as statSync6, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, readFileSync as readFileSync11, readdirSync as readdirSync5 } from "node:fs";
8335
+ import { existsSync as existsSync11, statSync as statSync7, mkdirSync as mkdirSync5, writeFileSync as writeFileSync4, readFileSync as readFileSync12, readdirSync as readdirSync6 } from "node:fs";
6670
8336
 
6671
8337
  // src/sky/index.ts
6672
8338
  import { spawnSync as spawnSync2 } from "node:child_process";
@@ -6951,7 +8617,7 @@ var formatSkyReport = (r, name = "SKY") => {
6951
8617
  };
6952
8618
 
6953
8619
  // src/cli.ts
6954
- import { basename as basename6, dirname as dirname7, extname as extname4, join as join9 } from "node:path";
8620
+ import { basename as basename7, dirname as dirname7, extname as extname5, join as join10, resolve as resolve6 } from "node:path";
6955
8621
  var USAGE = `lecodes-assets — FBX → GLB converter + GLB doctor (docs/animation-plan.md §2.9)
6956
8622
 
6957
8623
  convert <model.fbx|.glb> [options] FBX (or an existing GLB) → LeCodes-ready GLB
@@ -6991,16 +8657,35 @@ var USAGE = `lecodes-assets — FBX → GLB converter + GLB doctor (docs/animati
6991
8657
  "align": { bone: "keep" | "direction" | [ refBones ] },
6992
8658
  "legs": [ footBones ] } (align/legs optional)
6993
8659
  --skeletons print both bone lists and exit (to write that map)
6994
- --auto-map [out.json] | --map auto derive the map from the two skeletons' SHAPE (quadrupeds:
6995
- spine/neck/head/tail/legs/ears/jaw) — write it, or use it directly
8660
+ --auto-map [out.json] | --map auto derive the map from the two skeletons' SHAPE — write it, or use it
8661
+ directly. Quadrupeds: spine/neck/head/tail/legs/ears/jaw chains.
8662
+ Humanoids: both rigs' AVATARS (see \`avatar\`) joined on the canon
8663
+ --avatar <a.avatar.json|model.meta> the target's avatar instead of deriving it (a Unity .meta works)
8664
+ --donor-avatar <…> the donor's
8665
+ --donor-rig <model.fbx|.glb> the rig a skeleton-only clip file (mocap packs: nodes + curves,
8666
+ no mesh) was animated on — its clips bind to it by bone name
6996
8667
  --clips a b … only these donor clips --keep-clips keep the target's own
6997
8668
  --align none|bone-direction posture alignment (default bone-direction)
6998
- --ik none|legs foot placement against the ground (default legs)
8669
+ --ik none|legs|hands foot placement against the ground (default legs); hands = the
8670
+ hands as effectors too (clips where they meet or hold)
8671
+ --root-motion auto|hips|root|none where the travel lands: a root above the hips takes the horizontal
8672
+ travel, the hips the bob (auto/root); all on the hips; none = in place
8673
+ --no-plant no foot planting (a standing foot is otherwise held where it landed)
8674
+ --twist N|off twist helpers' share of their limb's axial turn (default 0.5)
6999
8675
  --donor-ref bind|auto|<clip> pose the donor's motion is measured from (auto = mean of its idle/walk)
7000
8676
  --fps N --root-scale N --prefix s sampling rate / root-motion scale / clip name prefix
7001
8677
  --clip-names n1 n2 … rename the produced clips, positionally
7002
8678
  --anim a.anim b.anim … Unity AnimationClip assets to use as the donor's clips
7003
8679
  (for packs shipping an empty FBX rig + .anim files)
8680
+ avatar <model.fbx|.glb> [--meta model.fbx.meta] [-o model.avatar.json]
8681
+ a humanoid rig's AVATAR: its bones named in the Unity/VRM canon
8682
+ (Hips, Spine…, LeftUpperArm…, Left Thumb Proximal…), read off the
8683
+ skeleton's shape — hips = the joint two ground-reaching chains hang
8684
+ from, hands = the lateral joints fanning into fingers, head = the
8685
+ last branch above the arms; twist/roll helpers recorded, not mapped.
8686
+ --meta checks the result against Unity's own humanoid map (and
8687
+ imports it verbatim when you trust it more: -o with --meta writes
8688
+ Unity's). The file is what \`retarget --avatar\` reads
7004
8689
  sky <env.exr|.hdr> equirectangular HDR → the TWO cubemaps a scene needs
7005
8690
  -o dir where they land (default: beside the input)
7006
8691
  --size N edge of the DRAWN sky cubemap (default 512)
@@ -7016,6 +8701,19 @@ var USAGE = `lecodes-assets — FBX → GLB converter + GLB doctor (docs/animati
7016
8701
  can be aimed to match instead of by eye.
7017
8702
  Needs filament's cmgen (LECODES_CMGEN / ~/.lecodes/cmgen).
7018
8703
 
8704
+ terrain-pack <layer> [<layer> …] a terrain's splat layers (≤ 4) → <name>.ktx2 per layer + the PACKED
8705
+ normal maps normals01.ktx2 / normals23.ktx2 (layer XY pairs in
8706
+ RG/BA — terrain.mat has nine samplers, so normals ship two per
8707
+ texture). A layer = an albedo file (its normal guessed from the
8708
+ name, or --normals) or a FOLDER holding *Albedo* + *Normal*
8709
+ (Unity terrain packs). Prints the Terrain.create fragment
8710
+ -o dir where they land (default: ./terrain)
8711
+ --normals n0 n1 … normal map per layer, positionally ("-" = none)
8712
+ --names a b … layer names → file names (default: from the inputs)
8713
+ --size N output edge (1024) --quality N UASTC quality 0–4 (1)
8714
+ --png PNGs instead of KTX2 (no toktx needed)
8715
+ --flip-y DirectX-style normal maps (green inverted)
8716
+
7019
8717
  probe <file.fbx|.glb> skeleton, clips (+ per-clip root motion / first-frame-vs-rest),
7020
8718
  meshes, materials, textures, bind sanity, world size, duplicate names
7021
8719
  --filmstrip [out.png] (GLB) CPU-skinned PNG strip of every clip — the fastest "does it play
@@ -7034,8 +8732,12 @@ var USAGE = `lecodes-assets — FBX → GLB converter + GLB doctor (docs/animati
7034
8732
  [--unlit] flat base colour, no lighting (KHR_materials_unlit) — for stylised assets
7035
8733
  [--lightmap-uv] add a lightmap UV set (TEXCOORD_1, xatlas) — what \`lecodes lightmap bake\`
7036
8734
  samples; skinned meshes are skipped. --lightmap-padding N (4),
7037
- --lightmap-iterations N (1, more = better charts, slower)`;
7038
- var MULTI = new Set(["--clips", "--clip-names", "--rename", "--anim", "--only", "--keep", "--merge-skins"]);
8735
+ --lightmap-iterations N (1, more = better charts, slower)
8736
+ [--lod 0.5,0.2] [--lod-bones] add mesh LOD levels (<name>_LOD1... siblings at these triangle
8737
+ ratios, same skin; >= 2000-tri meshes only) - creator-gl shows one
8738
+ by screen size (docs/lod-plan.md); --lod-bones folds fingers /
8739
+ toes / twists onto their parents in the last level's skin`;
8740
+ var MULTI = new Set(["--clips", "--clip-names", "--rename", "--anim", "--only", "--keep", "--merge-skins", "--normals", "--names"]);
7039
8741
  var SINGLE = new Set([
7040
8742
  "-o",
7041
8743
  "--max-size",
@@ -7054,6 +8756,12 @@ var SINGLE = new Set([
7054
8756
  "--prefix",
7055
8757
  "--unity-mats",
7056
8758
  "--unity-index",
8759
+ "--avatar",
8760
+ "--donor-avatar",
8761
+ "--meta",
8762
+ "--root-motion",
8763
+ "--twist",
8764
+ "--donor-rig",
7057
8765
  "--split",
7058
8766
  "--list-nodes",
7059
8767
  "--filmstrip",
@@ -7071,7 +8779,9 @@ var SINGLE = new Set([
7071
8779
  "--name",
7072
8780
  "--ibl-name",
7073
8781
  "--lightmap-padding",
7074
- "--lightmap-iterations"
8782
+ "--lightmap-iterations",
8783
+ "--quality",
8784
+ "--lod"
7075
8785
  ]);
7076
8786
  var isValue = (s) => !s.startsWith("-") || /^-(\d|\.\d)/.test(s);
7077
8787
  var parse = (argv) => {
@@ -7104,7 +8814,7 @@ var cmdConvert = async (p) => {
7104
8814
  log(USAGE);
7105
8815
  return 2;
7106
8816
  }
7107
- if (!existsSync10(input)) {
8817
+ if (!existsSync11(input)) {
7108
8818
  console.error(`✗ not found: ${input}`);
7109
8819
  return 1;
7110
8820
  }
@@ -7123,7 +8833,7 @@ var cmdConvert = async (p) => {
7123
8833
  log(`\uD83D\uDD04 ${input}${clips.length ? ` + ${clips.length} clip file(s)` : ""}`);
7124
8834
  let doc;
7125
8835
  let report = { clips: [] };
7126
- if (extname4(input).toLowerCase() === ".fbx") {
8836
+ if (extname5(input).toLowerCase() === ".fbx") {
7127
8837
  const r = await convertFbx2(input, {
7128
8838
  clips,
7129
8839
  clipNames,
@@ -7145,8 +8855,8 @@ var cmdConvert = async (p) => {
7145
8855
  } else {
7146
8856
  doc = await makeIO3().read(input);
7147
8857
  for (let i = 0;i < clips.length; i++) {
7148
- const cs = await readFbx2(new Uint8Array(readFileSync11(clips[i])));
7149
- report.clips.push(...mergeClipScene2(doc, cs, basename6(clips[i]), clipNames?.[i], rename, warn));
8858
+ const cs = await readFbx2(new Uint8Array(readFileSync12(clips[i])));
8859
+ report.clips.push(...mergeClipScene2(doc, cs, basename7(clips[i]), clipNames?.[i], rename, warn));
7150
8860
  }
7151
8861
  }
7152
8862
  for (const c of report.clips)
@@ -7157,12 +8867,12 @@ var cmdConvert = async (p) => {
7157
8867
  const { convertFbx: convertFbx3 } = await Promise.resolve().then(() => (init_convert(), exports_convert));
7158
8868
  const donors = [];
7159
8869
  for (const f of mergeFiles) {
7160
- if (!existsSync10(f)) {
8870
+ if (!existsSync11(f)) {
7161
8871
  console.error(`✗ not found: ${f}`);
7162
8872
  return 1;
7163
8873
  }
7164
- const d = extname4(f).toLowerCase() === ".fbx" ? (await convertFbx3(f, { dropModelClips: true, warn, unity: p.flags.has("--no-unity") ? false : p.values.get("--unity-mats")?.[0] ?? true, unityIndex: p.values.get("--unity-index")?.[0] })).doc : await makeIO3().read(f);
7165
- donors.push({ doc: d, label: basename6(f) });
8874
+ const d = extname5(f).toLowerCase() === ".fbx" ? (await convertFbx3(f, { dropModelClips: true, warn, unity: p.flags.has("--no-unity") ? false : p.values.get("--unity-mats")?.[0] ?? true, unityIndex: p.values.get("--unity-index")?.[0] })).doc : await makeIO3().read(f);
8875
+ donors.push({ doc: d, label: basename7(f) });
7166
8876
  }
7167
8877
  const mr = await mergeSkins2(doc, donors, warn);
7168
8878
  for (const d of mr.donors)
@@ -7207,32 +8917,42 @@ var cmdConvert = async (p) => {
7207
8917
  log(` baked scale ×${+res.scaled.toPrecision(6)}`);
7208
8918
  if (res.lightmapUv)
7209
8919
  log(` lightmap UV: ${res.lightmapUv.charts} charts over ${res.lightmapUv.primitives} primitive(s) (xatlas ${res.lightmapUv.width}×${res.lightmapUv.height}); vertices ${res.lightmapUv.verticesBefore} → ${res.lightmapUv.verticesAfter}`);
7210
- }
7211
- const out = p.values.get("-o")?.[0] ?? join9(dirname7(input), basename6(input, extname4(input)) + ".glb");
8920
+ if (res.lod) {
8921
+ for (const t of res.lod.tris)
8922
+ log(` lod: ${t.name} ${t.levels.join(" / ")} tris`);
8923
+ for (const b of res.lod.bones)
8924
+ log(` lod bones: ${b.name} last level ${b.before} → ${b.after} joints`);
8925
+ for (const k of res.lod.skipped)
8926
+ log(` lod: ${k.name} level ${k.level} dropped — the simplifier reached only ${k.tris} tris (seams / hard surfaces; try a smaller ratio)`);
8927
+ if (!res.lod.nodes)
8928
+ log(` lod: no mesh with >= 2000 triangles (or the _LOD nodes are already there)`);
8929
+ }
8930
+ }
8931
+ const out = p.values.get("-o")?.[0] ?? join10(dirname7(input), basename7(input, extname5(input)) + ".glb");
7212
8932
  const { validateGlb: validateGlb2, formatReport: formatReport2 } = await Promise.resolve().then(() => (init_validateGlb(), exports_validateGlb));
7213
8933
  const { probeDocument: probeDocument2 } = await Promise.resolve().then(() => (init_probe(), exports_probe));
7214
8934
  if (p.values.has("--split")) {
7215
8935
  const { splitDoc: splitDoc2, fileStem: fileStem2 } = await Promise.resolve().then(() => (init_props(), exports_props));
7216
- const dir2 = p.values.get("--split")?.[0] ?? (p.values.has("-o") ? out.replace(/\.glb$/i, "") : join9(dirname7(input), basename6(input, extname4(input))));
7217
- if (!existsSync10(dir2))
7218
- mkdirSync4(dir2, { recursive: true });
8936
+ const dir2 = p.values.get("--split")?.[0] ?? (p.values.has("-o") ? out.replace(/\.glb$/i, "") : join10(dirname7(input), basename7(input, extname5(input))));
8937
+ if (!existsSync11(dir2))
8938
+ mkdirSync5(dir2, { recursive: true });
7219
8939
  const parts = await splitDoc2(doc, { ground: !p.flags.has("--no-ground") });
7220
8940
  let total = 0;
7221
8941
  for (const part of parts) {
7222
- const f = join9(dir2, fileStem2(part.name) + ".glb");
8942
+ const f = join10(dir2, fileStem2(part.name) + ".glb");
7223
8943
  const glb2 = await writeGlb2(part.doc);
7224
8944
  writeFileSync4(f, glb2);
7225
8945
  total += glb2.byteLength;
7226
8946
  const v = validateGlb2(glb2);
7227
- log(` ${v.ok ? "✅" : "✗"} ${basename6(f).padEnd(40)} ${part.size.map((x) => x.toFixed(2)).join("×")} m ${(glb2.byteLength / 1024).toFixed(0)} KB`);
8947
+ log(` ${v.ok ? "✅" : "✗"} ${basename7(f).padEnd(40)} ${part.size.map((x) => x.toFixed(2)).join("×")} m ${(glb2.byteLength / 1024).toFixed(0)} KB`);
7228
8948
  }
7229
8949
  log(`\uD83D\uDCBE wrote ${parts.length} file(s) → ${dir2} (${(total / 1024 / 1024).toFixed(2)} MB, ${((performance.now() - t0) / 1000).toFixed(1)}s)`);
7230
8950
  return 0;
7231
8951
  }
7232
8952
  const glb = await writeGlb2(doc);
7233
8953
  const dir = dirname7(out);
7234
- if (dir && !existsSync10(dir))
7235
- mkdirSync4(dir, { recursive: true });
8954
+ if (dir && !existsSync11(dir))
8955
+ mkdirSync5(dir, { recursive: true });
7236
8956
  writeFileSync4(out, glb);
7237
8957
  log(formatReport2(validateGlb2(glb), out));
7238
8958
  log(probeDocument2(doc, out).split(`
@@ -7257,6 +8977,7 @@ var real = (p, key) => {
7257
8977
  var scaleOpts = (p) => ({ scale: real(p, "--scale"), scaleTo: real(p, "--scale-to"), fixScale: p.flags.has("--fix-scale") });
7258
8978
  var shadingOpts = (p) => ({ unlit: p.flags.has("--unlit"), fixEmissive: p.flags.has("--fix-emissive") });
7259
8979
  var lightmapOpts = (p) => ({ lightmapUv: p.flags.has("--lightmap-uv"), lightmapPadding: num2(p, "--lightmap-padding"), lightmapIterations: num2(p, "--lightmap-iterations") });
8980
+ var lodOpts = (p) => ({ lod: (p.values.get("--lod") ?? []).flatMap((v) => v.split(",")).map(Number).filter((n) => n > 0 && n < 1), lodBones: p.flags.has("--lod-bones") });
7260
8981
  var cmdRetarget = async (p) => {
7261
8982
  const input = p.positionals[0];
7262
8983
  const donorPath = p.values.get("--from")?.[0];
@@ -7265,27 +8986,41 @@ var cmdRetarget = async (p) => {
7265
8986
  return 2;
7266
8987
  }
7267
8988
  for (const f of [input, donorPath])
7268
- if (!existsSync10(f)) {
8989
+ if (!existsSync11(f)) {
7269
8990
  console.error(`✗ not found: ${f}`);
7270
8991
  return 1;
7271
8992
  }
7272
- const { loadRig: loadRig2, rigFromDocument: rigFromDocument2, checkUniformScale: checkUniformScale2 } = await Promise.resolve().then(() => (init_rig(), exports_rig));
8993
+ const { loadRig: loadRig2, rigFromDocument: rigFromDocument2, checkUniformScale: checkUniformScale2, hasSkin: hasSkin2, rebindClips: rebindClips2 } = await Promise.resolve().then(() => (init_rig(), exports_rig));
7273
8994
  const { retarget: retarget2, bindAccessors: bindAccessors2, analyzeFacing: analyzeFacing2, facingMismatch: facingMismatch2 } = await Promise.resolve().then(() => (init_retarget(), exports_retarget));
7274
8995
  const { makeIO: makeIO3, writeGlb: writeGlb2 } = await Promise.resolve().then(() => (init_convert(), exports_convert));
7275
8996
  const { probeDocument: probeDocument2 } = await Promise.resolve().then(() => (init_probe(), exports_probe));
7276
8997
  const doc = await makeIO3().read(input);
7277
- const target = rigFromDocument2(doc, basename6(input));
7278
- let donor = donorPath.toLowerCase().endsWith(".fbx") ? await loadRig2(donorPath) : rigFromDocument2(await makeIO3().read(donorPath), basename6(donorPath));
8998
+ const target = rigFromDocument2(doc, basename7(input));
8999
+ const loadAny = async (path) => path.toLowerCase().endsWith(".fbx") ? loadRig2(path) : rigFromDocument2(await makeIO3().read(path), basename7(path));
9000
+ let donor = await loadAny(donorPath);
9001
+ const donorRigPath = p.values.get("--donor-rig")?.[0];
9002
+ if (donorRigPath) {
9003
+ if (!existsSync11(donorRigPath)) {
9004
+ console.error(`✗ not found: ${donorRigPath}`);
9005
+ return 1;
9006
+ }
9007
+ const r = rebindClips2(await loadAny(donorRigPath), donor);
9008
+ log(` \uD83D\uDCCE ${basename7(donorPath)}: ${r.bound}/${r.total} bones bound to ${basename7(donorRigPath)} by name${Math.abs(r.scale - 1) > 0.01 ? ` · the clip's skeleton is ×${(1 / r.scale).toFixed(2)} the rig's size (rotations taken, lengths the rig's)` : ""}${r.unbound.length ? ` (no bone: ${r.unbound.slice(0, 3).join(", ")}${r.unbound.length > 3 ? ` +${r.unbound.length - 3}` : ""})` : ""}`);
9009
+ donor = r.rig;
9010
+ } else if (!hasSkin2(donor) && !p.flags.has("--skeletons")) {
9011
+ console.error(`✗ ${basename7(donorPath)} carries no skin (a clip-only file) — name the rig it was animated on: --donor-rig <model.fbx|.glb>`);
9012
+ return 2;
9013
+ }
7279
9014
  const animFiles = p.values.get("--anim") ?? [];
7280
9015
  if (animFiles.length) {
7281
9016
  const { parseUnityAnim: parseUnityAnim2, attachUnityClips: attachUnityClips2 } = await Promise.resolve().then(() => (init_unityAnim(), exports_unityAnim));
7282
9017
  const parsed = [];
7283
9018
  for (const f of animFiles) {
7284
- if (!existsSync10(f)) {
9019
+ if (!existsSync11(f)) {
7285
9020
  console.error(`✗ not found: ${f}`);
7286
9021
  return 1;
7287
9022
  }
7288
- parsed.push(parseUnityAnim2(readFileSync11(f, "utf8"), basename6(f).replace(/\.anim$/i, "")));
9023
+ parsed.push(parseUnityAnim2(readFileSync12(f, "utf8"), basename7(f).replace(/\.anim$/i, "")));
7289
9024
  }
7290
9025
  const { clips: attached, report } = attachUnityClips2(donor, parsed);
7291
9026
  donor = { ...donor, clips: [...donor.clips, ...attached] };
@@ -7309,31 +9044,76 @@ var cmdRetarget = async (p) => {
7309
9044
  };
7310
9045
  const a = col(target), b = col(donor);
7311
9046
  log(`
7312
- ${`TARGET ${basename6(input)} (${target.names.length})`.padEnd(46)}DONOR ${basename6(donorPath)} (${donor.names.length})`);
9047
+ ${`TARGET ${basename7(input)} (${target.names.length})`.padEnd(46)}DONOR ${basename7(donorPath)} (${donor.names.length})`);
7313
9048
  log(`${"(* = bound by a skin)".padEnd(46)}(* = bound by a skin)`);
7314
9049
  for (let i = 0;i < Math.max(a.length, b.length); i++)
7315
9050
  log(`${(a[i] ?? "").slice(0, 44).padEnd(46)}${b[i] ?? ""}`);
7316
9051
  log(`
7317
9052
  donor clips: ${donor.clips.map((c) => `"${c.name}" ${c.duration.toFixed(2)}s`).join(", ") || "none"}`);
9053
+ const { classifyBiped: classifyBiped3, describeBiped: describeBiped3 } = await Promise.resolve().then(() => (init_humanoid(), exports_humanoid));
9054
+ for (const [label, rig] of [["target", target], ["donor", donor]]) {
9055
+ try {
9056
+ log(`${label} reads as a humanoid: ${describeBiped3(rig, classifyBiped3(rig))}`);
9057
+ } catch {}
9058
+ }
7318
9059
  return 0;
7319
9060
  }
9061
+ const { avatarFromRig: avatarFromRig2, avatarFromUnity: avatarFromUnity2, avatarFromFile: avatarFromFile2, parseUnityAvatar: parseUnityAvatar2, mapAvatars: mapAvatars2, describeHumanoidMap: describeHumanoidMap2, describeBiped: describeBiped2, classifyBiped: classifyBiped2, isBiped: isBiped2 } = await Promise.resolve().then(() => (init_humanoid(), exports_humanoid));
9062
+ const loadAvatar = (rig, file) => {
9063
+ if (!file)
9064
+ return isBiped2(rig) ? avatarFromRig2(rig) : null;
9065
+ if (!existsSync11(file))
9066
+ throw new Error(`not found: ${file}`);
9067
+ const text = readFileSync12(file, "utf8");
9068
+ return /\.meta$/i.test(file) ? avatarFromUnity2(rig, parseUnityAvatar2(text)) : avatarFromFile2(rig, JSON.parse(text));
9069
+ };
9070
+ let humanoid;
7320
9071
  const mapPath = p.values.get("--map")?.[0];
7321
9072
  const autoOut = p.values.get("--auto-map");
7322
9073
  let spec;
7323
- if (autoOut !== undefined || mapPath === "auto") {
7324
- const { autoMap: autoMap2, describeSkeleton: describeSkeleton2 } = await Promise.resolve().then(() => exports_automap);
7325
- const am = autoMap2(target, donor);
7326
- log(`\uD83E\uDDED auto-map ${basename6(input)} ← ${basename6(donorPath)}`);
7327
- log(` target: ${describeSkeleton2(target, am.target)}`);
7328
- log(` donor: ${describeSkeleton2(donor, am.donor)}`);
7329
- for (const l of am.lines)
7330
- log(l);
7331
- spec = { _comment: `auto-mapped ${basename6(input)} ← ${basename6(donorPath)} by skeleton shape — review before trusting`, root: am.root, bones: am.map, legs: am.legs };
9074
+ const avatarFile = p.values.get("--avatar")?.[0], donorAvatarFile = p.values.get("--donor-avatar")?.[0];
9075
+ if (autoOut !== undefined || mapPath === "auto" || avatarFile || donorAvatarFile) {
9076
+ let tAv, dAv;
9077
+ try {
9078
+ tAv = loadAvatar(target, avatarFile);
9079
+ dAv = loadAvatar(donor, donorAvatarFile);
9080
+ } catch (e) {
9081
+ console.error(`✗ ${e.message}`);
9082
+ return 1;
9083
+ }
9084
+ if (tAv && dAv) {
9085
+ const hm = mapAvatars2(tAv, dAv);
9086
+ log(`\uD83E\uDDED humanoid map ${basename7(input)} ← ${basename7(donorPath)}`);
9087
+ for (const [label, rig, file] of [["target", target, avatarFile], ["donor", donor, donorAvatarFile]]) {
9088
+ try {
9089
+ log(` ${label}: ${file ? `avatar from ${basename7(file)} · ` : ""}${describeBiped2(rig, classifyBiped2(rig))}`);
9090
+ } catch {
9091
+ log(` ${label}: avatar from ${basename7(file ?? "?")} (shape did not classify)`);
9092
+ }
9093
+ }
9094
+ for (const l of describeHumanoidMap2(tAv, dAv, hm))
9095
+ log(l);
9096
+ humanoid = { target: tAv, donor: dAv };
9097
+ const { avatarToFile: avatarToFile2 } = await Promise.resolve().then(() => (init_humanoid(), exports_humanoid));
9098
+ spec = { _comment: `humanoid map ${basename7(input)} ← ${basename7(donorPath)} — target and donor avatars joined on the Unity/VRM canon; review before trusting`, root: hm.root, bones: hm.map, legs: hm.legs, twist: tAv.twist, avatars: { target: avatarToFile2(tAv), donor: avatarToFile2(dAv) } };
9099
+ } else if (tAv || dAv) {
9100
+ console.error(`✗ ${tAv ? "the donor" : "the target"} does not read as a humanoid while ${tAv ? "the target" : "the donor"} does — a biped↔quadruped retarget needs a hand-written map`);
9101
+ return 1;
9102
+ } else {
9103
+ const { autoMap: autoMap2, describeSkeleton: describeSkeleton2 } = await Promise.resolve().then(() => exports_automap);
9104
+ const am = autoMap2(target, donor);
9105
+ log(`\uD83E\uDDED auto-map ${basename7(input)} ← ${basename7(donorPath)}`);
9106
+ log(` target: ${describeSkeleton2(target, am.target)}`);
9107
+ log(` donor: ${describeSkeleton2(donor, am.donor)}`);
9108
+ for (const l of am.lines)
9109
+ log(l);
9110
+ spec = { _comment: `auto-mapped ${basename7(input)} ← ${basename7(donorPath)} by skeleton shape — review before trusting`, root: am.root, bones: am.map, legs: am.legs };
9111
+ }
7332
9112
  if (autoOut !== undefined) {
7333
- const out2 = autoOut[0] ?? input.replace(/\.glb$/i, "") + "-from-" + basename6(donorPath).replace(/\.[^.]+$/, "") + ".map.json";
9113
+ const out2 = autoOut[0] ?? input.replace(/\.glb$/i, "") + "-from-" + basename7(donorPath).replace(/\.[^.]+$/, "") + ".map.json";
7334
9114
  writeFileSync4(out2, JSON.stringify(spec, null, 2) + `
7335
9115
  `);
7336
- log(`\uD83D\uDCBE wrote ${out2} (${Object.keys(am.map).length} bones) — edit it, then: retarget … --map ${basename6(out2)}`);
9116
+ log(`\uD83D\uDCBE wrote ${out2} (${Object.keys(spec.bones).length} bones) — edit it, then: retarget … --map ${basename7(out2)}`);
7337
9117
  return 0;
7338
9118
  }
7339
9119
  } else {
@@ -7341,27 +9121,41 @@ donor clips: ${donor.clips.map((c) => `"${c.name}" ${c.duration.toFixed(2)}s`).j
7341
9121
  console.error("✗ --map <map.json> is required (--map auto / --auto-map [out.json] to derive one from the skeletons, --skeletons to list them)");
7342
9122
  return 2;
7343
9123
  }
7344
- if (!existsSync10(mapPath)) {
9124
+ if (!existsSync11(mapPath)) {
7345
9125
  console.error(`✗ not found: ${mapPath}`);
7346
9126
  return 1;
7347
9127
  }
7348
- spec = JSON.parse(readFileSync11(mapPath, "utf8"));
9128
+ spec = JSON.parse(readFileSync12(mapPath, "utf8"));
7349
9129
  if (!spec.bones) {
7350
9130
  console.error(`✗ ${mapPath} has no "bones" object`);
7351
9131
  return 2;
7352
9132
  }
9133
+ if (spec.avatars) {
9134
+ try {
9135
+ humanoid = { target: avatarFromFile2(target, spec.avatars.target), donor: avatarFromFile2(donor, spec.avatars.donor) };
9136
+ } catch (e) {
9137
+ log(` ⚠ ${basename7(mapPath)} carries avatars that do not fit these rigs (${e.message}) — plain map`);
9138
+ }
9139
+ }
7353
9140
  }
7354
9141
  checkUniformScale2(target);
7355
9142
  checkUniformScale2(donor);
7356
9143
  const t0 = performance.now();
7357
- log(`\uD83D\uDD01 ${basename6(input)} ← ${basename6(donorPath)}`);
9144
+ log(`\uD83D\uDD01 ${basename7(input)} ← ${basename7(donorPath)}`);
7358
9145
  let donorYawDeg = 0;
7359
9146
  {
7360
9147
  let fa = null, da = null;
7361
9148
  try {
7362
9149
  const { classify: classify2 } = await Promise.resolve().then(() => exports_automap);
7363
- fa = classify2(target);
7364
- da = classify2(donor);
9150
+ const facingOf = (rig) => {
9151
+ try {
9152
+ return classifyBiped2(rig);
9153
+ } catch {
9154
+ return classify2(rig);
9155
+ }
9156
+ };
9157
+ fa = facingOf(target);
9158
+ da = facingOf(donor);
7365
9159
  } catch {
7366
9160
  const spine = Object.entries(spec.bones);
7367
9161
  const headPair = spine.find(([t]) => /head/i.test(t)), tailPair = spine.find(([t]) => /tail|root|hip|pelvis/i.test(t));
@@ -7388,6 +9182,27 @@ donor clips: ${donor.clips.map((c) => `"${c.name}" ${c.duration.toFixed(2)}s`).j
7388
9182
  const prefix = p.values.get("--prefix")?.[0] ?? "";
7389
9183
  const names = p.values.get("--clip-names");
7390
9184
  let produced = 0;
9185
+ const tr = humanoid?.target.transfer;
9186
+ if (tr && Object.keys(tr).length) {
9187
+ const parts = [];
9188
+ if (tr.rootMotion)
9189
+ parts.push(`root motion ${tr.rootMotion}`);
9190
+ if (tr.plant !== undefined)
9191
+ parts.push(tr.plant ? "plant" : "no plant");
9192
+ if (tr.hands)
9193
+ parts.push("hands IK");
9194
+ if (tr.twist !== undefined)
9195
+ parts.push(`twist ${tr.twist}`);
9196
+ for (const s of ["Left", "Right"])
9197
+ if (tr.roll?.[s])
9198
+ parts.push(`${s[0]} roll ${tr.roll[s] > 0 ? "+" : ""}${tr.roll[s]}°`);
9199
+ const keep = Object.entries(tr.align ?? {}).filter(([, v]) => v === "keep").map(([k]) => k), al = Object.entries(tr.align ?? {}).filter(([, v]) => v === "align").map(([k]) => k);
9200
+ if (keep.length)
9201
+ parts.push(`keep ${keep.join(", ")}`);
9202
+ if (al.length)
9203
+ parts.push(`align ${al.join(", ")}`);
9204
+ log(` avatar transfer: ${parts.join(" · ")}`);
9205
+ }
7391
9206
  const rep = retarget2(target, donor, spec.bones, spec.root ?? null, {
7392
9207
  align: p.values.get("--align")?.[0] ?? "bone-direction",
7393
9208
  alignTo: spec.alignTo,
@@ -7397,35 +9212,51 @@ donor clips: ${donor.clips.map((c) => `"${c.name}" ${c.duration.toFixed(2)}s`).j
7397
9212
  const v = p.values.get("--donor-ref")?.[0];
7398
9213
  return v === undefined ? spec.donorRef : v === "bind" || v === "auto" ? v : { clip: v };
7399
9214
  })(),
7400
- ik: p.values.get("--ik")?.[0] === "none" || spec.ik === false ? "none" : "legs",
9215
+ ik: p.values.get("--ik")?.[0] === "none" || spec.ik === false ? "none" : p.values.get("--ik")?.[0] === "hands" || p.values.get("--ik") === undefined && tr?.hands ? "legs+hands" : "legs",
9216
+ rootMotion: p.values.get("--root-motion")?.[0] ?? tr?.rootMotion ?? "auto",
9217
+ rootRotation: p.values.get("--root-rotation")?.[0] ?? "none",
9218
+ plant: p.flags.has("--no-plant") ? false : tr?.plant ?? true,
9219
+ twist: (() => {
9220
+ const v = p.values.get("--twist")?.[0];
9221
+ return v === undefined ? tr?.twist !== undefined ? tr.twist > 0 ? tr.twist : false : 0.5 : v === "off" ? false : Number(v);
9222
+ })(),
7401
9223
  legs: spec.legs,
7402
9224
  fps: num2(p, "--fps"),
7403
9225
  rootScale: real(p, "--root-scale"),
7404
9226
  clips: p.values.get("--clips"),
7405
9227
  rename: names?.length || prefix ? (n) => prefix + (names?.[produced++] ?? n) : undefined,
9228
+ humanoid,
7406
9229
  warn: (w) => log(` ⚠ ${w}`)
7407
9230
  });
9231
+ if (humanoid)
9232
+ log(` humanoid: torso/legs keep, arms + fingers aligned (≥ 8°)${rep.roll.length ? ` · palm roll ${rep.roll.map((r) => `${r.side[0]} ${r.degrees.toFixed(0)}° (${r.via})`).join(", ")}` : ""}${rep.rootTranslateOnly ? " · root takes the hips' travel only" : ""}`);
7408
9233
  if (rep.bindGap.donor.worst > 0.001)
7409
9234
  log(` ℹ donor's node pose sits ${rep.bindGap.donor.worst.toFixed(3)}m from its bind pose (worst: ${rep.bindGap.donor.bone}) — anchoring on the bind pose`);
7410
- log(` ${rep.mapped.length} bones mapped · ${rep.align.length} direction-aligned (worst ${rep.align[0]?.degrees.toFixed(0) ?? 0}° on ${rep.align[0]?.bone ?? "-"}) · ${rep.kept.length} keep bind (${rep.kept.join(", ") || "-"})`);
9235
+ log(` ${rep.mapped.length} bones mapped · ${rep.align.length} direction-aligned (worst ${rep.align[0]?.degrees.toFixed(0) ?? 0}° on ${rep.align[0]?.bone ?? "-"})${rep.underThreshold.length ? ` · ${rep.underThreshold.length} under 8° kept (worst ${rep.underThreshold[0].degrees.toFixed(0)}° on ${rep.underThreshold[0].bone})` : ""} · ${rep.kept.length} keep bind (${rep.kept.join(", ") || "-"})`);
7411
9236
  log(` donor reference: ${rep.donorRef} (leg directions ${rep.legDisagreement.toFixed(0)}° apart between the binds) · target binds ${rep.stance ? "in a stance — legs keep it" : "posed — legs aligned to the donor"}`);
7412
9237
  log(` root ${rep.root ? `${rep.root.target}←${rep.root.donor} ×${+rep.rootScale.toPrecision(3)} (from ${rep.rootScaleFrom})` : "none"}${rep.ground ? ` · ground target ${rep.ground.target.toFixed(3)}m / donor ${rep.ground.donor.toFixed(3)}m` : ""}`);
7413
9238
  if (rep.legs.length)
7414
9239
  for (const l of rep.legs)
7415
- log(` \uD83E\uDDB6 ${l.effector.padEnd(14)} ← ${l.donorEffector.padEnd(12)} chain ${l.chain.join("→")} ×${l.ratio.toFixed(3)}${l.shortfall > 0.0001 ? ` ⚠ out of reach by up to ${(l.shortfall * 1000).toFixed(0)}mm` : ""}`);
9240
+ log(` ${l.isFoot ? "\uD83E\uDDB6" : "✋"} ${l.effector.padEnd(14)} ← ${l.donorEffector.padEnd(12)} chain ${l.chain.join("→")}${l.contact !== l.effector ? ` contact ${l.contact}` : ""} ×${l.ratio.toFixed(3)}${l.planted ? ` planted ${l.planted} frames` : ""}${l.shortfall > 0.0001 ? ` ⚠ out of reach by up to ${(l.shortfall * 1000).toFixed(0)}mm` : ""}`);
7416
9241
  else if (p.values.get("--ik")?.[0] !== "none" && spec.ik !== false)
7417
9242
  log(` ℹ no leg chains detected — feet follow FK only (name them in the map: "legs": [ … ])`);
9243
+ if (rep.root)
9244
+ log(` root motion: ${rep.rootMotion}${rep.hipsBob ? ` · hips carry the ${rep.hipsBob === "donor" ? "donor's own bob" : "vertical bob"}` : ""}`);
9245
+ if (rep.twist.length)
9246
+ log(` twist helpers: ${rep.twist.map((t) => `${t.bone} (${(t.at * 100).toFixed(0)}% of ${t.of})`).join(", ")}`);
7418
9247
  if (rep.bridges.length)
7419
9248
  log(` bridged (interpolated between mapped bones): ${rep.bridges.join(", ")}`);
7420
9249
  if (rep.unmappedTarget.length)
7421
9250
  log(` unmapped (left at rest): ${rep.unmappedTarget.join(", ")}`);
7422
- for (const c of rep.clips)
7423
- log(` \uD83C\uDF9E ${c.name.padEnd(22)} ${c.duration.toFixed(2)}s ${c.frames} frames → ${c.keys} keys`);
9251
+ for (const c of rep.clips) {
9252
+ const reach = c.events.filter((e) => e.kind === "reach"), below = c.events.filter((e) => e.kind === "below");
9253
+ log(` \uD83C\uDF9E ${c.name.padEnd(22)} ${c.duration.toFixed(2)}s ${c.frames} frames → ${c.keys} keys${reach.length ? ` ⚠ ${reach.length}× out of reach (${(Math.max(...reach.map((e) => e.value ?? 0)) * 1000).toFixed(0)} mm)` : ""}${below.length ? ` ⚠ ${below.length}× toe under the ground (${(Math.max(...below.map((e) => e.value ?? 0)) * 1000).toFixed(0)} mm)` : ""}`);
9254
+ }
7424
9255
  bindAccessors2(doc);
7425
9256
  const out = p.values.get("-o")?.[0] ?? input.replace(/\.glb$/i, ".retargeted.glb");
7426
9257
  const dir = dirname7(out);
7427
- if (dir && !existsSync10(dir))
7428
- mkdirSync4(dir, { recursive: true });
9258
+ if (dir && !existsSync11(dir))
9259
+ mkdirSync5(dir, { recursive: true });
7429
9260
  const glb = await writeGlb2(doc);
7430
9261
  writeFileSync4(out, glb);
7431
9262
  const { validateGlb: validateGlb2, formatReport: formatReport2 } = await Promise.resolve().then(() => (init_validateGlb(), exports_validateGlb));
@@ -7436,22 +9267,103 @@ donor clips: ${donor.clips.map((c) => `"${c.name}" ${c.duration.toFixed(2)}s`).j
7436
9267
  log(`\uD83D\uDCBE wrote ${out} (${(glb.byteLength / 1024 / 1024).toFixed(2)} MB, ${((performance.now() - t0) / 1000).toFixed(1)}s)`);
7437
9268
  return 0;
7438
9269
  };
9270
+ var cmdAvatar = async (p) => {
9271
+ const input = p.positionals[0];
9272
+ if (!input) {
9273
+ log(USAGE);
9274
+ return 2;
9275
+ }
9276
+ if (!existsSync11(input)) {
9277
+ console.error(`✗ not found: ${input}`);
9278
+ return 1;
9279
+ }
9280
+ const { loadRig: loadRig2, rigFromDocument: rigFromDocument2 } = await Promise.resolve().then(() => (init_rig(), exports_rig));
9281
+ const { classifyBiped: classifyBiped2, avatarFromBiped: avatarFromBiped2, avatarFromUnity: avatarFromUnity2, avatarToFile: avatarToFile2, parseUnityAvatar: parseUnityAvatar2, describeBiped: describeBiped2, HUMAN_BONES: HUMAN_BONES2 } = await Promise.resolve().then(() => (init_humanoid(), exports_humanoid));
9282
+ const { makeIO: makeIO3 } = await Promise.resolve().then(() => (init_convert(), exports_convert));
9283
+ const rig = /\.fbx$/i.test(input) ? await loadRig2(input) : rigFromDocument2(await makeIO3().read(input), basename7(input));
9284
+ let shape = null;
9285
+ try {
9286
+ const b = classifyBiped2(rig);
9287
+ shape = avatarFromBiped2(rig, b);
9288
+ log(`\uD83E\uDDCD ${basename7(input)}: ${describeBiped2(rig, b)}`);
9289
+ const groups = [
9290
+ ["torso", ["Hips", "Spine", "Chest", "UpperChest", "Neck", "Head", "Jaw", "LeftEye", "RightEye"]],
9291
+ ["left arm", ["LeftShoulder", "LeftUpperArm", "LeftLowerArm", "LeftHand"]],
9292
+ ["right arm", ["RightShoulder", "RightUpperArm", "RightLowerArm", "RightHand"]],
9293
+ ["left leg", ["LeftUpperLeg", "LeftLowerLeg", "LeftFoot", "LeftToes"]],
9294
+ ["right leg", ["RightUpperLeg", "RightLowerLeg", "RightFoot", "RightToes"]]
9295
+ ];
9296
+ const av = shape;
9297
+ for (const [g, keys] of groups)
9298
+ log(` ${g}: ${keys.filter((k) => av.bones[k]).map((k) => `${k}=${av.bones[k]}`).join(" ")}`);
9299
+ const fingers = HUMAN_BONES2.filter((k) => k.includes(" ") && shape.bones[k]);
9300
+ if (fingers.length)
9301
+ log(` fingers: ${fingers.length} phalanges (${fingers.filter((k) => k.endsWith("Proximal")).map((k) => shape.bones[k]).join(", ")})`);
9302
+ if (Object.keys(shape.twist).length)
9303
+ log(` twist helpers: ${Object.entries(shape.twist).map(([t, of]) => `${t}→${of}`).join(", ")}`);
9304
+ if (shape.bridged.length)
9305
+ log(` bridged: ${shape.bridged.join(", ")}`);
9306
+ if (b.helpers.length)
9307
+ log(` not part of the body (${b.helpers.length}): ${b.helpers.map((i) => rig.names[i]).slice(0, 16).join(", ")}${b.helpers.length > 16 ? " …" : ""}`);
9308
+ for (const n of shape.notes)
9309
+ log(` ⚠ ${n}`);
9310
+ } catch (e) {
9311
+ log(`✗ ${basename7(input)} does not read as a humanoid: ${e.message}`);
9312
+ }
9313
+ const metaPath = p.values.get("--meta")?.[0];
9314
+ let unity = null;
9315
+ if (metaPath) {
9316
+ if (!existsSync11(metaPath)) {
9317
+ console.error(`✗ not found: ${metaPath}`);
9318
+ return 1;
9319
+ }
9320
+ unity = avatarFromUnity2(rig, parseUnityAvatar2(readFileSync12(metaPath, "utf8")));
9321
+ for (const n of unity.notes)
9322
+ log(` ⚠ .meta: ${n}`);
9323
+ if (shape) {
9324
+ let agree = 0;
9325
+ const differ = [];
9326
+ for (const k of HUMAN_BONES2) {
9327
+ const a = shape.bones[k], m = unity.bones[k];
9328
+ if (a === m) {
9329
+ if (a)
9330
+ agree++;
9331
+ continue;
9332
+ }
9333
+ differ.push(`${k}: shape=${a ?? "-"} .meta=${m ?? "-"}`);
9334
+ }
9335
+ log(` vs ${basename7(metaPath)}: ${agree} bones agree${differ.length ? `, ${differ.length} differ:` : ""}`);
9336
+ for (const d of differ)
9337
+ log(` ${d}`);
9338
+ }
9339
+ }
9340
+ const chosen = unity ?? shape;
9341
+ if (!chosen)
9342
+ return 1;
9343
+ const out = p.values.get("-o")?.[0];
9344
+ if (out) {
9345
+ writeFileSync4(out, JSON.stringify(avatarToFile2(chosen), null, 2) + `
9346
+ `);
9347
+ log(`\uD83D\uDCBE wrote ${out} (${Object.keys(chosen.bones).length} bones, ${unity ? "Unity's map" : "from shape"}) — retarget … --avatar ${basename7(out)}`);
9348
+ }
9349
+ return 0;
9350
+ };
7439
9351
  var cmdUnpack = async (p) => {
7440
9352
  const pkg = p.positionals[0];
7441
9353
  if (!pkg) {
7442
9354
  log(USAGE);
7443
9355
  return 2;
7444
9356
  }
7445
- if (!existsSync10(pkg)) {
9357
+ if (!existsSync11(pkg)) {
7446
9358
  console.error(`✗ not found: ${pkg}`);
7447
9359
  return 1;
7448
9360
  }
7449
9361
  const { indexUnityPackage: indexUnityPackage2, writeUnityIndex: writeUnityIndex2, readUnityIndex: readUnityIndex2, extractUnityPackage: extractUnityPackage2, INDEX_NAME: INDEX_NAME2 } = await Promise.resolve().then(() => (init_unpack(), exports_unpack));
7450
- const outDir = p.values.get("-o")?.[0] ?? join9(dirname7(pkg), basename6(pkg, extname4(pkg)));
9362
+ const outDir = p.values.get("-o")?.[0] ?? join10(dirname7(pkg), basename7(pkg, extname5(pkg)));
7451
9363
  const t0 = performance.now();
7452
- const indexFile = join9(outDir, INDEX_NAME2);
9364
+ const indexFile = join10(outDir, INDEX_NAME2);
7453
9365
  let index;
7454
- if (existsSync10(indexFile)) {
9366
+ if (existsSync11(indexFile)) {
7455
9367
  index = readUnityIndex2(indexFile);
7456
9368
  log(`\uD83D\uDCC7 ${index.size} assets (cached ${indexFile})`);
7457
9369
  } else {
@@ -7480,14 +9392,14 @@ var cmdProbe = async (p) => {
7480
9392
  log(USAGE);
7481
9393
  return 2;
7482
9394
  }
7483
- if (!existsSync10(input)) {
9395
+ if (!existsSync11(input)) {
7484
9396
  console.error(`✗ not found: ${input}`);
7485
9397
  return 1;
7486
9398
  }
7487
9399
  const { probeFile: probeFile2 } = await Promise.resolve().then(() => (init_probe(), exports_probe));
7488
9400
  log(await probeFile2(input));
7489
9401
  if (p.values.has("--filmstrip")) {
7490
- if (extname4(input).toLowerCase() === ".fbx") {
9402
+ if (extname5(input).toLowerCase() === ".fbx") {
7491
9403
  console.error("✗ --filmstrip wants a GLB (convert first)");
7492
9404
  return 2;
7493
9405
  }
@@ -7518,7 +9430,7 @@ var cmdScene = async (p) => {
7518
9430
  log(USAGE);
7519
9431
  return 2;
7520
9432
  }
7521
- if (!existsSync10(input)) {
9433
+ if (!existsSync11(input)) {
7522
9434
  console.error(`✗ not found: ${input}`);
7523
9435
  return 1;
7524
9436
  }
@@ -7552,8 +9464,8 @@ var cmdScene = async (p) => {
7552
9464
  log(` ${l}`);
7553
9465
  const out = p.values.get("-o")?.[0] ?? input.replace(/\.unity$/i, "") + ".level.json";
7554
9466
  const dir = dirname7(out);
7555
- if (dir && !existsSync10(dir))
7556
- mkdirSync4(dir, { recursive: true });
9467
+ if (dir && !existsSync11(dir))
9468
+ mkdirSync5(dir, { recursive: true });
7557
9469
  writeFileSync4(out, JSON.stringify(r, null, 1) + `
7558
9470
  `);
7559
9471
  log(`\uD83D\uDCBE wrote ${out} (${r.instances.length} instances, ${((performance.now() - t0) / 1000).toFixed(1)}s) — frame: ${r.frame}`);
@@ -7566,8 +9478,8 @@ var resolveInputs = (args) => {
7566
9478
  out.push(f);
7567
9479
  };
7568
9480
  const walk = (dir, recursive, pattern) => {
7569
- for (const entry of readdirSync5(dir, { withFileTypes: true })) {
7570
- const p = join9(dir, entry.name).replace(/\\/g, "/");
9481
+ for (const entry of readdirSync6(dir, { withFileTypes: true })) {
9482
+ const p = join10(dir, entry.name).replace(/\\/g, "/");
7571
9483
  if (entry.isDirectory()) {
7572
9484
  if (recursive)
7573
9485
  walk(p, true, pattern);
@@ -7585,9 +9497,9 @@ var resolveInputs = (args) => {
7585
9497
  if (recursive)
7586
9498
  segs.pop();
7587
9499
  const dir = segs.join("/") || ".";
7588
- if (existsSync10(dir))
9500
+ if (existsSync11(dir))
7589
9501
  walk(dir, recursive, toRegex(last));
7590
- } else if (existsSync10(a) && statSync6(a).isDirectory()) {
9502
+ } else if (existsSync11(a) && statSync7(a).isDirectory()) {
7591
9503
  walk(a.replace(/\/+$/, ""), false, /\.glb$/i);
7592
9504
  } else
7593
9505
  out.push(a);
@@ -7608,12 +9520,13 @@ var cmdDoctor = async (p) => {
7608
9520
  const sh = shadingOpts(p);
7609
9521
  const sc = scaleOpts(p);
7610
9522
  const lmo = lightmapOpts(p);
9523
+ const lod = lodOpts(p);
7611
9524
  const wantsScale = sc.fixScale || sc.scale !== undefined || sc.scaleTo !== undefined;
7612
- const doFix = p.flags.has("--fix") || useKtx2 || fixMetallic || wantsScale || sh.unlit || sh.fixEmissive || lmo.lightmapUv;
9525
+ const doFix = p.flags.has("--fix") || useKtx2 || fixMetallic || wantsScale || sh.unlit || sh.fixEmissive || lmo.lightmapUv || lod.lod.length > 0;
7613
9526
  const dumpMode = p.values.has("--dump");
7614
9527
  const dumpVal = p.values.get("--dump")?.[0] ?? null;
7615
9528
  const oVal = p.values.get("-o")?.[0] ?? null;
7616
- const opts = { ktx2: useKtx2, ktx2Hq: p.flags.has("--ktx2-hq"), ktx2Uastc: p.flags.has("--ktx2-uastc"), maxSize: num2(p, "--max-size"), target: num2(p, "--target"), ...sc, ...sh, ...lmo };
9529
+ const opts = { ktx2: useKtx2, ktx2Hq: p.flags.has("--ktx2-hq"), ktx2Uastc: p.flags.has("--ktx2-uastc"), maxSize: num2(p, "--max-size"), target: num2(p, "--target"), ...sc, ...sh, ...lmo, ...lod };
7617
9530
  let toktx = "";
7618
9531
  if (useKtx2) {
7619
9532
  toktx = findToktx2();
@@ -7631,7 +9544,7 @@ var cmdDoctor = async (p) => {
7631
9544
  log(`Processing ${files.length} files…`);
7632
9545
  let exitCode = 0;
7633
9546
  for (const input of files) {
7634
- if (!existsSync10(input)) {
9547
+ if (!existsSync11(input)) {
7635
9548
  console.error(`
7636
9549
  ✗ not found: ${input}`);
7637
9550
  exitCode = 1;
@@ -7667,13 +9580,23 @@ var cmdDoctor = async (p) => {
7667
9580
  log(` baked scale ×${+res.scaled.toPrecision(6)}`);
7668
9581
  if (res.lightmapUv)
7669
9582
  log(` lightmap UV: ${res.lightmapUv.charts} charts over ${res.lightmapUv.primitives} primitive(s) (xatlas ${res.lightmapUv.width}×${res.lightmapUv.height}); vertices ${res.lightmapUv.verticesBefore} → ${res.lightmapUv.verticesAfter}`);
9583
+ if (res.lod) {
9584
+ for (const t of res.lod.tris)
9585
+ log(` lod: ${t.name} ${t.levels.join(" / ")} tris`);
9586
+ for (const b of res.lod.bones)
9587
+ log(` lod bones: ${b.name} last level ${b.before} → ${b.after} joints`);
9588
+ for (const k of res.lod.skipped)
9589
+ log(` lod: ${k.name} level ${k.level} dropped — the simplifier reached only ${k.tris} tris (seams / hard surfaces; try a smaller ratio)`);
9590
+ if (!res.lod.nodes)
9591
+ log(` lod: no mesh with >= 2000 triangles (or the _LOD nodes are already there)`);
9592
+ }
7670
9593
  const output = outputFor2(input, oVal, files.length > 1, useKtx2);
7671
9594
  const root = doc.getRoot();
7672
9595
  if (!root.getDefaultScene() && root.listScenes().length > 0)
7673
9596
  root.setDefaultScene(root.listScenes()[0]);
7674
9597
  await io.write(output, doc);
7675
- const beforeBytes = statSync6(input).size;
7676
- const after = readFileSync11(output);
9598
+ const beforeBytes = statSync7(input).size;
9599
+ const after = readFileSync12(output);
7677
9600
  log(`\uD83D\uDCBE wrote ${output} (${kb2(beforeBytes)} → ${kb2(after.byteLength)})`);
7678
9601
  log(formatReport2(validateGlb2(after), output));
7679
9602
  }
@@ -7704,6 +9627,42 @@ var cmdSky = async (p) => {
7704
9627
  return 1;
7705
9628
  }
7706
9629
  };
9630
+ var cmdTerrainPack = async (p) => {
9631
+ if (p.positionals.length === 0) {
9632
+ console.error("✗ usage: lecodes-assets terrain-pack <layer> [<layer> …] [-o dir] [--normals n0 n1 …] [--names a b …] [--size N] [--png] [--flip-y]");
9633
+ return 2;
9634
+ }
9635
+ const { resolveTerrainLayer: resolveTerrainLayer2, packTerrainTextures: packTerrainTextures2 } = await Promise.resolve().then(() => (init_terrainPack(), exports_terrainPack));
9636
+ const normals = p.values.get("--normals") ?? [];
9637
+ const names = p.values.get("--names") ?? [];
9638
+ const outDir = resolve6(p.values.get("-o")?.[0] ?? "terrain");
9639
+ try {
9640
+ const layers = p.positionals.map((input, i) => {
9641
+ const n = normals[i];
9642
+ return resolveTerrainLayer2(input, n && n !== "-" ? n : undefined, names[i]);
9643
+ });
9644
+ log(`
9645
+ \uD83C\uDFD4 ${layers.length} layer(s) → ${outDir}`);
9646
+ const res = await packTerrainTextures2(layers, {
9647
+ outDir,
9648
+ size: num2(p, "--size"),
9649
+ png: p.flags.has("--png"),
9650
+ flipY: p.flags.has("--flip-y"),
9651
+ quality: num2(p, "--quality"),
9652
+ log
9653
+ });
9654
+ log(`
9655
+ Terrain.create({ …,
9656
+ ${res.snippet.split(`
9657
+ `).map((l) => " " + l).join(`
9658
+ `)}
9659
+ }) (asset() paths relative to the calling file)`);
9660
+ return 0;
9661
+ } catch (e) {
9662
+ console.error(`✗ ${e.message}`);
9663
+ return 1;
9664
+ }
9665
+ };
7707
9666
  var runAssetsCli = async (argv) => {
7708
9667
  const [cmd, ...rest] = argv;
7709
9668
  const p = parse(rest);
@@ -7718,8 +9677,12 @@ var runAssetsCli = async (argv) => {
7718
9677
  return cmdDoctor(p);
7719
9678
  case "retarget":
7720
9679
  return cmdRetarget(p);
9680
+ case "avatar":
9681
+ return cmdAvatar(p);
7721
9682
  case "sky":
7722
9683
  return cmdSky(p);
9684
+ case "terrain-pack":
9685
+ return cmdTerrainPack(p);
7723
9686
  case "scene":
7724
9687
  return cmdScene(p);
7725
9688
  default:
@@ -7745,22 +9708,129 @@ init_validateGlb();
7745
9708
  init_normalizeScale();
7746
9709
  init_rig();
7747
9710
  init_retarget();
9711
+
9712
+ // src/retarget/inPlace.ts
9713
+ var qmul3 = (a, b) => [
9714
+ a[3] * b[0] + a[0] * b[3] + a[1] * b[2] - a[2] * b[1],
9715
+ a[3] * b[1] - a[0] * b[2] + a[1] * b[3] + a[2] * b[0],
9716
+ a[3] * b[2] + a[0] * b[1] - a[1] * b[0] + a[2] * b[3],
9717
+ a[3] * b[3] - a[0] * b[0] - a[1] * b[1] - a[2] * b[2]
9718
+ ];
9719
+ var qinv2 = (q) => [-q[0], -q[1], -q[2], q[3]];
9720
+ var headingOf = (q) => Math.atan2(2 * (q[0] * q[2] + q[3] * q[1]), 1 - 2 * (q[0] * q[0] + q[1] * q[1]));
9721
+ var makeInPlace = (doc, animationName, rootNodeName, opts = {}) => {
9722
+ const anim = doc.getRoot().listAnimations().find((a) => a.getName() === animationName);
9723
+ if (!anim)
9724
+ throw new Error(`makeInPlace: no animation "${animationName}"`);
9725
+ const channel = anim.listChannels().find((c) => c.getTargetPath() === "translation" && c.getTargetNode()?.getName() === rootNodeName);
9726
+ if (!channel)
9727
+ return null;
9728
+ const node = channel.getTargetNode();
9729
+ const [rx, , rz] = node.getTranslation();
9730
+ const sampler = channel.getSampler();
9731
+ const input = sampler.getInput(), output = sampler.getOutput();
9732
+ const times = Array.from(input.getArray());
9733
+ const values = new Float32Array(output.getArray());
9734
+ const travel = [];
9735
+ for (let k = 0;k < times.length; k++) {
9736
+ travel.push([values[k * 3] - rx, values[k * 3 + 2] - rz]);
9737
+ values[k * 3] = rx;
9738
+ values[k * 3 + 2] = rz;
9739
+ }
9740
+ output.setArray(values);
9741
+ const speed = times.map((_, k) => {
9742
+ const a = Math.max(0, k - 1), b = Math.min(times.length - 1, k + 1);
9743
+ const dt = times[b] - times[a];
9744
+ return dt > 0 ? Math.hypot(travel[b][0] - travel[a][0], travel[b][1] - travel[a][1]) / dt : 0;
9745
+ });
9746
+ const yaw = [];
9747
+ if (opts.yaw) {
9748
+ const rc = anim.listChannels().find((c) => c.getTargetPath() === "rotation" && c.getTargetNode() === node);
9749
+ if (rc) {
9750
+ const rs = rc.getSampler();
9751
+ const rt = Array.from(rs.getInput().getArray());
9752
+ const rv = new Float32Array(rs.getOutput().getArray());
9753
+ const rest = node.getRotation();
9754
+ const at = (t) => {
9755
+ let i = 0;
9756
+ while (i < rt.length - 2 && rt[i + 1] <= t)
9757
+ i++;
9758
+ const t0 = rt[i], t1 = rt[i + 1] ?? t0, u = t1 > t0 ? Math.min(1, Math.max(0, (t - t0) / (t1 - t0))) : 0;
9759
+ const a = Array.from(rv.subarray(i * 4, i * 4 + 4)), b = rt.length > i + 1 ? Array.from(rv.subarray(i * 4 + 4, i * 4 + 8)) : a;
9760
+ const s = a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3] < 0 ? -1 : 1;
9761
+ const q = [a[0] + (s * b[0] - a[0]) * u, a[1] + (s * b[1] - a[1]) * u, a[2] + (s * b[2] - a[2]) * u, a[3] + (s * b[3] - a[3]) * u];
9762
+ const n = Math.hypot(...q) || 1;
9763
+ return [q[0] / n, q[1] / n, q[2] / n, q[3] / n];
9764
+ };
9765
+ let acc = 0, prev = 0;
9766
+ for (let k = 0;k < times.length; k++) {
9767
+ const h = headingOf(qmul3(at(times[k]), qinv2(rest)));
9768
+ if (k > 0) {
9769
+ let d = h - prev;
9770
+ d = Math.atan2(Math.sin(d), Math.cos(d));
9771
+ acc += d;
9772
+ }
9773
+ prev = h;
9774
+ yaw.push(acc * 180 / Math.PI);
9775
+ }
9776
+ const base = yaw[0] ?? 0;
9777
+ for (let k = 0;k < yaw.length; k++)
9778
+ yaw[k] = yaw[k] - base;
9779
+ for (let k = 0;k < rt.length; k++) {
9780
+ rv[k * 4] = rest[0];
9781
+ rv[k * 4 + 1] = rest[1];
9782
+ rv[k * 4 + 2] = rest[2];
9783
+ rv[k * 4 + 3] = rest[3];
9784
+ }
9785
+ rs.getOutput().setArray(rv);
9786
+ }
9787
+ }
9788
+ const extras = anim.getExtras();
9789
+ const lecodes = { ...extras.lecodes ?? {}, inPlace: true };
9790
+ anim.setExtras({ ...extras, lecodes });
9791
+ return { times, travel, speed, yaw };
9792
+ };
9793
+ var resample = (times, series, hz) => {
9794
+ const end = times[times.length - 1] ?? 0;
9795
+ const n = Math.ceil(end * hz);
9796
+ const out = [];
9797
+ for (let k = 0;k <= n; k++) {
9798
+ const t = Math.min(end, k / hz);
9799
+ let i = 0;
9800
+ while (i < times.length - 2 && times[i + 1] <= t)
9801
+ i++;
9802
+ const t0 = times[i], t1 = times[i + 1] ?? t0;
9803
+ const u = t1 > t0 ? (t - t0) / (t1 - t0) : 0;
9804
+ out.push(series[i] + ((series[i + 1] ?? series[i]) - series[i]) * u);
9805
+ }
9806
+ return out;
9807
+ };
9808
+ var speedProfile = (r, hz = 30) => resample(r.times, r.speed, hz);
9809
+ var yawProfile = (r, hz = 30) => r.yaw.length ? resample(r.times, r.yaw, hz) : [];
9810
+
9811
+ // src/index.ts
9812
+ init_terrainPack();
7748
9813
  export {
9814
+ yawProfile,
7749
9815
  writeGlb,
7750
9816
  validateGlb,
7751
9817
  toDocument,
9818
+ speedProfile,
7752
9819
  sceneBounds,
7753
9820
  runAssetsCli,
7754
9821
  rigFromFbx,
7755
9822
  rigFromDocument,
7756
9823
  retarget,
9824
+ resolveTerrainLayer,
7757
9825
  reportPlan,
7758
9826
  readFbx,
7759
9827
  probeFile,
7760
9828
  probeFbx,
7761
9829
  probeDocument,
9830
+ packTerrainTextures,
7762
9831
  mergeClipScene,
7763
9832
  measureScale,
9833
+ makeInPlace,
7764
9834
  makeIO,
7765
9835
  loadRig,
7766
9836
  formatReport,