@rig-c/pose 0.24.1 → 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 +463 -20
- package/src/switch.ts +244 -20
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 } => {
|
|
@@ -1380,19 +1611,36 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1380
1611
|
const set = map.alternates.find((a) => a.name === s.set)!;
|
|
1381
1612
|
const art: ArtRange[] = [];
|
|
1382
1613
|
for (const cn of s.chains) {
|
|
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;
|
|
1383
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
|
+
}
|
|
1384
1620
|
const drawn = set.drawn[c.bones[0]!];
|
|
1385
1621
|
const up = isChainHeadPlaced(set, c, drawn);
|
|
1386
1622
|
const low = hipsOk !== null && set.place?.chains.includes(c.name) === true;
|
|
1387
1623
|
if (!up && !low && headOk !== null && drawn !== undefined && isRaised(drawn) && c.bones.length >= 1 && c.joints.length >= 2) {
|
|
1388
|
-
|
|
1389
|
-
const p1 = set.pose[c.joints[1]!];
|
|
1390
|
-
if (p0 !== undefined && p1 !== undefined) {
|
|
1391
|
-
const key0 = pictureAngle(p0, p1);
|
|
1624
|
+
if (set.support !== undefined && c.bones.length >= 2) {
|
|
1392
1625
|
const t0 = targets.get(c.bones[0]!)!;
|
|
1626
|
+
const t1 = targets.get(c.bones[1]!)!;
|
|
1393
1627
|
for (let i = s.frames[0]; i <= s.frames[1]; i++) {
|
|
1394
|
-
const
|
|
1395
|
-
|
|
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
|
+
}
|
|
1396
1644
|
}
|
|
1397
1645
|
}
|
|
1398
1646
|
}
|
|
@@ -1560,7 +1808,7 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1560
1808
|
t1[i] = a1;
|
|
1561
1809
|
}
|
|
1562
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.
|
|
1563
|
-
if (turn0 !== undefined) {
|
|
1811
|
+
if (turn0 !== undefined && set.symmetric !== true) {
|
|
1564
1812
|
for (const [a, b] of runsOf(heldMiss.map((m) => m > HELD_MISS), RANGE_GAP)) {
|
|
1565
1813
|
let worst = a;
|
|
1566
1814
|
for (let k = a; k <= b; k++) if (heldMiss[k]! > heldMiss[worst]!) worst = k;
|
|
@@ -1569,7 +1817,7 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1569
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 }) });
|
|
1570
1818
|
}
|
|
1571
1819
|
}
|
|
1572
|
-
if (turn1 !== undefined) {
|
|
1820
|
+
if (turn1 !== undefined && set.symmetric !== true) {
|
|
1573
1821
|
for (const [a, b] of runsOf(foreOver.map((o) => o > RANGE_SLACK), RANGE_GAP)) {
|
|
1574
1822
|
let worst = a;
|
|
1575
1823
|
for (let k = a; k <= b; k++) if (foreOver[k]! > foreOver[worst]!) worst = k;
|
|
@@ -1580,10 +1828,51 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1580
1828
|
}
|
|
1581
1829
|
raisedJobs.push(job);
|
|
1582
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
|
+
}
|
|
1583
1847
|
art.sort((x, y) => x.frames[0] - y.frames[0]);
|
|
1584
1848
|
if (art.length > 0) raisedArt.push({ sw: s, entries: art });
|
|
1585
1849
|
}
|
|
1586
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
|
+
}
|
|
1587
1876
|
p.throwIfAny();
|
|
1588
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).
|
|
1589
1878
|
const measured = measureArt(sw.switches, map, targets, endOf, keyTimes, RANGE_GAP);
|
|
@@ -1633,6 +1922,158 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1633
1922
|
if (job.anchor.kind === 'head') raised.push({ set, chain, frames, seconds, eyes: unit, ...body });
|
|
1634
1923
|
else byHip.push({ set, chain, frames, seconds, hips: unit, ...body });
|
|
1635
1924
|
}
|
|
1925
|
+
if (opts.short !== 'off') {
|
|
1926
|
+
for (const s of short) {
|
|
1927
|
+
if (s.step === undefined || s.across === true) continue;
|
|
1928
|
+
const cov = covered.get(s.chain);
|
|
1929
|
+
const [lo, hi] = s.frames;
|
|
1930
|
+
if (lo <= 0 || hi + 1 >= n) continue;
|
|
1931
|
+
if (cov?.[lo - 1] !== true && cov?.[hi + 1] !== true) continue;
|
|
1932
|
+
const tg = targets.get(s.bone)!;
|
|
1933
|
+
const startA = tg[lo - 1]!;
|
|
1934
|
+
const total = wrap(tg[hi + 1]! - startA);
|
|
1935
|
+
const steps = hi - lo + 2;
|
|
1936
|
+
for (let k = lo; k <= hi; k++) {
|
|
1937
|
+
tg[k] = wrap(startA + ((k - lo + 1) / steps) * total);
|
|
1938
|
+
}
|
|
1939
|
+
s.step = r3(total / steps);
|
|
1940
|
+
}
|
|
1941
|
+
const holds = sw.chainHolds ?? [];
|
|
1942
|
+
for (const h of holds) {
|
|
1943
|
+
const swParent = sw.switches.find((s) => s.set === h.set && s.frames[0] === h.frames[0] && s.frames[1] > h.frames[1]);
|
|
1944
|
+
if (!swParent) continue;
|
|
1945
|
+
const nextH = holds.find((nh) => nh.chain === h.chain && nh.frames[0] > h.frames[1] && nh.frames[0] - h.frames[1] <= 10);
|
|
1946
|
+
if (!nextH) continue;
|
|
1947
|
+
const lo = h.frames[1] + 1;
|
|
1948
|
+
const hi = nextH.frames[0] - 1;
|
|
1949
|
+
if (lo > hi || lo <= 0 || hi + 1 >= n) continue;
|
|
1950
|
+
const ch = map.chains.find((c) => c.name === h.chain);
|
|
1951
|
+
if (!ch) continue;
|
|
1952
|
+
const steps = hi - lo + 2;
|
|
1953
|
+
for (const b of ch.bones) {
|
|
1954
|
+
const tg = targets.get(b);
|
|
1955
|
+
if (!tg) continue;
|
|
1956
|
+
const startA = tg[lo - 1]!;
|
|
1957
|
+
const total = wrap(tg[hi + 1]! - startA);
|
|
1958
|
+
for (let k = lo; k <= hi; k++) {
|
|
1959
|
+
tg[k] = wrap(startA + ((k - lo + 1) / steps) * total);
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
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
|
+
}
|
|
1636
2077
|
|
|
1637
2078
|
// The legs on their final targets: a bone under a scaled bone is keyed through
|
|
1638
2079
|
// its parent's posed matrix (legs.ts), and each ankle's world position is read.
|
|
@@ -2038,24 +2479,26 @@ export function capShortSteps(
|
|
|
2038
2479
|
}
|
|
2039
2480
|
let segEnd = segStart;
|
|
2040
2481
|
while (segEnd + 1 < n && covered?.[segEnd + 1] !== true) segEnd++;
|
|
2041
|
-
|
|
2482
|
+
const loBound = Math.max(0, segStart - 1);
|
|
2483
|
+
const hiBound = Math.min(n - 1, segEnd + 1);
|
|
2484
|
+
if (hiBound - loBound >= 2) {
|
|
2042
2485
|
const u: number[] = new Array<number>(n).fill(0);
|
|
2043
|
-
u[
|
|
2044
|
-
for (let i =
|
|
2486
|
+
u[loBound] = angles[loBound]!;
|
|
2487
|
+
for (let i = loBound + 1; i <= hiBound; i++) u[i] = u[i - 1]! + wrap(angles[i]! - angles[i - 1]!);
|
|
2045
2488
|
const fits = (lo: number, hi: number) => Math.abs(u[hi]! - u[lo]!) <= stepCap * (hi - lo);
|
|
2046
2489
|
interface Win { lo: number; hi: number; asked: number }
|
|
2047
2490
|
const wins: Win[] = [];
|
|
2048
|
-
for (let k =
|
|
2491
|
+
for (let k = loBound + 1; k <= hiBound; k++) {
|
|
2049
2492
|
if (!(Math.min(fraction[k - 1]!, fraction[k]!) < fractionCutoff)) continue;
|
|
2050
2493
|
const jump = u[k]! - u[k - 1]!;
|
|
2051
2494
|
if (Math.abs(jump) <= stepCap) continue;
|
|
2052
2495
|
let h = 1;
|
|
2053
|
-
let lo = Math.max(
|
|
2054
|
-
let hi = Math.min(
|
|
2055
|
-
while (!fits(lo, hi) && (lo >
|
|
2496
|
+
let lo = Math.max(loBound, k - h - 1);
|
|
2497
|
+
let hi = Math.min(hiBound, k + h);
|
|
2498
|
+
while (!fits(lo, hi) && (lo > loBound || hi < hiBound)) {
|
|
2056
2499
|
h++;
|
|
2057
|
-
lo = Math.max(
|
|
2058
|
-
hi = Math.min(
|
|
2500
|
+
lo = Math.max(loBound, k - h - 1);
|
|
2501
|
+
hi = Math.min(hiBound, k + h);
|
|
2059
2502
|
}
|
|
2060
2503
|
wins.push({ lo, hi, asked: jump });
|
|
2061
2504
|
}
|
|
@@ -2072,9 +2515,9 @@ export function capShortSteps(
|
|
|
2072
2515
|
}
|
|
2073
2516
|
if (changed) continue;
|
|
2074
2517
|
for (const w of wins) {
|
|
2075
|
-
if (fits(w.lo, w.hi) || (w.lo ===
|
|
2076
|
-
w.lo = Math.max(
|
|
2077
|
-
w.hi = Math.min(
|
|
2518
|
+
if (fits(w.lo, w.hi) || (w.lo === loBound && w.hi === hiBound)) continue;
|
|
2519
|
+
w.lo = Math.max(loBound, w.lo - 1);
|
|
2520
|
+
w.hi = Math.min(hiBound, w.hi + 1);
|
|
2078
2521
|
changed = true;
|
|
2079
2522
|
}
|
|
2080
2523
|
}
|
package/src/switch.ts
CHANGED
|
@@ -322,6 +322,7 @@ function groupLayering(group: readonly AlternateSet[], map: JointMap, slots: Rig
|
|
|
322
322
|
const setupSlots = hiddenSetup.filter((s) => slotChain.get(s) === cn && slots.get(s)?.bone !== torsoSlot);
|
|
323
323
|
const setSlots = Object.keys(a.slots).filter((s) => slotChain.get(s) === cn && rank.has(s));
|
|
324
324
|
if (setupSlots.length === 0 || setSlots.length === 0) return false;
|
|
325
|
+
if (setupSlots.every((s) => a.slots[s] !== undefined && a.slots[s] !== slots.get(s)?.setup)) return true;
|
|
325
326
|
const boneSetup = setupSlots.filter((s) => boneOwner.has(slots.get(s)?.bone ?? ''));
|
|
326
327
|
const refSetup = boneSetup.length > 0 ? boneSetup : setupSlots;
|
|
327
328
|
return setupSlots.every((s) => a.hides.includes(s)) && Math.max(...setSlots.map((s) => rank.get(s)!)) < Math.min(...refSetup.map((s) => rank.get(s)!));
|
|
@@ -626,8 +627,9 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
626
627
|
for (let i = sw.frames[0]; i <= sw.frames[1]; i++) row[i] = o;
|
|
627
628
|
}
|
|
628
629
|
}
|
|
629
|
-
// #136/#139/#162/#180: between two sets
|
|
630
|
-
//
|
|
630
|
+
// #136/#139/#162/#180/#187: between two gesture sets, switch both arms on one frame t_switch
|
|
631
|
+
// where the measured path crosses halfway between their drawn poses without dropping to the base arm,
|
|
632
|
+
// and in a one-sided gesture set keep an arm whose wrist stays behind the torso on its previous behind set.
|
|
631
633
|
const chainHolds: Array<{ set: string; chain: string; frames: [number, number] }> = [];
|
|
632
634
|
for (const group of groups) {
|
|
633
635
|
const layer = groupLayering(group, map, slots);
|
|
@@ -645,6 +647,57 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
645
647
|
}
|
|
646
648
|
cClamp.set(cn, arr);
|
|
647
649
|
}
|
|
650
|
+
for (const s of layer.touched) {
|
|
651
|
+
if (!shown.has(s)) shown.set(s, Array.from({ length: n }, () => slots.get(s)!.setup));
|
|
652
|
+
}
|
|
653
|
+
// #187 Rule 3: in a one-sided gesture set (one chain raised with drawn > 0 while another chain has all
|
|
654
|
+
// bones drawn < 0 within switchTolerance of the preceding behind-the-torso set), an arm whose wrist the
|
|
655
|
+
// scan places behind the torso keeps its previous behind-the-torso set and angles.
|
|
656
|
+
const heldBehindOnSwitch = new Map<number, Set<string>>();
|
|
657
|
+
for (let idx = 1; idx < gSwitches.length; idx++) {
|
|
658
|
+
const sw = gSwitches[idx]!;
|
|
659
|
+
const prevSw = gSwitches[idx - 1]!;
|
|
660
|
+
const set = group.find((a) => a.name === sw.set)!;
|
|
661
|
+
const prevSet = group.find((a) => a.name === prevSw.set)!;
|
|
662
|
+
if (!layer.isBehind(prevSet) || set === prevSet || set.chains.length < 2) continue;
|
|
663
|
+
const hasUpChain = set.chains.some((cn) => {
|
|
664
|
+
const c = map.chains.find((x) => x.name === cn);
|
|
665
|
+
return c !== undefined && c.bones.some((b) => (set.drawn[b] ?? -180) > 0);
|
|
666
|
+
});
|
|
667
|
+
if (!hasUpChain) continue;
|
|
668
|
+
for (const cn of set.chains) {
|
|
669
|
+
if (set.support?.includes(cn) === true) continue;
|
|
670
|
+
const c = map.chains.find((x) => x.name === cn);
|
|
671
|
+
if (c === undefined) continue;
|
|
672
|
+
const allDown = c.bones.every((b) => (set.drawn[b] ?? 0) < 0);
|
|
673
|
+
if (!allDown) continue;
|
|
674
|
+
const keyNearPrev =
|
|
675
|
+
c.bones.every((b) => set.drawn[b] !== undefined && prevSet.drawn[b] !== undefined) &&
|
|
676
|
+
c.bones.reduce((s, b) => s + Math.abs(wrap(set.drawn[b]! - prevSet.drawn[b]!)), 0) / c.bones.length <= opts.switchTolerance;
|
|
677
|
+
if (!keyNearPrev) continue;
|
|
678
|
+
const ho = opts.handOffsets?.get(cn);
|
|
679
|
+
if (ho === undefined) continue;
|
|
680
|
+
const slice = ho.slice(sw.frames[0], sw.frames[1] + 1);
|
|
681
|
+
const medDy = median(slice.map((p) => p[1]));
|
|
682
|
+
const medDx = median(slice.map((p) => p[0]));
|
|
683
|
+
const isRight = /(?:^|_)r(?:_|$)/.test(cn);
|
|
684
|
+
const inward = isRight ? medDx >= -0.05 : medDx <= 0.05;
|
|
685
|
+
if (!(medDy > 0.5 && inward)) continue;
|
|
686
|
+
let setHeld = heldBehindOnSwitch.get(idx);
|
|
687
|
+
if (setHeld === undefined) {
|
|
688
|
+
setHeld = new Set();
|
|
689
|
+
heldBehindOnSwitch.set(idx, setHeld);
|
|
690
|
+
}
|
|
691
|
+
setHeld.add(cn);
|
|
692
|
+
const cArr = cClamp.get(cn)!;
|
|
693
|
+
for (let i = sw.frames[0]; i <= sw.frames[1]; i++) {
|
|
694
|
+
cArr[i] = prevSet;
|
|
695
|
+
for (const [s, sc] of layer.slotChain) {
|
|
696
|
+
if (sc === cn && shown.has(s)) shown.get(s)![i] = prevSet.slots[s] ?? null;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
648
701
|
for (let m = 0; m + 1 < gSwitches.length; m++) {
|
|
649
702
|
const sw0 = gSwitches[m]!;
|
|
650
703
|
const sw1 = gSwitches[m + 1]!;
|
|
@@ -656,6 +709,14 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
656
709
|
if (g0 > g1) continue;
|
|
657
710
|
const chains = [...new Set([...set0.chains, ...set1.chains])].filter((cn) => layer.isChainBehindBase(set0, cn) && layer.isChainBehindBase(set1, cn));
|
|
658
711
|
if (chains.length === 0) continue;
|
|
712
|
+
const heldOn0 = heldBehindOnSwitch.get(m);
|
|
713
|
+
const heldOn1 = heldBehindOnSwitch.get(m + 1);
|
|
714
|
+
const heldSetFor = (cn: string): AlternateSet | null => {
|
|
715
|
+
if (heldOn1?.has(cn)) return set0;
|
|
716
|
+
if (heldOn0?.has(cn) && m > 0 && set1 === group.find((a) => a.name === gSwitches[m - 1]!.set)) return set1;
|
|
717
|
+
return null;
|
|
718
|
+
};
|
|
719
|
+
const switchingChains = chains.filter((cn) => heldSetFor(cn) === null);
|
|
659
720
|
for (const cn of chains) {
|
|
660
721
|
if (!layer.isBehind(set0)) for (let i = sw0.frames[0]; i <= sw0.frames[1]; i++) cClamp.get(cn)![i] = set0;
|
|
661
722
|
if (!layer.isBehind(set1)) for (let i = sw1.frames[0]; i <= sw1.frames[1]; i++) cClamp.get(cn)![i] = set1;
|
|
@@ -673,11 +734,29 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
673
734
|
}
|
|
674
735
|
return d0 <= d1 ? set0 : set1;
|
|
675
736
|
};
|
|
737
|
+
const rawChainMax = (a: AlternateSet, cn: string, i: number): number => {
|
|
738
|
+
const c = map.chains.find((x) => x.name === cn);
|
|
739
|
+
if (c === undefined) return Infinity;
|
|
740
|
+
return Math.max(...c.bones.map((b) => (a.drawn[b] !== undefined ? Math.abs(wrap(targets.get(b)![i]! - a.drawn[b]!)) : 0)));
|
|
741
|
+
};
|
|
742
|
+
const isTightSet = (a: AlternateSet, cn: string): boolean => {
|
|
743
|
+
const c = map.chains.find((x) => x.name === cn);
|
|
744
|
+
return c !== undefined && c.bones.some((b) => a.turn[b] !== undefined && a.turn[b]![1] - a.turn[b]![0] <= 10);
|
|
745
|
+
};
|
|
676
746
|
const cPick = new Map<string, Array<AlternateSet | null>>();
|
|
677
747
|
for (const cn of chains) {
|
|
748
|
+
const heldSet = heldSetFor(cn);
|
|
678
749
|
const arr: Array<AlternateSet | null> = Array.from({ length: n }, () => null);
|
|
679
|
-
for (let i = sw0.frames[0]; i <= sw0.frames[1]; i++) arr[i] = set0;
|
|
680
|
-
for (let i = sw1.frames[0]; i <= sw1.frames[1]; i++) arr[i] = set1;
|
|
750
|
+
for (let i = sw0.frames[0]; i <= sw0.frames[1]; i++) arr[i] = heldSet ?? set0;
|
|
751
|
+
for (let i = sw1.frames[0]; i <= sw1.frames[1]; i++) arr[i] = heldSet ?? set1;
|
|
752
|
+
if (heldSet !== null) {
|
|
753
|
+
for (let i = g0; i <= g1; i++) {
|
|
754
|
+
arr[i] = heldSet;
|
|
755
|
+
cClamp.get(cn)![i] = heldSet;
|
|
756
|
+
}
|
|
757
|
+
cPick.set(cn, arr);
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
681
760
|
const d0s = byChain.get(set0.name)?.get(cn);
|
|
682
761
|
const d1s = byChain.get(set1.name)?.get(cn);
|
|
683
762
|
for (let i = g0; i <= g1; i++) {
|
|
@@ -706,24 +785,151 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
706
785
|
}
|
|
707
786
|
i = j + 1;
|
|
708
787
|
}
|
|
709
|
-
|
|
710
|
-
|
|
788
|
+
const tight0 = isTightSet(set0, cn);
|
|
789
|
+
const tight1 = isTightSet(set1, cn);
|
|
790
|
+
if (tight0 && set0 !== set1) {
|
|
791
|
+
let i = g0;
|
|
792
|
+
while (i <= g1 && rawChainMax(set0, cn, i) <= opts.switchTolerance + hyst) {
|
|
793
|
+
cClamp.get(cn)![i] = set0;
|
|
794
|
+
i++;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
if (tight1 && set0 !== set1) {
|
|
798
|
+
let enter1 = g1 + 1;
|
|
799
|
+
for (let i = g0; i <= g1; i++) {
|
|
800
|
+
if (rawChainMax(set1, cn, i) <= opts.switchTolerance) {
|
|
801
|
+
enter1 = i;
|
|
802
|
+
break;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
for (let i = enter1; i <= g1; i++) cClamp.get(cn)![i] = set1;
|
|
806
|
+
}
|
|
807
|
+
if (!tight0 && !tight1) {
|
|
808
|
+
for (let i = g0; i <= g1; i++) {
|
|
809
|
+
if (arr[i] !== null) cClamp.get(cn)![i] = arr[i]!;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
if (!tight0 && set0 !== set1) {
|
|
813
|
+
const c = map.chains.find((x) => x.name === cn);
|
|
814
|
+
if (c !== undefined) {
|
|
815
|
+
let k = sw0.frames[1];
|
|
816
|
+
while (
|
|
817
|
+
k >= sw0.frames[0] &&
|
|
818
|
+
c.bones.some((b) => {
|
|
819
|
+
const lim = set0.turn[b];
|
|
820
|
+
const dr = set0.drawn[b];
|
|
821
|
+
const ndr = set1.drawn[b];
|
|
822
|
+
if (lim === undefined || dr === undefined || ndr === undefined) return false;
|
|
823
|
+
const t = wrap(targets.get(b)![k]! - dr);
|
|
824
|
+
return overflow(t, lim) > RANGE_SLACK && Math.abs(wrap(targets.get(b)![k + 1]! - ndr)) < Math.abs(wrap(targets.get(b)![k]! - ndr));
|
|
825
|
+
})
|
|
826
|
+
) {
|
|
827
|
+
cClamp.get(cn)![k] = null;
|
|
828
|
+
k--;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
711
831
|
}
|
|
712
832
|
cPick.set(cn, arr);
|
|
713
833
|
}
|
|
714
|
-
|
|
834
|
+
// #187 Rule 1 & Rule 2: when two gesture sets are adjacent (gap <= 10 frames, or the arm is moving
|
|
835
|
+
// toward the next set's pose), unwrap any bone that turned the long way (> 180° through inverted angles)
|
|
836
|
+
// onto the short arc and pick one shared switch frame t_switch where the measured path crosses halfway
|
|
837
|
+
// between set0.drawn and set1.drawn.
|
|
838
|
+
const movingTowardNext =
|
|
839
|
+
set0 !== set1 &&
|
|
840
|
+
distances.get(set1.name)![g1]! < distances.get(set1.name)![g0]! &&
|
|
841
|
+
distances.get(set0.name)![g1]! > distances.get(set0.name)![g0]!;
|
|
842
|
+
const bridgeDirect = set0 !== set1 && (g1 - g0 + 1 <= 10 || movingTowardNext);
|
|
843
|
+
let tSwitch = g1 + 1;
|
|
844
|
+
if (bridgeDirect && switchingChains.length > 0) {
|
|
845
|
+
const swBones = switchingChains.flatMap((cn) => map.chains.find((c) => c.name === cn)?.bones ?? []);
|
|
846
|
+
for (const b of swBones) {
|
|
847
|
+
const tg = targets.get(b) as number[] | undefined;
|
|
848
|
+
const d0 = set0.drawn[b];
|
|
849
|
+
const d1 = set1.drawn[b];
|
|
850
|
+
if (tg === undefined || d0 === undefined || d1 === undefined) continue;
|
|
851
|
+
const i0 = sw0.frames[1];
|
|
852
|
+
const i1 = sw1.frames[0];
|
|
853
|
+
const startA = tg[i0]!;
|
|
854
|
+
const endA = tg[i1]!;
|
|
855
|
+
const unwrapped: number[] = [startA];
|
|
856
|
+
for (let i = i0 + 1; i <= i1; i++) {
|
|
857
|
+
unwrapped.push(unwrapped[unwrapped.length - 1]! + wrap(tg[i]! - tg[i - 1]!));
|
|
858
|
+
}
|
|
859
|
+
const totalTurn = unwrapped[unwrapped.length - 1]! - startA;
|
|
860
|
+
const shortTurn = wrap(endA - startA);
|
|
861
|
+
const invertsBetween = Array.from({ length: g1 - g0 + 1 }, (_, k) => {
|
|
862
|
+
const a = tg[g0 + k]!;
|
|
863
|
+
return Math.abs(wrap(a - d0)) > 90 && Math.abs(wrap(a - d1)) > 90;
|
|
864
|
+
}).some(Boolean);
|
|
865
|
+
if (Math.abs(totalTurn) > 180 && Math.abs(totalTurn - shortTurn) > 1e-6 && invertsBetween && Math.abs(shortTurn) <= 150) {
|
|
866
|
+
for (let i = g0; i <= g1; i++) {
|
|
867
|
+
const p = (unwrapped[i - i0]! - startA) / totalTurn;
|
|
868
|
+
tg[i] = wrap(startA + p * shortTurn);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
let clampEnd0 = g0 - 1;
|
|
873
|
+
while (clampEnd0 + 1 <= g1 && switchingChains.every((cn) => cClamp.get(cn)![clampEnd0 + 1] === set0)) {
|
|
874
|
+
clampEnd0++;
|
|
875
|
+
}
|
|
876
|
+
const effAngle = (b: string, i: number): number => {
|
|
877
|
+
const raw = targets.get(b)![i]!;
|
|
878
|
+
if (clampEnd0 >= g0 && i > clampEnd0) {
|
|
879
|
+
const frac = opts.shortInfo?.fractions.get(b)?.[i] ?? 1;
|
|
880
|
+
const d0 = set0.drawn[b];
|
|
881
|
+
const d1 = set1.drawn[b];
|
|
882
|
+
if (frac < 0.6 && d0 !== undefined && d1 !== undefined) {
|
|
883
|
+
const steps = g1 + 1 - clampEnd0;
|
|
884
|
+
return wrap(d0 + ((i - clampEnd0) / steps) * wrap(d1 - d0));
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
return raw;
|
|
888
|
+
};
|
|
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);
|
|
896
|
+
const span = pairs.length > 0
|
|
897
|
+
? pairs.reduce((s, b) => s + Math.abs(wrap(set1.drawn[b]! - set0.drawn[b]!)), 0) / pairs.length
|
|
898
|
+
: 0;
|
|
899
|
+
for (let i = Math.max(g0, clampEnd0 + 1); i <= g1; i++) {
|
|
900
|
+
const d0 = pairs.length > 0
|
|
901
|
+
? pairs.reduce((s, b) => s + Math.abs(wrap(effAngle(b, i) - set0.drawn[b]!)), 0) / pairs.length
|
|
902
|
+
: (distances.get(set0.name)?.[i] ?? Infinity);
|
|
903
|
+
const d1 = pairs.length > 0
|
|
904
|
+
? pairs.reduce((s, b) => s + Math.abs(wrap(effAngle(b, i) - set1.drawn[b]!)), 0) / pairs.length
|
|
905
|
+
: (distances.get(set1.name)?.[i] ?? Infinity);
|
|
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) {
|
|
910
|
+
tSwitch = i;
|
|
911
|
+
break;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
const anyHeldBehind = switchingChains.some((cn) => {
|
|
715
916
|
const arr = cPick.get(cn)!;
|
|
716
917
|
for (let i = g0; i <= g1; i++) if (arr[i] === null) return false;
|
|
717
918
|
return true;
|
|
718
919
|
});
|
|
719
|
-
if (!anyHeldBehind
|
|
720
|
-
const frontNear = group.filter((a) => layer.isFront(a)).some((a) => {
|
|
920
|
+
if (!anyHeldBehind) {
|
|
921
|
+
const frontNear = group.filter((a) => layer.isFront(a) && a !== set0 && a !== set1).some((a) => {
|
|
721
922
|
for (let i = g0; i <= g1; i++) if (distances.get(a.name)![i]! <= opts.switchTolerance) return true;
|
|
722
923
|
return false;
|
|
723
924
|
});
|
|
724
|
-
if (
|
|
925
|
+
if (bridgeDirect) {
|
|
926
|
+
for (const cn of switchingChains) {
|
|
927
|
+
for (let i = g0; i <= g1; i++) cPick.get(cn)![i] = i < tSwitch ? set0 : set1;
|
|
928
|
+
}
|
|
929
|
+
} else if (!frontNear) for (const cn of chains) for (let i = g0; i <= g1; i++) cPick.get(cn)![i] = set0;
|
|
725
930
|
} else {
|
|
726
931
|
for (const cn of chains) {
|
|
932
|
+
if (heldSetFor(cn) !== null) continue;
|
|
727
933
|
const arr = cPick.get(cn)!;
|
|
728
934
|
const frontSets = group.filter((a) => layer.isFront(a) && a.chains.includes(cn) && a !== set0 && a !== set1);
|
|
729
935
|
const d0s = byChain.get(set0.name)?.get(cn);
|
|
@@ -771,21 +977,20 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
771
977
|
}
|
|
772
978
|
i = j + 1;
|
|
773
979
|
}
|
|
774
|
-
|
|
775
|
-
|
|
980
|
+
}
|
|
981
|
+
if (bridgeDirect) {
|
|
982
|
+
for (const cn of switchingChains) {
|
|
983
|
+
for (let i = g0; i <= g1; i++) cPick.get(cn)![i] = i < tSwitch ? set0 : set1;
|
|
776
984
|
}
|
|
777
985
|
}
|
|
778
986
|
}
|
|
779
|
-
for (const s of layer.touched) {
|
|
780
|
-
if (!shown.has(s)) shown.set(s, Array.from({ length: n }, () => slots.get(s)!.setup));
|
|
781
|
-
}
|
|
782
987
|
for (let i = g0; i <= g1; i++) {
|
|
783
988
|
for (const [s, cn] of layer.slotChain) {
|
|
784
989
|
const a = cPick.get(cn)?.[i] ?? null;
|
|
785
990
|
if (a !== null) shown.get(s)![i] = a.slots[s] ?? null;
|
|
786
991
|
}
|
|
787
|
-
if (layer.torsoSlot !== null && shown.has(layer.torsoSlot)
|
|
788
|
-
const picked = chains.map((cn) => cPick.get(cn)![i]);
|
|
992
|
+
if (layer.torsoSlot !== null && shown.has(layer.torsoSlot)) {
|
|
993
|
+
const picked = (switchingChains.length > 0 ? switchingChains : chains).map((cn) => cPick.get(cn)![i]);
|
|
789
994
|
if (picked.every((a) => a !== null)) shown.get(layer.torsoSlot)![i] = picked[0]!.slots[layer.torsoSlot] ?? slots.get(layer.torsoSlot)!.setup;
|
|
790
995
|
}
|
|
791
996
|
}
|
|
@@ -834,8 +1039,10 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
834
1039
|
/** Per chain, the frames on which a switch of a set depicting it shows: its art was painted for that pose. */
|
|
835
1040
|
export function coveredChains(switches: readonly Switch[], n: number, chainHolds?: ReadonlyArray<{ set: string; chain: string; frames: [number, number] }>): Map<string, boolean[]> {
|
|
836
1041
|
const out = new Map<string, boolean[]>();
|
|
1042
|
+
const held = new Set((chainHolds ?? []).map((h) => `${h.set}:${h.chain}`));
|
|
837
1043
|
for (const sw of switches) {
|
|
838
1044
|
for (const c of sw.chains) {
|
|
1045
|
+
if (held.has(`${sw.set}:${c}`)) continue;
|
|
839
1046
|
if (!out.has(c)) out.set(c, Array.from({ length: n }, () => false));
|
|
840
1047
|
const row = out.get(c)!;
|
|
841
1048
|
for (let i = sw.frames[0]; i <= sw.frames[1]; i++) row[i] = true;
|
|
@@ -1017,7 +1224,7 @@ export const ART_EASE_STEP = 3;
|
|
|
1017
1224
|
export function clampArt(switches: readonly Switch[], map: JointMap, targets: Map<string, number[]>, chainHolds?: ReadonlyArray<{ set: string; chain: string; frames: [number, number] }>): void {
|
|
1018
1225
|
const boneChain = new Map<string, string>();
|
|
1019
1226
|
for (const c of map.chains) for (const b of c.bones) boneChain.set(b, c.name);
|
|
1020
|
-
const clampSpan = (set: AlternateSet, bone: string, [a,
|
|
1227
|
+
const clampSpan = (set: AlternateSet, bone: string, [a, b0]: [number, number]) => {
|
|
1021
1228
|
const limits = set.turn[bone];
|
|
1022
1229
|
if (limits === undefined) return;
|
|
1023
1230
|
const drawn = set.drawn[bone]!;
|
|
@@ -1028,12 +1235,29 @@ export function clampArt(switches: readonly Switch[], map: JointMap, targets: Ma
|
|
|
1028
1235
|
const c = Math.min(Math.max(t, limits[0]), limits[1]);
|
|
1029
1236
|
return Math.abs(c - t) > RANGE_SLACK ? c - t : 0;
|
|
1030
1237
|
};
|
|
1238
|
+
const tightHold = limits[1] - limits[0] <= 10;
|
|
1239
|
+
let b = b0;
|
|
1240
|
+
let trimmedExit = false;
|
|
1241
|
+
if (!tightHold && b + 1 < n && delta(b) !== 0) {
|
|
1242
|
+
const nextSw = switches.find((s) => s.frames[0] > b && s.frames[0] - b <= 10 && s.set !== set.name);
|
|
1243
|
+
const nextDrawn = nextSw !== undefined ? map.alternates.find((x) => x.name === nextSw.set)?.drawn[bone] : undefined;
|
|
1244
|
+
if (nextDrawn !== undefined) {
|
|
1245
|
+
const distNext = (i: number) => Math.abs(wrap(target[i]! - nextDrawn));
|
|
1246
|
+
if (distNext(b + 1) < distNext(b)) {
|
|
1247
|
+
while (b >= a && delta(b) !== 0 && (b === a || distNext(b) < distNext(b - 1))) {
|
|
1248
|
+
b--;
|
|
1249
|
+
trimmedExit = true;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
if (a > b) return;
|
|
1031
1255
|
const da = delta(a);
|
|
1032
1256
|
const db = delta(b);
|
|
1033
|
-
const easeIn = a > 0 && da !== 0 && Math.sign(delta(a - 1)) === Math.sign(da)
|
|
1257
|
+
const easeIn = a > 0 && da !== 0 && Math.sign(delta(a - 1)) === Math.sign(da) && !tightHold
|
|
1034
1258
|
? Math.max(ART_EASE_FRAMES, Math.ceil(Math.abs(da) / ART_EASE_STEP) - 1)
|
|
1035
1259
|
: 0;
|
|
1036
|
-
const easeOut = b + 1 < n && db !== 0 && Math.sign(delta(b + 1)) === Math.sign(db)
|
|
1260
|
+
const easeOut = b + 1 < n && db !== 0 && Math.sign(delta(b + 1)) === Math.sign(db) && !tightHold && !trimmedExit
|
|
1037
1261
|
? Math.max(ART_EASE_FRAMES, Math.ceil(Math.abs(db) / ART_EASE_STEP) - 1)
|
|
1038
1262
|
: 0;
|
|
1039
1263
|
for (let i = a; i <= b; i++) {
|