@rig-c/pose 0.25.0 → 0.26.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.
- package/README.md +4 -2
- package/package.json +1 -1
- package/src/map.ts +51 -2
- package/src/retarget.ts +409 -8
- package/src/switch.ts +12 -2
package/README.md
CHANGED
|
@@ -349,10 +349,12 @@ Bone rotation cannot carry every pose: an arm behind the head, a forearm toward
|
|
|
349
349
|
|
|
350
350
|
- `slots`: what each slot of the set shows while it shows; each attachment must be in that slot under some skin of the rig, as rig-c built it. `hides`: the slots that are empty while it shows. A slot's place in the draw order is the rig's own, so a drawing that must sit in front of the torso is an attachment in a slot that is: a switch is attachment keys only, and no draw-order key is written.
|
|
351
351
|
- `chains`: the map's chains whose pose the set depicts; it is selected by them. `pose`: the key skeleton the art was painted from, body-18 (or `mid_hip`/`mid_shoulder`) in picture px, y down, any scale and place — only its directions are read; every joint the listed chains use must be in it.
|
|
352
|
-
- `drawn` (optional): each bone's world angle (Spine degrees) as painted. It changes no key: while a set shows, a bone is keyed to the pose as always, and the art turns with it. What it gives is a figure: the art turn, how far each bone is from the angle its art was painted at (`artTurn`, and `ART-RANGE` past the bone's range).
|
|
352
|
+
- `drawn` (optional): each bone's world angle (Spine degrees) as painted. It changes no key unless the set declares `symmetric` or `support` ([below](#alternate-drawings-alternates)): otherwise, while a set shows, a bone is keyed to the pose as always, and the art turns with it. What it gives is a figure: the art turn, how far each bone is from the angle its art was painted at (`artTurn`, and `ART-RANGE` past the bone's range).
|
|
353
353
|
- `offsets` (optional): a translate per bone, in its parent's axes, held while the set shows and 0 elsewhere (stepped keys), so the joints as painted are the pivots. A bone `translate` already keys (the hip) cannot take one.
|
|
354
354
|
- `place` (optional): `"by-hip"` places a hand that meets the body by its offset from the hip ([below](#alternate-drawings-alternates)).
|
|
355
|
-
-
|
|
355
|
+
- `symmetric` (optional, [#188](https://github.com/rig-c/pose/issues/188)): `true` normalises a two-arm symmetric gesture set (such as hands behind the head) to its `drawn` symmetry centre so both elbows sit at the same world height and neither wrist extends past the head silhouette ([below](#alternate-drawings-alternates)). Requires two 2-bone chains with `drawn` angles on all four bones (`MAP_ALT_SYMMETRIC`).
|
|
356
|
+
- `support` (optional, [#188](https://github.com/rig-c/pose/issues/188)): a chain name or non-empty list of chain names of the set (`"support": "arm_l"` or `["arm_l"]`) whose bones carry a stationary support arm (such as a waist-resting hand while the other arm gestures); while the set shows, each support chain's bones anchor to `drawn` within `±2.5°` (`≤ ±3°`) of the pose's delta relative to the set's reference pose, and an unplaced active raised arm in the same set clears the head/bun outward along the 2D shoulder→wrist direction (`MAP_ALT_SUPPORT`).
|
|
357
|
+
- Each name resolves against the built rig and the map, and a miss is refused by name, listing what exists: `MAP_ALT_SLOT`, `MAP_ALT_ATTACHMENT`, `MAP_ALT_HIDES`, `MAP_ALT_CHAIN`, `MAP_ALT_JOINT`, `MAP_ALT_POSE`, `MAP_ALT_DRAWN`, `MAP_ALT_OFFSET` (a bone that is not a bone of the set's chains), `MAP_ALT_PLACE`, `MAP_ALT_SYMMETRIC` and `MAP_ALT_SUPPORT`.
|
|
356
358
|
|
|
357
359
|
**When one shows.** Per frame and per set, the distance is the largest difference, in degrees, between a segment of its chains as the retarget asked for it (after holds, the short hold, the flip guard and the hinge check; before any range clamp) and the same segment in its key skeleton, with a foreshortened segment (`SHORT`) weighted at `0.5 ×` (`SHORT_SWITCH_WEIGHT`, [#180](https://github.com/rig-c/pose/issues/180)). On each frame the nearest set within `--switch-tolerance` (default 25°) shows, ties going to the map's order; once entered, a set stays active until its distance exceeds `--switch-tolerance + 15°` (`SWITCH_LEAVE_HYSTERESIS`, [#180](https://github.com/rig-c/pose/issues/180)) for at least `--switch-min` (default 0.25 s) or a competing set with a lower distance enters within `--switch-tolerance`; a run of one set shorter than `--switch-min` is dropped and the base art shows there. Sets compete only with those they share a slot (shown or hidden) or an offset bone with, so a drawing of each arm can show at once; two that compete never overlap.
|
|
358
360
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/pose",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"description": "Puts motion from a body-18 pose sequence onto a rig built with rig-c: bone keys in rig-c's motion format, checked through rig-c's gate before anything is written. A CLI for agents that cannot see the rig.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/map.ts
CHANGED
|
@@ -100,6 +100,10 @@ export interface AlternateSet {
|
|
|
100
100
|
frames: Array<[number, number]> | null;
|
|
101
101
|
/** `place: "by-hip"` (rig-c/pose#93): the chains whose wrist `pose` puts within one hip width of the mid-hip, their wrists placed by the offset from the hip while the set shows. Absent or null: the pose's angles. */
|
|
102
102
|
place?: { by: 'hip'; chains: string[] } | null;
|
|
103
|
+
/** `symmetric: true` (rig-c/pose#188): normalise the set's two two-bone arm chains to the drawn symmetry centre and keep their wrists inside the head silhouette while the set shows. */
|
|
104
|
+
symmetric?: boolean;
|
|
105
|
+
/** `support` (rig-c/pose#188): the set's chains whose bones anchor to the set's `drawn` local angles while the non-support chains track the active gesture. */
|
|
106
|
+
support?: string[];
|
|
103
107
|
}
|
|
104
108
|
export interface YawSet {
|
|
105
109
|
set: string;
|
|
@@ -435,7 +439,7 @@ function readAlternates(p: Problems, where: string, raw: unknown, chains: readon
|
|
|
435
439
|
p.add('MAP_ALT_SHAPE', `${aw} is ${typeName(a)}; a set is ${SET_SHAPE}`);
|
|
436
440
|
return;
|
|
437
441
|
}
|
|
438
|
-
refuseUnknownKeys(p, 'MAP_KEYS', aw, a, ['name', 'note', 'slots', 'hides', 'chains', 'pose', 'drawn', 'offsets', 'turn', 'frames', 'place']);
|
|
442
|
+
refuseUnknownKeys(p, 'MAP_KEYS', aw, a, ['name', 'note', 'slots', 'hides', 'chains', 'pose', 'drawn', 'offsets', 'turn', 'frames', 'place', 'symmetric', 'support']);
|
|
439
443
|
const name = typeof a.name === 'string' && a.name !== '' ? a.name : null;
|
|
440
444
|
if (name === null) p.add('MAP_ALT_NAME', `${aw} "name" is ${typeName(a.name)}; it must be a non-empty string`);
|
|
441
445
|
else if (names.has(name)) p.add('MAP_ALT_NAME', `${aw} "name" "${name}" names two sets`);
|
|
@@ -553,7 +557,52 @@ function readAlternates(p: Problems, where: string, raw: unknown, chains: readon
|
|
|
553
557
|
else place = { by: 'hip', chains: by.chains };
|
|
554
558
|
}
|
|
555
559
|
}
|
|
556
|
-
|
|
560
|
+
let symmetric: boolean | undefined;
|
|
561
|
+
if ('symmetric' in a) {
|
|
562
|
+
if (a.symmetric !== true) {
|
|
563
|
+
p.add('MAP_ALT_SYMMETRIC', `${label} "symmetric" is ${JSON.stringify(a.symmetric)}; it must be true`);
|
|
564
|
+
ok = false;
|
|
565
|
+
} else if (own.length !== 2 || !own.every((c) => c.bones.length === 2)) {
|
|
566
|
+
p.add('MAP_ALT_SYMMETRIC', `${label} "symmetric" is true, and its chains are ${ownList}; a symmetric set normalises a pair of two-bone arm chains`);
|
|
567
|
+
ok = false;
|
|
568
|
+
} else {
|
|
569
|
+
const missing = ownBones.filter((b) => !(b in drawn));
|
|
570
|
+
if (missing.length > 0) {
|
|
571
|
+
p.add('MAP_ALT_SYMMETRIC', `${label} "symmetric" is true, and "drawn" has no ${missing.map((b) => `"${b}"`).join(', ')}; a symmetric set normalises both chains around their drawn angles`);
|
|
572
|
+
ok = false;
|
|
573
|
+
} else symmetric = true;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
let support: string[] | undefined;
|
|
577
|
+
if ('support' in a) {
|
|
578
|
+
const sup = typeof a.support === 'string' && a.support !== '' ? [a.support] : strings(a.support);
|
|
579
|
+
if (sup === null || sup.length === 0 || sup.some((s) => s === '')) {
|
|
580
|
+
p.add('MAP_ALT_SUPPORT', `${label} "support" is ${JSON.stringify(a.support)}; it must name one or more of the set's chains (${(cs ?? []).join(', ') || 'none'})`);
|
|
581
|
+
ok = false;
|
|
582
|
+
} else {
|
|
583
|
+
if (new Set(sup).size !== sup.length) {
|
|
584
|
+
p.add('MAP_ALT_SUPPORT', `${label} "support" names a chain twice: ${sup.join(', ')}`);
|
|
585
|
+
ok = false;
|
|
586
|
+
}
|
|
587
|
+
const outside = sup.filter((c) => !(cs ?? []).includes(c));
|
|
588
|
+
if (outside.length > 0) {
|
|
589
|
+
p.add('MAP_ALT_SUPPORT', `${label} "support" names chain ${outside.map((c) => `"${c}"`).join(', ')}, which is not in the set's chains (${(cs ?? []).join(', ') || 'none'})`);
|
|
590
|
+
ok = false;
|
|
591
|
+
}
|
|
592
|
+
if (a.symmetric === true) {
|
|
593
|
+
p.add('MAP_ALT_SUPPORT', `${label} names both "symmetric" and "support"; a set either normalises both arms symmetrically or anchors a support arm`);
|
|
594
|
+
ok = false;
|
|
595
|
+
}
|
|
596
|
+
const supBones = own.filter((c) => sup.includes(c.name)).flatMap((c) => c.bones);
|
|
597
|
+
const missing = supBones.filter((b) => !(b in drawn));
|
|
598
|
+
if (missing.length > 0) {
|
|
599
|
+
p.add('MAP_ALT_SUPPORT', `${label} "support" chain has no "drawn" angle for ${missing.map((b) => `"${b}"`).join(', ')}; a support chain anchors to its drawn angles`);
|
|
600
|
+
ok = false;
|
|
601
|
+
}
|
|
602
|
+
if (ok) support = sup;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
if (ok && name !== null && cs !== null) out.push({ name, slots, hides, chains: cs, pose, drawn, offsets, turn, frames, ...(place === null ? {} : { place }), ...(symmetric === undefined ? {} : { symmetric }), ...(support === undefined ? {} : { support }) });
|
|
557
606
|
});
|
|
558
607
|
return out;
|
|
559
608
|
}
|
package/src/retarget.ts
CHANGED
|
@@ -1157,6 +1157,7 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1157
1157
|
handOffsets.set(c.name, arr);
|
|
1158
1158
|
}
|
|
1159
1159
|
}
|
|
1160
|
+
const rawTargets = new Map<string, number[]>([...targets].map(([b, arr]) => [b, [...arr]]));
|
|
1160
1161
|
const sw = selectSwitches(map, targets, keyTimes, keyTimes[n - 1]!, slots, { ...opts, shortInfo, handOffsets });
|
|
1161
1162
|
|
|
1162
1163
|
// The range clamp, outside the spans of a set depicting the bone's chain. Parents
|
|
@@ -1326,6 +1327,236 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1326
1327
|
const raisedJobs: Array<{ anchor: Anchor; sw: Switch; chain: Chain; wrist: (i: number) => Pt; before: Pt[]; reference: Pt[]; placed: Pt[]; refEye: number; rigEye: number; short: Array<{ i: number; how: 'far' | 'near' | 'missed'; shortfall: number }>; upperHeld: number[]; l0: number; l1: number; farthest: number }> = [];
|
|
1327
1328
|
const headOk: Anchor | null = head !== null && !('problem' in head) ? { kind: 'head', joints: head.joints, points: head.points } : null;
|
|
1328
1329
|
const hipsOk: Anchor | null = hips !== null && !('problem' in hips) ? { kind: 'hip', joints: hips.joints, points: hips.points } : null;
|
|
1330
|
+
const fallbackHead = headOf(map);
|
|
1331
|
+
const gestureHead: Anchor | null = headOk ?? (!('problem' in fallbackHead) ? { kind: 'head', joints: fallbackHead.joints, points: fallbackHead.points } : null);
|
|
1332
|
+
/**
|
|
1333
|
+
* #188: For an active raised chain in a set that declares a `support` arm, compute the
|
|
1334
|
+
* outward-cleared world angles `[a0, a1]` on frame `i` from the scan's 2D shoulder→wrist
|
|
1335
|
+
* direction and the head/bun clearance.
|
|
1336
|
+
*/
|
|
1337
|
+
const solveSupportActiveFrame = (set: (typeof map.alternates)[number], c: Chain, i: number): [number, number] | null => {
|
|
1338
|
+
if (c.bones.length < 2 || c.joints.length < 3) return null;
|
|
1339
|
+
const [b0, b1] = c.bones as [string, string];
|
|
1340
|
+
const drawn0 = set.drawn[b0];
|
|
1341
|
+
const drawn1 = set.drawn[b1];
|
|
1342
|
+
if (drawn0 === undefined || drawn1 === undefined || !isRaised(drawn0)) return null;
|
|
1343
|
+
const S0 = rigPoint(rest, b0);
|
|
1344
|
+
const E0 = rigPoint(rest, b1);
|
|
1345
|
+
const wSpec = c.aim[1] === 'x' ? `${b1}:tip` : c.aim[1]!;
|
|
1346
|
+
const W0 = rigPoint(rest, wSpec);
|
|
1347
|
+
if (S0 === null || E0 === null || W0 === null) return null;
|
|
1348
|
+
const l0 = Math.hypot(E0[0] - S0[0], E0[1] - S0[1]);
|
|
1349
|
+
const l1 = Math.hypot(W0[0] - E0[0], W0[1] - E0[1]);
|
|
1350
|
+
let rigEye0 = 0.32 * l0;
|
|
1351
|
+
let headX0 = 0;
|
|
1352
|
+
if (gestureHead !== null) {
|
|
1353
|
+
const ha0 = rigPoint(rest, gestureHead.points[0]);
|
|
1354
|
+
const hb0 = rigPoint(rest, gestureHead.points[1]);
|
|
1355
|
+
if (ha0 !== null && hb0 !== null) {
|
|
1356
|
+
const d = Math.hypot(hb0[0] - ha0[0], hb0[1] - ha0[1]);
|
|
1357
|
+
if (d > 1e-6) {
|
|
1358
|
+
rigEye0 = d;
|
|
1359
|
+
headX0 = (ha0[0] + hb0[0]) / 2;
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
const Ex = S0[0] + l0 * Math.cos(drawn0 / DEG);
|
|
1364
|
+
const outSign: 1 | -1 = Ex <= headX0 ? 1 : -1;
|
|
1365
|
+
const clear0 = Math.asin(Math.min(1, (0.55 * rigEye0) / Math.max(1e-6, l0))) * DEG;
|
|
1366
|
+
const clear1 = Math.asin(Math.min(1, (0.90 * rigEye0) / Math.max(1e-6, l1))) * DEG;
|
|
1367
|
+
let swMod = 0;
|
|
1368
|
+
const j0 = c.joints[0]!;
|
|
1369
|
+
const jLast = c.joints[c.joints.length - 1]!;
|
|
1370
|
+
const kp0 = set.pose[j0];
|
|
1371
|
+
const kpLast = set.pose[jLast];
|
|
1372
|
+
const shS = series.get(j0);
|
|
1373
|
+
const wrS = series.get(jLast);
|
|
1374
|
+
if (kp0 !== undefined && kpLast !== undefined && shS !== undefined && wrS !== undefined) {
|
|
1375
|
+
const keySW = pictureAngle(kp0, kpLast);
|
|
1376
|
+
const scanSW = pictureAngle(shS[i]!, wrS[i]!);
|
|
1377
|
+
swMod = Math.max(0, Math.min(6, outSign * wrap(scanSW - keySW)));
|
|
1378
|
+
}
|
|
1379
|
+
const lim0 = set.turn[b0];
|
|
1380
|
+
const lim1 = set.turn[b1];
|
|
1381
|
+
const off0 = outSign * (clear0 + 0.5 * swMod);
|
|
1382
|
+
const off1 = outSign * (clear1 + 0.5 * swMod);
|
|
1383
|
+
const cl0 = lim0 !== undefined ? Math.min(Math.max(off0, lim0[0]), lim0[1]) : off0;
|
|
1384
|
+
const cl1 = lim1 !== undefined ? Math.min(Math.max(off1, lim1[0]), lim1[1]) : off1;
|
|
1385
|
+
return [wrap(drawn0 + cl0), wrap(drawn1 + cl1)];
|
|
1386
|
+
};
|
|
1387
|
+
/**
|
|
1388
|
+
* #188: For a support chain in `set.support`, anchor its bones on frame `i` to `set.drawn`
|
|
1389
|
+
* within a ±2.5° (≤ ±3°) breathing tolerance from the pose's delta relative to the set's
|
|
1390
|
+
* reference pose.
|
|
1391
|
+
*/
|
|
1392
|
+
const solveSupportChainFrame = (set: (typeof map.alternates)[number], c: Chain, i: number): void => {
|
|
1393
|
+
for (let k = 0; k < c.bones.length; k++) {
|
|
1394
|
+
const b = c.bones[k]!;
|
|
1395
|
+
const dr = set.drawn[b];
|
|
1396
|
+
if (dr === undefined) continue;
|
|
1397
|
+
const kp0 = c.joints[k] !== undefined ? set.pose[c.joints[k]!] : undefined;
|
|
1398
|
+
const kp1 = c.joints[k + 1] !== undefined ? set.pose[c.joints[k + 1]!] : undefined;
|
|
1399
|
+
const keyAng = kp0 !== undefined && kp1 !== undefined ? pictureAngle(kp0, kp1) : dr;
|
|
1400
|
+
const raw = rawTargets.get(b)?.[i] ?? keyAng;
|
|
1401
|
+
const diff = Math.max(-2.5, Math.min(2.5, 0.1 * wrap(raw - keyAng)));
|
|
1402
|
+
targets.get(b)![i] = wrap(dr + diff);
|
|
1403
|
+
}
|
|
1404
|
+
};
|
|
1405
|
+
/**
|
|
1406
|
+
* #188: For a `set.symmetric === true` gesture set, normalise its two two-bone arm chains
|
|
1407
|
+
* on frame `i` to the set's `drawn` symmetry centre so `|elbow_l.y - elbow_r.y| == 0` and
|
|
1408
|
+
* clamp both forearms so the wrists stay inside the head silhouette.
|
|
1409
|
+
*/
|
|
1410
|
+
const solveSymmetricFrame = (set: (typeof map.alternates)[number], i: number): { cR: Chain; cL: Chain; wR: Pt; wL: Pt; hc: Pt; ed: number } | null => {
|
|
1411
|
+
if (set.chains.length !== 2) return null;
|
|
1412
|
+
const c0 = map.chains.find((x) => x.name === set.chains[0]!);
|
|
1413
|
+
const c1 = map.chains.find((x) => x.name === set.chains[1]!);
|
|
1414
|
+
if (c0 === undefined || c1 === undefined || c0.bones.length !== 2 || c1.bones.length !== 2) return null;
|
|
1415
|
+
const S0_0 = rigPoint(rest, c0.bones[0]!);
|
|
1416
|
+
const S0_1 = rigPoint(rest, c1.bones[0]!);
|
|
1417
|
+
if (S0_0 === null || S0_1 === null) return null;
|
|
1418
|
+
const [cR, cL] = S0_0[0] <= S0_1[0] ? [c0, c1] : [c1, c0];
|
|
1419
|
+
const [bR0, bR1] = cR.bones as [string, string];
|
|
1420
|
+
const [bL0, bL1] = cL.bones as [string, string];
|
|
1421
|
+
const wSpecR = cR.aim[1] === 'x' ? `${bR1}:tip` : cR.aim[1]!;
|
|
1422
|
+
const wSpecL = cL.aim[1] === 'x' ? `${bL1}:tip` : cL.aim[1]!;
|
|
1423
|
+
const SR0 = rigPoint(rest, bR0)!;
|
|
1424
|
+
const ER0 = rigPoint(rest, bR1)!;
|
|
1425
|
+
const WR0 = rigPoint(rest, wSpecR)!;
|
|
1426
|
+
const SL0 = rigPoint(rest, bL0)!;
|
|
1427
|
+
const EL0 = rigPoint(rest, bL1)!;
|
|
1428
|
+
const WL0 = rigPoint(rest, wSpecL)!;
|
|
1429
|
+
const lR0 = Math.hypot(ER0[0] - SR0[0], ER0[1] - SR0[1]);
|
|
1430
|
+
const lR1 = Math.hypot(WR0[0] - ER0[0], WR0[1] - ER0[1]);
|
|
1431
|
+
const lL0 = Math.hypot(EL0[0] - SL0[0], EL0[1] - SL0[1]);
|
|
1432
|
+
const lL1 = Math.hypot(WL0[0] - EL0[0], WL0[1] - EL0[1]);
|
|
1433
|
+
const dR0 = wrap(Math.atan2(ER0[1] - SR0[1], ER0[0] - SR0[0]) * DEG - restOf.get(bR0)!);
|
|
1434
|
+
const dR1 = wrap(Math.atan2(WR0[1] - ER0[1], WR0[0] - ER0[0]) * DEG - restOf.get(bR1)!);
|
|
1435
|
+
const dL0 = wrap(Math.atan2(EL0[1] - SL0[1], EL0[0] - SL0[0]) * DEG - restOf.get(bL0)!);
|
|
1436
|
+
const dL1 = wrap(Math.atan2(WL0[1] - EL0[1], WL0[0] - EL0[0]) * DEG - restOf.get(bL1)!);
|
|
1437
|
+
const sr = posedOrigin(bR0, i);
|
|
1438
|
+
const sl = posedOrigin(bL0, i);
|
|
1439
|
+
let hc: Pt = [(sr[0] + sl[0]) / 2, (sr[1] + sl[1]) / 2 + 0.85 * Math.hypot(sl[0] - sr[0], sl[1] - sr[1])];
|
|
1440
|
+
let ed = Math.max(1e-3, 0.30 * Math.hypot(sl[0] - sr[0], sl[1] - sr[1]));
|
|
1441
|
+
if (gestureHead !== null) {
|
|
1442
|
+
const ha = posedPoint(gestureHead.points[0], i);
|
|
1443
|
+
const hb = posedPoint(gestureHead.points[1], i);
|
|
1444
|
+
const d = Math.hypot(hb[0] - ha[0], hb[1] - ha[1]);
|
|
1445
|
+
if (d > 1e-6) {
|
|
1446
|
+
hc = [(ha[0] + hb[0]) / 2, (ha[1] + hb[1]) / 2];
|
|
1447
|
+
ed = d;
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
const drawnR0 = set.drawn[bR0]!;
|
|
1451
|
+
const drawnR1 = set.drawn[bR1]!;
|
|
1452
|
+
const drawnL0 = set.drawn[bL0]!;
|
|
1453
|
+
const drawnL1 = set.drawn[bL1]!;
|
|
1454
|
+
const turnR0 = set.turn[bR0] ?? [-20, 20];
|
|
1455
|
+
const turnR1 = set.turn[bR1] ?? [-20, 20];
|
|
1456
|
+
const turnL0 = set.turn[bL0] ?? [-20, 20];
|
|
1457
|
+
const turnL1 = set.turn[bL1] ?? [-20, 20];
|
|
1458
|
+
const rawR0 = rawTargets.get(bR0)?.[i] ?? drawnR0;
|
|
1459
|
+
const rawL0 = rawTargets.get(bL0)?.[i] ?? drawnL0;
|
|
1460
|
+
const offR0 = wrap(rawR0 - drawnR0);
|
|
1461
|
+
const offL0 = wrap(rawL0 - drawnL0);
|
|
1462
|
+
let cOpen = Math.max(-4, Math.min(2, 0.15 * 0.5 * (offR0 - offL0)));
|
|
1463
|
+
const solveTilt = (open: number): { aR0: number; aL0: number; exR: number; eyR: number; exL: number; eyL: number } => {
|
|
1464
|
+
const evalDiff = (deg: number) => {
|
|
1465
|
+
const dR = deg < 0 ? 0.25 * deg : deg;
|
|
1466
|
+
const dL = deg > 0 ? 0.25 * deg : deg;
|
|
1467
|
+
const ar = drawnR0 + Math.min(Math.max(open + dR, turnR0[0]), turnR0[1]);
|
|
1468
|
+
const al = drawnL0 + Math.min(Math.max(-open + dL, turnL0[0]), turnL0[1]);
|
|
1469
|
+
const eyr = sr[1] + lR0 * Math.sin((ar + dR0) / DEG);
|
|
1470
|
+
const eyl = sl[1] + lL0 * Math.sin((al + dL0) / DEG);
|
|
1471
|
+
return eyl - eyr;
|
|
1472
|
+
};
|
|
1473
|
+
let lo = -45;
|
|
1474
|
+
let hi = 45;
|
|
1475
|
+
let fLo = evalDiff(lo);
|
|
1476
|
+
const fHi = evalDiff(hi);
|
|
1477
|
+
let deg: number;
|
|
1478
|
+
if ((fLo <= 0 && fHi <= 0) || (fLo > 0 && fHi > 0)) {
|
|
1479
|
+
deg = Math.abs(fLo) <= Math.abs(fHi) ? lo : hi;
|
|
1480
|
+
} else {
|
|
1481
|
+
for (let it = 0; it < 36; it++) {
|
|
1482
|
+
const mid = 0.5 * (lo + hi);
|
|
1483
|
+
const fMid = evalDiff(mid);
|
|
1484
|
+
if ((fLo <= 0 && fMid <= 0) || (fLo > 0 && fMid > 0)) {
|
|
1485
|
+
lo = mid;
|
|
1486
|
+
fLo = fMid;
|
|
1487
|
+
} else {
|
|
1488
|
+
hi = mid;
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
deg = 0.5 * (lo + hi);
|
|
1492
|
+
}
|
|
1493
|
+
const dR = deg < 0 ? 0.25 * deg : deg;
|
|
1494
|
+
const dL = deg > 0 ? 0.25 * deg : deg;
|
|
1495
|
+
let aR0 = drawnR0 + Math.min(Math.max(open + dR, turnR0[0]), turnR0[1]);
|
|
1496
|
+
let aL0 = drawnL0 + Math.min(Math.max(-open + dL, turnL0[0]), turnL0[1]);
|
|
1497
|
+
const eyR0 = sr[1] + lR0 * Math.sin((aR0 + dR0) / DEG);
|
|
1498
|
+
const eyL0 = sl[1] + lL0 * Math.sin((aL0 + dL0) / DEG);
|
|
1499
|
+
if (Math.abs(eyL0 - eyR0) > 1e-4) {
|
|
1500
|
+
const sL = Math.max(-1, Math.min(1, (eyR0 - sl[1]) / Math.max(1e-6, lL0)));
|
|
1501
|
+
const candL = wrap(Math.asin(sL) * DEG - dL0);
|
|
1502
|
+
const tL = wrap(candL - drawnL0);
|
|
1503
|
+
if (tL >= turnL0[0] && tL <= turnL0[1]) aL0 = candL;
|
|
1504
|
+
}
|
|
1505
|
+
const rR0 = (aR0 + dR0) / DEG;
|
|
1506
|
+
const rL0 = (aL0 + dL0) / DEG;
|
|
1507
|
+
return {
|
|
1508
|
+
aR0: wrap(aR0),
|
|
1509
|
+
aL0: wrap(aL0),
|
|
1510
|
+
exR: sr[0] + lR0 * Math.cos(rR0),
|
|
1511
|
+
eyR: sr[1] + lR0 * Math.sin(rR0),
|
|
1512
|
+
exL: sl[0] + lL0 * Math.cos(rL0),
|
|
1513
|
+
eyL: sl[1] + lL0 * Math.sin(rL0),
|
|
1514
|
+
};
|
|
1515
|
+
};
|
|
1516
|
+
const upper = solveTilt(cOpen);
|
|
1517
|
+
const aimFore = (ex: number, ey: number, l1: number, d1: number, drawn1: number, turn1: readonly [number, number], tx: number, ty: number): [number, Pt] => {
|
|
1518
|
+
const want = wrap(Math.atan2(ty - ey, tx - ex) * DEG - d1);
|
|
1519
|
+
const off = Math.min(Math.max(wrap(want - drawn1), turn1[0]), turn1[1]);
|
|
1520
|
+
let a1 = wrap(drawn1 + off);
|
|
1521
|
+
let r1 = (a1 + d1) / DEG;
|
|
1522
|
+
let wx = ex + l1 * Math.cos(r1);
|
|
1523
|
+
let wy = ey + l1 * Math.sin(r1);
|
|
1524
|
+
const xMin = hc[0] - 0.30 * ed;
|
|
1525
|
+
const xMax = hc[0] + 0.30 * ed;
|
|
1526
|
+
const yMin = hc[1] + 0.15 * ed;
|
|
1527
|
+
const yMax = hc[1] + 0.95 * ed;
|
|
1528
|
+
if (wx < xMin || wx > xMax || wy < yMin || wy > yMax) {
|
|
1529
|
+
let bestA = a1;
|
|
1530
|
+
let bestErr = Infinity;
|
|
1531
|
+
for (let step = -200; step <= 200; step++) {
|
|
1532
|
+
const candOff = turn1[0] + ((turn1[1] - turn1[0]) * (step + 200)) / 400;
|
|
1533
|
+
const candA = wrap(drawn1 + candOff);
|
|
1534
|
+
const cr = (candA + d1) / DEG;
|
|
1535
|
+
const cwx = ex + l1 * Math.cos(cr);
|
|
1536
|
+
const cwy = ey + l1 * Math.sin(cr);
|
|
1537
|
+
const ox = cwx < xMin ? xMin - cwx : cwx > xMax ? cwx - xMax : 0;
|
|
1538
|
+
const oy = cwy < yMin ? yMin - cwy : cwy > yMax ? cwy - yMax : 0;
|
|
1539
|
+
const err = (ox + oy) * 1000 + Math.hypot(cwx - tx, cwy - ty);
|
|
1540
|
+
if (err < bestErr) {
|
|
1541
|
+
bestErr = err;
|
|
1542
|
+
bestA = candA;
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
a1 = bestA;
|
|
1546
|
+
r1 = (a1 + d1) / DEG;
|
|
1547
|
+
wx = ex + l1 * Math.cos(r1);
|
|
1548
|
+
wy = ey + l1 * Math.sin(r1);
|
|
1549
|
+
}
|
|
1550
|
+
return [a1, [wx, wy]];
|
|
1551
|
+
};
|
|
1552
|
+
const [aR1, wR] = aimFore(upper.exR, upper.eyR, lR1, dR1, drawnR1, turnR1, hc[0] - 0.20 * ed, hc[1] + 0.65 * ed);
|
|
1553
|
+
const [aL1, wL] = aimFore(upper.exL, upper.eyL, lL1, dL1, drawnL1, turnL1, hc[0] + 0.20 * ed, hc[1] + 0.65 * ed);
|
|
1554
|
+
targets.get(bR0)![i] = upper.aR0;
|
|
1555
|
+
targets.get(bR1)![i] = aR1;
|
|
1556
|
+
targets.get(bL0)![i] = upper.aL0;
|
|
1557
|
+
targets.get(bL1)![i] = aL1;
|
|
1558
|
+
return { cR, cL, wR, wL, hc, ed };
|
|
1559
|
+
};
|
|
1329
1560
|
if (headOk !== null || hipsOk !== null) {
|
|
1330
1561
|
/** The anchor's two rig points posed on frame i: their midpoint and their distance apart. */
|
|
1331
1562
|
const pairOn = (an: Anchor, i: number): { centre: Pt; dist: number } => {
|
|
@@ -1382,18 +1613,34 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1382
1613
|
for (const cn of s.chains) {
|
|
1383
1614
|
if (sw.chainHolds?.some((h) => h.chain === cn && h.set !== s.set && h.frames[0] <= s.frames[0] && h.frames[1] >= s.frames[1]) === true) continue;
|
|
1384
1615
|
const c = map.chains.find((x) => x.name === cn)!;
|
|
1616
|
+
if (set.support?.includes(c.name) === true) {
|
|
1617
|
+
for (let i = s.frames[0]; i <= s.frames[1]; i++) solveSupportChainFrame(set, c, i);
|
|
1618
|
+
continue;
|
|
1619
|
+
}
|
|
1385
1620
|
const drawn = set.drawn[c.bones[0]!];
|
|
1386
1621
|
const up = isChainHeadPlaced(set, c, drawn);
|
|
1387
1622
|
const low = hipsOk !== null && set.place?.chains.includes(c.name) === true;
|
|
1388
1623
|
if (!up && !low && headOk !== null && drawn !== undefined && isRaised(drawn) && c.bones.length >= 1 && c.joints.length >= 2) {
|
|
1389
|
-
|
|
1390
|
-
const p1 = set.pose[c.joints[1]!];
|
|
1391
|
-
if (p0 !== undefined && p1 !== undefined) {
|
|
1392
|
-
const key0 = pictureAngle(p0, p1);
|
|
1624
|
+
if (set.support !== undefined && c.bones.length >= 2) {
|
|
1393
1625
|
const t0 = targets.get(c.bones[0]!)!;
|
|
1626
|
+
const t1 = targets.get(c.bones[1]!)!;
|
|
1394
1627
|
for (let i = s.frames[0]; i <= s.frames[1]; i++) {
|
|
1395
|
-
const
|
|
1396
|
-
|
|
1628
|
+
const pair = solveSupportActiveFrame(set, c, i);
|
|
1629
|
+
if (pair !== null) {
|
|
1630
|
+
t0[i] = pair[0];
|
|
1631
|
+
t1[i] = pair[1];
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
} else {
|
|
1635
|
+
const p0 = set.pose[c.joints[0]!];
|
|
1636
|
+
const p1 = set.pose[c.joints[1]!];
|
|
1637
|
+
if (p0 !== undefined && p1 !== undefined) {
|
|
1638
|
+
const key0 = pictureAngle(p0, p1);
|
|
1639
|
+
const t0 = targets.get(c.bones[0]!)!;
|
|
1640
|
+
for (let i = s.frames[0]; i <= s.frames[1]; i++) {
|
|
1641
|
+
const diff = Math.max(-3, Math.min(3, wrap(t0[i]! - key0)));
|
|
1642
|
+
t0[i] = wrap(drawn + diff);
|
|
1643
|
+
}
|
|
1397
1644
|
}
|
|
1398
1645
|
}
|
|
1399
1646
|
}
|
|
@@ -1561,7 +1808,7 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1561
1808
|
t1[i] = a1;
|
|
1562
1809
|
}
|
|
1563
1810
|
// A held upper arm whose re-aimed forearm leaves the wrist more than HELD_MISS short of its placed point: ART-RANGE on the upper arm, with the shortfall.
|
|
1564
|
-
if (turn0 !== undefined) {
|
|
1811
|
+
if (turn0 !== undefined && set.symmetric !== true) {
|
|
1565
1812
|
for (const [a, b] of runsOf(heldMiss.map((m) => m > HELD_MISS), RANGE_GAP)) {
|
|
1566
1813
|
let worst = a;
|
|
1567
1814
|
for (let k = a; k <= b; k++) if (heldMiss[k]! > heldMiss[worst]!) worst = k;
|
|
@@ -1570,7 +1817,7 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1570
1817
|
art.push({ set: set.name, chain: c.name, bone: b0, frames: [first, last], seconds: r6(endOf(last) - keyTimes[first]!), maxOverflow: r3(upperOver[worst]!), turn: r3(upperAsked[worst]!), limits: turn0, clamped: true, shortfall: r3(heldMiss[worst]!), ...(up ? {} : { anchor: 'hip' as const }) });
|
|
1571
1818
|
}
|
|
1572
1819
|
}
|
|
1573
|
-
if (turn1 !== undefined) {
|
|
1820
|
+
if (turn1 !== undefined && set.symmetric !== true) {
|
|
1574
1821
|
for (const [a, b] of runsOf(foreOver.map((o) => o > RANGE_SLACK), RANGE_GAP)) {
|
|
1575
1822
|
let worst = a;
|
|
1576
1823
|
for (let k = a; k <= b; k++) if (foreOver[k]! > foreOver[worst]!) worst = k;
|
|
@@ -1581,10 +1828,51 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1581
1828
|
}
|
|
1582
1829
|
raisedJobs.push(job);
|
|
1583
1830
|
}
|
|
1831
|
+
if (set.symmetric === true) {
|
|
1832
|
+
for (let i = s.frames[0]; i <= s.frames[1]; i++) {
|
|
1833
|
+
const sym = solveSymmetricFrame(set, i);
|
|
1834
|
+
if (sym === null) continue;
|
|
1835
|
+
const k = i - s.frames[0];
|
|
1836
|
+
for (const job of raisedJobs) {
|
|
1837
|
+
if (job.sw !== s) continue;
|
|
1838
|
+
const w = job.chain.name === sym.cR.name ? sym.wR : job.chain.name === sym.cL.name ? sym.wL : null;
|
|
1839
|
+
if (w !== null && k >= 0 && k < job.placed.length) {
|
|
1840
|
+
job.placed[k] = [(w[0] - sym.hc[0]) / job.rigEye, (w[1] - sym.hc[1]) / job.rigEye];
|
|
1841
|
+
job.upperHeld = [];
|
|
1842
|
+
job.short = [];
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1584
1847
|
art.sort((x, y) => x.frames[0] - y.frames[0]);
|
|
1585
1848
|
if (art.length > 0) raisedArt.push({ sw: s, entries: art });
|
|
1586
1849
|
}
|
|
1587
1850
|
}
|
|
1851
|
+
// #188: Even without --raised-by-head, apply symmetric normalisation and support-arm anchoring on each switch's span.
|
|
1852
|
+
for (const s of sw.switches) {
|
|
1853
|
+
const set = map.alternates.find((a) => a.name === s.set)!;
|
|
1854
|
+
if (set.symmetric === true && headOk === null && hipsOk === null) {
|
|
1855
|
+
for (let i = s.frames[0]; i <= s.frames[1]; i++) solveSymmetricFrame(set, i);
|
|
1856
|
+
}
|
|
1857
|
+
if (set.support !== undefined) {
|
|
1858
|
+
for (const cn of s.chains) {
|
|
1859
|
+
const c = map.chains.find((x) => x.name === cn)!;
|
|
1860
|
+
if (set.support.includes(c.name)) {
|
|
1861
|
+
for (let i = s.frames[0]; i <= s.frames[1]; i++) solveSupportChainFrame(set, c, i);
|
|
1862
|
+
} else if (headOk === null && hipsOk === null && c.bones.length >= 2) {
|
|
1863
|
+
const t0 = targets.get(c.bones[0]!)!;
|
|
1864
|
+
const t1 = targets.get(c.bones[1]!)!;
|
|
1865
|
+
for (let i = s.frames[0]; i <= s.frames[1]; i++) {
|
|
1866
|
+
const pair = solveSupportActiveFrame(set, c, i);
|
|
1867
|
+
if (pair !== null) {
|
|
1868
|
+
t0[i] = pair[0];
|
|
1869
|
+
t1[i] = pair[1];
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1588
1876
|
p.throwIfAny();
|
|
1589
1877
|
// The art turn of every span; a raised chain's bones were held by the solve, and only its re-aimed forearm is listed (above).
|
|
1590
1878
|
const measured = measureArt(sw.switches, map, targets, endOf, keyTimes, RANGE_GAP);
|
|
@@ -1673,6 +1961,119 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1673
1961
|
}
|
|
1674
1962
|
}
|
|
1675
1963
|
}
|
|
1964
|
+
// #188: Cover the full shown/bridged span of symmetric sets and support-arm sets after short interpolation.
|
|
1965
|
+
for (let sIdx = 0; sIdx < sw.switches.length; sIdx++) {
|
|
1966
|
+
const s = sw.switches[sIdx]!;
|
|
1967
|
+
const set = map.alternates.find((a) => a.name === s.set)!;
|
|
1968
|
+
const setSlots = Object.entries(set.slots);
|
|
1969
|
+
const isSetShownAt = (i: number): boolean =>
|
|
1970
|
+
setSlots.length > 0 && setSlots.every(([sl, att]) => sw.shown.get(sl)?.[i] === att);
|
|
1971
|
+
let loShow = s.frames[0];
|
|
1972
|
+
while (loShow - 1 >= 0 && isSetShownAt(loShow - 1)) loShow--;
|
|
1973
|
+
let hiShow = s.frames[1];
|
|
1974
|
+
while (hiShow + 1 < n && isSetShownAt(hiShow + 1)) hiShow++;
|
|
1975
|
+
if (set.symmetric === true) {
|
|
1976
|
+
for (let i = loShow; i <= s.frames[1]; i++) solveSymmetricFrame(set, i);
|
|
1977
|
+
if (loShow < s.frames[0]) {
|
|
1978
|
+
const loRamp = Math.max(0, loShow - 6);
|
|
1979
|
+
const steps = loShow - loRamp;
|
|
1980
|
+
if (steps >= 2) {
|
|
1981
|
+
for (const cn of set.chains) {
|
|
1982
|
+
const c = map.chains.find((x) => x.name === cn);
|
|
1983
|
+
if (!c) continue;
|
|
1984
|
+
for (const b of c.bones) {
|
|
1985
|
+
const tg = targets.get(b)!;
|
|
1986
|
+
const startA = tg[loRamp]!;
|
|
1987
|
+
const total = wrap(tg[loShow]! - startA);
|
|
1988
|
+
for (let k = loRamp + 1; k < loShow; k++) {
|
|
1989
|
+
tg[k] = wrap(startA + ((k - loRamp) / steps) * total);
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
if (hiShow > s.frames[1] && set.chains.length === 2) {
|
|
1996
|
+
const c0 = map.chains.find((x) => x.name === set.chains[0]!);
|
|
1997
|
+
const c1 = map.chains.find((x) => x.name === set.chains[1]!);
|
|
1998
|
+
if (c0 !== undefined && c1 !== undefined && c0.bones.length === 2 && c1.bones.length === 2) {
|
|
1999
|
+
const S0_0 = rigPoint(rest, c0.bones[0]!)!;
|
|
2000
|
+
const S0_1 = rigPoint(rest, c1.bones[0]!)!;
|
|
2001
|
+
const [cR, cL] = S0_0[0] <= S0_1[0] ? [c0, c1] : [c1, c0];
|
|
2002
|
+
const bR0 = cR.bones[0]!;
|
|
2003
|
+
const bL0 = cL.bones[0]!;
|
|
2004
|
+
const SR0 = rigPoint(rest, bR0)!;
|
|
2005
|
+
const ER0 = rigPoint(rest, cR.bones[1]!)!;
|
|
2006
|
+
const SL0 = rigPoint(rest, bL0)!;
|
|
2007
|
+
const EL0 = rigPoint(rest, cL.bones[1]!)!;
|
|
2008
|
+
const lR0 = Math.hypot(ER0[0] - SR0[0], ER0[1] - SR0[1]);
|
|
2009
|
+
const lL0 = Math.hypot(EL0[0] - SL0[0], EL0[1] - SL0[1]);
|
|
2010
|
+
const dR0 = wrap(Math.atan2(ER0[1] - SR0[1], ER0[0] - SR0[0]) * DEG - restOf.get(bR0)!);
|
|
2011
|
+
const dL0 = wrap(Math.atan2(EL0[1] - SL0[1], EL0[0] - SL0[0]) * DEG - restOf.get(bL0)!);
|
|
2012
|
+
for (let i = s.frames[1] + 1; i <= hiShow; i++) {
|
|
2013
|
+
const sr = posedOrigin(bR0, i);
|
|
2014
|
+
const sl = posedOrigin(bL0, i);
|
|
2015
|
+
const baseR = targets.get(bR0)![i]!;
|
|
2016
|
+
const baseL = targets.get(bL0)![i]!;
|
|
2017
|
+
let lo = -20;
|
|
2018
|
+
let hi = 20;
|
|
2019
|
+
const evalD = (deg: number) =>
|
|
2020
|
+
sl[1] + lL0 * Math.sin((baseL + deg + dL0) / DEG) - (sr[1] + lR0 * Math.sin((baseR + deg + dR0) / DEG));
|
|
2021
|
+
let fLo = evalD(lo);
|
|
2022
|
+
for (let it = 0; it < 32; it++) {
|
|
2023
|
+
const mid = 0.5 * (lo + hi);
|
|
2024
|
+
const fMid = evalD(mid);
|
|
2025
|
+
if ((fLo <= 0 && fMid <= 0) || (fLo > 0 && fMid > 0)) {
|
|
2026
|
+
lo = mid;
|
|
2027
|
+
fLo = fMid;
|
|
2028
|
+
} else hi = mid;
|
|
2029
|
+
}
|
|
2030
|
+
const deg = 0.5 * (lo + hi);
|
|
2031
|
+
targets.get(bR0)![i] = wrap(baseR + deg);
|
|
2032
|
+
targets.get(bL0)![i] = wrap(baseL + deg);
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
if (set.support !== undefined) {
|
|
2038
|
+
const prevSw = sIdx > 0 ? sw.switches[sIdx - 1] : undefined;
|
|
2039
|
+
const loGap = prevSw !== undefined && s.frames[0] - prevSw.frames[1] <= 10 ? prevSw.frames[1] + 1 : loShow;
|
|
2040
|
+
for (const cn of s.chains) {
|
|
2041
|
+
const c = map.chains.find((x) => x.name === cn)!;
|
|
2042
|
+
if (set.support.includes(c.name)) {
|
|
2043
|
+
for (let i = Math.min(loGap, loShow); i <= hiShow; i++) solveSupportChainFrame(set, c, i);
|
|
2044
|
+
const anchorStart = Math.min(loGap, loShow);
|
|
2045
|
+
if (anchorStart >= 2) {
|
|
2046
|
+
for (const b of c.bones) {
|
|
2047
|
+
const tg = targets.get(b)!;
|
|
2048
|
+
tg[anchorStart - 1] = wrap(tg[anchorStart - 2]! + 0.5 * wrap(tg[anchorStart]! - tg[anchorStart - 2]!));
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
} else if (c.bones.length >= 2) {
|
|
2052
|
+
const [b0, b1] = c.bones as [string, string];
|
|
2053
|
+
const drawn0 = set.drawn[b0];
|
|
2054
|
+
if (drawn0 !== undefined && isRaised(drawn0)) {
|
|
2055
|
+
const S0 = rigPoint(rest, b0);
|
|
2056
|
+
let headX0 = 0;
|
|
2057
|
+
if (gestureHead !== null) {
|
|
2058
|
+
const ha0 = rigPoint(rest, gestureHead.points[0]);
|
|
2059
|
+
const hb0 = rigPoint(rest, gestureHead.points[1]);
|
|
2060
|
+
if (ha0 !== null && hb0 !== null) headX0 = (ha0[0] + hb0[0]) / 2;
|
|
2061
|
+
}
|
|
2062
|
+
const l0 = S0 !== null && rigPoint(rest, b1) !== null ? Math.hypot(rigPoint(rest, b1)![0] - S0[0], rigPoint(rest, b1)![1] - S0[1]) : 100;
|
|
2063
|
+
const outSign: 1 | -1 = (S0?.[0] ?? 0) + l0 * Math.cos(drawn0 / DEG) <= headX0 ? 1 : -1;
|
|
2064
|
+
const t0 = targets.get(b0)!;
|
|
2065
|
+
const t1 = targets.get(b1)!;
|
|
2066
|
+
for (let i = loShow; i <= hiShow; i++) {
|
|
2067
|
+
const pair = solveSupportActiveFrame(set, c, i);
|
|
2068
|
+
if (pair === null) continue;
|
|
2069
|
+
if (i >= s.frames[0] || outSign * wrap(t0[i]! - pair[0]) < 0) t0[i] = pair[0];
|
|
2070
|
+
if (i >= s.frames[0] || outSign * wrap(t1[i]! - pair[1]) < 0) t1[i] = pair[1];
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
1676
2077
|
|
|
1677
2078
|
// The legs on their final targets: a bone under a scaled bone is keyed through
|
|
1678
2079
|
// its parent's posed matrix (legs.ts), and each ankle's world position is read.
|
package/src/switch.ts
CHANGED
|
@@ -666,6 +666,7 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
666
666
|
});
|
|
667
667
|
if (!hasUpChain) continue;
|
|
668
668
|
for (const cn of set.chains) {
|
|
669
|
+
if (set.support?.includes(cn) === true) continue;
|
|
669
670
|
const c = map.chains.find((x) => x.name === cn);
|
|
670
671
|
if (c === undefined) continue;
|
|
671
672
|
const allDown = c.bones.every((b) => (set.drawn[b] ?? 0) < 0);
|
|
@@ -885,7 +886,13 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
885
886
|
}
|
|
886
887
|
return raw;
|
|
887
888
|
};
|
|
888
|
-
const
|
|
889
|
+
const activeSwChains = switchingChains.filter((cn) => !set1.support?.includes(cn) && !set0.support?.includes(cn));
|
|
890
|
+
const measureChains = activeSwChains.length > 0 ? activeSwChains : switchingChains;
|
|
891
|
+
const measureBones = measureChains.flatMap((cn) => map.chains.find((c) => c.name === cn)?.bones ?? []);
|
|
892
|
+
const rootBones = measureChains
|
|
893
|
+
.map((cn) => map.chains.find((c) => c.name === cn)?.bones[0])
|
|
894
|
+
.filter((b): b is string => b !== undefined && set0.drawn[b] !== undefined && set1.drawn[b] !== undefined);
|
|
895
|
+
const pairs = measureBones.filter((b) => set0.drawn[b] !== undefined && set1.drawn[b] !== undefined);
|
|
889
896
|
const span = pairs.length > 0
|
|
890
897
|
? pairs.reduce((s, b) => s + Math.abs(wrap(set1.drawn[b]! - set0.drawn[b]!)), 0) / pairs.length
|
|
891
898
|
: 0;
|
|
@@ -896,7 +903,10 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
896
903
|
const d1 = pairs.length > 0
|
|
897
904
|
? pairs.reduce((s, b) => s + Math.abs(wrap(effAngle(b, i) - set1.drawn[b]!)), 0) / pairs.length
|
|
898
905
|
: (distances.get(set1.name)?.[i] ?? Infinity);
|
|
899
|
-
|
|
906
|
+
const rootsCrossed =
|
|
907
|
+
activeSwChains.length === switchingChains.length ||
|
|
908
|
+
rootBones.every((b) => Math.abs(wrap(effAngle(b, i) - set1.drawn[b]!)) <= Math.abs(wrap(effAngle(b, i) - set0.drawn[b]!)));
|
|
909
|
+
if ((d1 <= d0 || (span > 1e-6 && d0 >= 0.5 * span)) && rootsCrossed) {
|
|
900
910
|
tSwitch = i;
|
|
901
911
|
break;
|
|
902
912
|
}
|