@rig-c/pose 0.22.2 → 0.24.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 +8 -6
- package/package.json +1 -1
- package/src/face.ts +295 -2
- package/src/map.ts +165 -14
- package/src/retarget.ts +297 -31
- package/src/rigc.ts +29 -2
- package/src/run.ts +34 -5
- package/src/switch.ts +172 -18
- package/src/verify.ts +30 -1
package/src/switch.ts
CHANGED
|
@@ -58,6 +58,15 @@ import type { MotionKey, MotionTrack } from './retarget.ts';
|
|
|
58
58
|
import { pictureAngle, wrap } from './retarget.ts';
|
|
59
59
|
import type { MeshFolds } from './rigc.ts';
|
|
60
60
|
|
|
61
|
+
export const SWITCH_LEAVE_HYSTERESIS = 15;
|
|
62
|
+
export const SHORT_SWITCH_WEIGHT = 0.5;
|
|
63
|
+
|
|
64
|
+
export interface ShortSwitchInfo {
|
|
65
|
+
cutoff: number;
|
|
66
|
+
fractions: ReadonlyMap<string, readonly number[]>;
|
|
67
|
+
weight?: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
61
70
|
export interface SwitchOptions {
|
|
62
71
|
/** Degrees: the largest segment difference at which a set may show. */
|
|
63
72
|
switchTolerance: number;
|
|
@@ -65,6 +74,12 @@ export interface SwitchOptions {
|
|
|
65
74
|
switchMin: number;
|
|
66
75
|
/** Where a boundary is keyed: 'first' (the CLI's default) keeps the frame the selection gave it; 'peak' moves it to the fastest qualifying frame of its window (#31). */
|
|
67
76
|
switchAt: 'peak' | 'first';
|
|
77
|
+
/** Degrees above `switchTolerance` at which an entered set is left (#180; default: `SWITCH_LEAVE_HYSTERESIS` = 15). */
|
|
78
|
+
switchHysteresis?: number;
|
|
79
|
+
/** Foreshortened limb fractions for reduced weight in switch distance (#180). */
|
|
80
|
+
shortInfo?: ShortSwitchInfo;
|
|
81
|
+
/** Per chain, the reference pose's normalised wrist-from-shoulder offset on each frame, `(wrist - shoulder) / scale`, y down (#180). */
|
|
82
|
+
handOffsets?: ReadonlyMap<string, ReadonlyArray<readonly [number, number]>>;
|
|
68
83
|
}
|
|
69
84
|
|
|
70
85
|
/** One boundary of a span: where the selection put it, where it is keyed, and how fast the chains it changes move on each. */
|
|
@@ -158,16 +173,41 @@ export interface Switches {
|
|
|
158
173
|
chainHolds?: Array<{ set: string; chain: string; frames: [number, number] }>;
|
|
159
174
|
}
|
|
160
175
|
|
|
161
|
-
/** Per set and chain, its distance to the pose on every frame: the largest segment difference over that chain. */
|
|
162
|
-
export function chainDistances(map: JointMap, targets: ReadonlyMap<string, readonly number[]>, n: number): Map<string, Map<string, number[]>> {
|
|
176
|
+
/** Per set and chain, its distance to the pose on every frame: the largest segment difference over that chain (with foreshortened `SHORT` segments weighted by `SHORT_SWITCH_WEIGHT`, #180). */
|
|
177
|
+
export function chainDistances(map: JointMap, targets: ReadonlyMap<string, readonly number[]>, n: number, shortInfo?: ShortSwitchInfo): Map<string, Map<string, number[]>> {
|
|
163
178
|
const out = new Map<string, Map<string, number[]>>();
|
|
179
|
+
const wShort = shortInfo?.weight ?? SHORT_SWITCH_WEIGHT;
|
|
164
180
|
for (const a of map.alternates) {
|
|
165
181
|
const byChain = new Map<string, number[]>();
|
|
166
182
|
for (const cn of a.chains) {
|
|
167
183
|
const c = map.chains.find((x) => x.name === cn)!;
|
|
168
|
-
const
|
|
184
|
+
const foldedKey = c.joints.length >= 3 && (() => {
|
|
185
|
+
const j0 = a.pose[c.joints[0]!];
|
|
186
|
+
const j1 = a.pose[c.joints[1]!];
|
|
187
|
+
const j2 = a.pose[c.joints[c.joints.length - 1]!];
|
|
188
|
+
return j0 !== undefined && j1 !== undefined && j2 !== undefined && Math.hypot(j2[0] - j0[0], j2[1] - j0[1]) <= Math.hypot(j1[0] - j0[0], j1[1] - j0[1]);
|
|
189
|
+
})();
|
|
190
|
+
const segs = c.bones.map((b, k) => {
|
|
191
|
+
const frac = shortInfo?.fractions.get(b);
|
|
192
|
+
const hasShort = frac !== undefined && frac.some((f) => f < shortInfo!.cutoff);
|
|
193
|
+
return {
|
|
194
|
+
asked: targets.get(b)!,
|
|
195
|
+
key: pictureAngle(a.pose[c.joints[k]!]!, a.pose[c.joints[k + 1]!]!),
|
|
196
|
+
frac,
|
|
197
|
+
alwaysShort: foldedKey && hasShort,
|
|
198
|
+
};
|
|
199
|
+
});
|
|
169
200
|
const d: number[] = [];
|
|
170
|
-
for (let i = 0; i < n; i++)
|
|
201
|
+
for (let i = 0; i < n; i++) {
|
|
202
|
+
d.push(
|
|
203
|
+
Math.max(
|
|
204
|
+
...segs.map((s) => {
|
|
205
|
+
const isShort = s.alwaysShort || (s.frac !== undefined && s.frac[i]! < shortInfo!.cutoff);
|
|
206
|
+
return (isShort ? wShort : 1) * Math.abs(wrap(s.asked[i]! - s.key));
|
|
207
|
+
}),
|
|
208
|
+
),
|
|
209
|
+
);
|
|
210
|
+
}
|
|
171
211
|
byChain.set(cn, d);
|
|
172
212
|
}
|
|
173
213
|
out.set(a.name, byChain);
|
|
@@ -176,8 +216,8 @@ export function chainDistances(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
176
216
|
}
|
|
177
217
|
|
|
178
218
|
/** Per set, its distance to the pose on every frame: the largest segment difference over its chains. */
|
|
179
|
-
export function setDistances(map: JointMap, targets: ReadonlyMap<string, readonly number[]>, n: number): Map<string, number[]> {
|
|
180
|
-
const byChain = chainDistances(map, targets, n);
|
|
219
|
+
export function setDistances(map: JointMap, targets: ReadonlyMap<string, readonly number[]>, n: number, shortInfo?: ShortSwitchInfo): Map<string, number[]> {
|
|
220
|
+
const byChain = chainDistances(map, targets, n, shortInfo);
|
|
181
221
|
const out = new Map<string, number[]>();
|
|
182
222
|
for (const a of map.alternates) {
|
|
183
223
|
const cs = a.chains.map((cn) => byChain.get(a.name)!.get(cn)!);
|
|
@@ -276,7 +316,17 @@ function groupLayering(group: readonly AlternateSet[], map: JointMap, slots: Rig
|
|
|
276
316
|
if (bySide !== undefined) slotChain.set(s, bySide);
|
|
277
317
|
else if (groupChains.length === 1) slotChain.set(s, groupChains[0]!);
|
|
278
318
|
}
|
|
279
|
-
|
|
319
|
+
const isChainBehindBase = (a: AlternateSet, cn: string): boolean => {
|
|
320
|
+
if (!a.chains.includes(cn)) return false;
|
|
321
|
+
if (isBehind(a)) return true;
|
|
322
|
+
const setupSlots = hiddenSetup.filter((s) => slotChain.get(s) === cn && slots.get(s)?.bone !== torsoSlot);
|
|
323
|
+
const setSlots = Object.keys(a.slots).filter((s) => slotChain.get(s) === cn && rank.has(s));
|
|
324
|
+
if (setupSlots.length === 0 || setSlots.length === 0) return false;
|
|
325
|
+
const boneSetup = setupSlots.filter((s) => boneOwner.has(slots.get(s)?.bone ?? ''));
|
|
326
|
+
const refSetup = boneSetup.length > 0 ? boneSetup : setupSlots;
|
|
327
|
+
return setupSlots.every((s) => a.hides.includes(s)) && Math.max(...setSlots.map((s) => rank.get(s)!)) < Math.min(...refSetup.map((s) => rank.get(s)!));
|
|
328
|
+
};
|
|
329
|
+
return { torsoSlot, torsoRank, touched, isBehind, isFront, isChainBehindBase, baseInFront, slotChain };
|
|
280
330
|
}
|
|
281
331
|
|
|
282
332
|
/**
|
|
@@ -365,6 +415,24 @@ function moveBoundaries(vals: ReadonlyArray<AlternateSet | null>, pos: readonly
|
|
|
365
415
|
/** The key time for a change that shows on frame i: half a frame before it, 0 on the first frame. */
|
|
366
416
|
const changeAt = (times: readonly number[], i: number): number => (i === 0 ? 0 : r6((times[i - 1]! + times[i]!) / 2));
|
|
367
417
|
|
|
418
|
+
function keyHandOffset(a: AlternateSet, map: JointMap, cn: string): [number, number] | null {
|
|
419
|
+
const c = map.chains.find((x) => x.name === cn);
|
|
420
|
+
if (c === undefined || c.joints.length < 2) return null;
|
|
421
|
+
const sh = a.pose[c.joints[0]!];
|
|
422
|
+
const wr = a.pose[c.joints[c.joints.length - 1]!];
|
|
423
|
+
if (sh === undefined || wr === undefined) return null;
|
|
424
|
+
const rs = a.pose.r_shoulder;
|
|
425
|
+
const ls = a.pose.l_shoulder;
|
|
426
|
+
const scale = rs !== undefined && ls !== undefined
|
|
427
|
+
? Math.hypot(ls[0] - rs[0], ls[1] - rs[1])
|
|
428
|
+
: c.joints.slice(0, -1).reduce((s, j, k) => {
|
|
429
|
+
const p0 = a.pose[j];
|
|
430
|
+
const p1 = a.pose[c.joints[k + 1]!];
|
|
431
|
+
return p0 !== undefined && p1 !== undefined ? s + Math.hypot(p1[0] - p0[0], p1[1] - p0[1]) : s;
|
|
432
|
+
}, 0);
|
|
433
|
+
return scale > 1e-6 ? [(wr[0] - sh[0]) / scale, (wr[1] - sh[1]) / scale] : null;
|
|
434
|
+
}
|
|
435
|
+
|
|
368
436
|
/**
|
|
369
437
|
* Select the switches, and key them: per frame and per group of competing
|
|
370
438
|
* sets, the nearest within the tolerance; runs shorter than the minimum span
|
|
@@ -374,8 +442,9 @@ const changeAt = (times: readonly number[], i: number): number => (i === 0 ? 0 :
|
|
|
374
442
|
*/
|
|
375
443
|
export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, readonly number[]>, times: readonly number[], duration: number, slots: RigSlots, opts: SwitchOptions): Switches {
|
|
376
444
|
const n = times.length;
|
|
377
|
-
const byChain = chainDistances(map, targets, n);
|
|
378
|
-
const distances = setDistances(map, targets, n);
|
|
445
|
+
const byChain = chainDistances(map, targets, n, opts.shortInfo);
|
|
446
|
+
const distances = setDistances(map, targets, n, opts.shortInfo);
|
|
447
|
+
const hyst = opts.switchHysteresis ?? SWITCH_LEAVE_HYSTERESIS;
|
|
379
448
|
const switches: Switch[] = [];
|
|
380
449
|
const groups = competing(map.alternates);
|
|
381
450
|
for (const group of groups) {
|
|
@@ -419,6 +488,39 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
419
488
|
// minimum span exists to prevent (measured on the dancer: one-frame breaks at
|
|
420
489
|
// a blurred frame inside a span of one set).
|
|
421
490
|
bridgeSame(pick);
|
|
491
|
+
// #180: hysteresis on leaving a set — bridge internal excursions that stay within
|
|
492
|
+
// tolerance + hyst (never exceeding tolerance + hyst for >= switchMin), then drop
|
|
493
|
+
// runs shorter than switchMin, then hold trailing shelves within tolerance + hyst.
|
|
494
|
+
const useHyst = (a: AlternateSet) =>
|
|
495
|
+
hyst > 0 && (opts.switchHysteresis !== undefined || (a.chains.length >= 2 && opts.shortInfo !== undefined));
|
|
496
|
+
if (hyst > 0) {
|
|
497
|
+
const allowedOn = (a: AlternateSet, k: number) => a.frames === null || a.frames.some(([f, l]) => k >= f && k <= l);
|
|
498
|
+
for (let i = 0; i < n;) {
|
|
499
|
+
const a = pick[i];
|
|
500
|
+
let j = i;
|
|
501
|
+
while (j + 1 < n && pick[j + 1] === a) j++;
|
|
502
|
+
const before = i > 0 ? pick[i - 1] : null;
|
|
503
|
+
const after = j + 1 < n ? pick[j + 1] : null;
|
|
504
|
+
if (a === null && before !== null && before !== undefined && before === after && useHyst(before)) {
|
|
505
|
+
const ds = distances.get(before.name)!;
|
|
506
|
+
let leftLong = false;
|
|
507
|
+
for (let k = i; k <= j;) {
|
|
508
|
+
if (!allowedOn(before, k) || ds[k]! > opts.switchTolerance + hyst) {
|
|
509
|
+
let m = k;
|
|
510
|
+
while (m + 1 <= j && (!allowedOn(before, m + 1) || ds[m + 1]! > opts.switchTolerance + hyst)) m++;
|
|
511
|
+
const to = m + 1 < n ? times[m + 1]! : duration;
|
|
512
|
+
if (!allowedOn(before, k) || to - times[k]! >= opts.switchMin - 1e-9) {
|
|
513
|
+
leftLong = true;
|
|
514
|
+
break;
|
|
515
|
+
}
|
|
516
|
+
k = m + 1;
|
|
517
|
+
} else k++;
|
|
518
|
+
}
|
|
519
|
+
if (!leftLong) for (let k = i; k <= j; k++) pick[k] = before;
|
|
520
|
+
}
|
|
521
|
+
i = j + 1;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
422
524
|
// A run shorter than the minimum span shows the base art.
|
|
423
525
|
for (let i = 0; i < n;) {
|
|
424
526
|
const a = pick[i];
|
|
@@ -428,6 +530,32 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
428
530
|
if (a !== null && a !== undefined && to - times[i]! < opts.switchMin - 1e-9) for (let k = i; k <= j; k++) pick[k] = null;
|
|
429
531
|
i = j + 1;
|
|
430
532
|
}
|
|
533
|
+
if (hyst > 0) {
|
|
534
|
+
const allowedOn = (a: AlternateSet, k: number) => a.frames === null || a.frames.some(([f, l]) => k >= f && k <= l);
|
|
535
|
+
const anyWithinTol = (a: AlternateSet, k: number) => a.chains.some((cn) => (byChain.get(a.name)?.get(cn)?.[k] ?? Infinity) <= opts.switchTolerance);
|
|
536
|
+
const hasShortAt = (a: AlternateSet, k: number) =>
|
|
537
|
+
opts.shortInfo !== undefined &&
|
|
538
|
+
Object.keys(a.drawn).some((b) => (opts.shortInfo!.fractions.get(b)?.[k] ?? 1) < 0.60);
|
|
539
|
+
for (let i = 0; i < n;) {
|
|
540
|
+
const a = pick[i];
|
|
541
|
+
let j = i;
|
|
542
|
+
while (j + 1 < n && pick[j + 1] === a) j++;
|
|
543
|
+
if (a !== null && a !== undefined && useHyst(a)) {
|
|
544
|
+
const ds = distances.get(a.name)!;
|
|
545
|
+
let l = i;
|
|
546
|
+
while (l - 1 >= 0 && pick[l - 1] === null && allowedOn(a, l - 1) && ds[l - 1]! <= opts.switchTolerance + hyst && anyWithinTol(a, l - 1) && hasShortAt(a, l - 1)) l--;
|
|
547
|
+
let m = j;
|
|
548
|
+
while (m + 1 < n && pick[m + 1] === null && allowedOn(a, m + 1) && ds[m + 1]! <= opts.switchTolerance + hyst && anyWithinTol(a, m + 1)) m++;
|
|
549
|
+
if (i - l >= 1 || m - j >= 2 || (m - j === 1 && hasShortAt(a, j + 1))) {
|
|
550
|
+
for (let k = l; k < i; k++) pick[k] = a;
|
|
551
|
+
for (let k = j + 1; k <= m; k++) pick[k] = a;
|
|
552
|
+
j = m;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
i = j + 1;
|
|
556
|
+
}
|
|
557
|
+
bridgeSame(pick);
|
|
558
|
+
}
|
|
431
559
|
// #136: a short front-set window (< 2 × switchMin) between two behind-set windows is dropped.
|
|
432
560
|
if (layer.torsoRank !== null) {
|
|
433
561
|
let dropped = false;
|
|
@@ -498,9 +626,8 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
498
626
|
for (let i = sw.frames[0]; i <= sw.frames[1]; i++) row[i] = o;
|
|
499
627
|
}
|
|
500
628
|
}
|
|
501
|
-
// #136/#139/#162: between two sets that
|
|
502
|
-
// neither set stays
|
|
503
|
-
// claim and whose arm is read in front — not a fallback.
|
|
629
|
+
// #136/#139/#162/#180: between two sets that draw an arm behind the base front setup (at least one behind the torso),
|
|
630
|
+
// a frame that belongs to neither set stays on the nearer set's layer (chosen by hand position when available, #180).
|
|
504
631
|
const chainHolds: Array<{ set: string; chain: string; frames: [number, number] }> = [];
|
|
505
632
|
for (const group of groups) {
|
|
506
633
|
const layer = groupLayering(group, map, slots);
|
|
@@ -523,11 +650,29 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
523
650
|
const sw1 = gSwitches[m + 1]!;
|
|
524
651
|
const set0 = group.find((a) => a.name === sw0.set)!;
|
|
525
652
|
const set1 = group.find((a) => a.name === sw1.set)!;
|
|
526
|
-
if (!layer.isBehind(set0)
|
|
653
|
+
if (!layer.isBehind(set0) && !layer.isBehind(set1)) continue;
|
|
527
654
|
const g0 = sw0.frames[1] + 1;
|
|
528
655
|
const g1 = sw1.frames[0] - 1;
|
|
529
656
|
if (g0 > g1) continue;
|
|
530
|
-
const chains = [...new Set([...set0.chains, ...set1.chains])];
|
|
657
|
+
const chains = [...new Set([...set0.chains, ...set1.chains])].filter((cn) => layer.isChainBehindBase(set0, cn) && layer.isChainBehindBase(set1, cn));
|
|
658
|
+
if (chains.length === 0) continue;
|
|
659
|
+
for (const cn of chains) {
|
|
660
|
+
if (!layer.isBehind(set0)) for (let i = sw0.frames[0]; i <= sw0.frames[1]; i++) cClamp.get(cn)![i] = set0;
|
|
661
|
+
if (!layer.isBehind(set1)) for (let i = sw1.frames[0]; i <= sw1.frames[1]; i++) cClamp.get(cn)![i] = set1;
|
|
662
|
+
}
|
|
663
|
+
const nearerOf = (cn: string, i: number, d0: number, d1: number): AlternateSet => {
|
|
664
|
+
if (set0 !== set1) {
|
|
665
|
+
const ref = opts.handOffsets?.get(cn)?.[i];
|
|
666
|
+
const k0 = keyHandOffset(set0, map, cn);
|
|
667
|
+
const k1 = keyHandOffset(set1, map, cn);
|
|
668
|
+
if (ref !== undefined && k0 !== null && k1 !== null) {
|
|
669
|
+
const h0 = Math.hypot(ref[0] - k0[0], ref[1] - k0[1]);
|
|
670
|
+
const h1 = Math.hypot(ref[0] - k1[0], ref[1] - k1[1]);
|
|
671
|
+
return h0 <= h1 ? set0 : set1;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
return d0 <= d1 ? set0 : set1;
|
|
675
|
+
};
|
|
531
676
|
const cPick = new Map<string, Array<AlternateSet | null>>();
|
|
532
677
|
for (const cn of chains) {
|
|
533
678
|
const arr: Array<AlternateSet | null> = Array.from({ length: n }, () => null);
|
|
@@ -580,7 +725,7 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
580
725
|
} else {
|
|
581
726
|
for (const cn of chains) {
|
|
582
727
|
const arr = cPick.get(cn)!;
|
|
583
|
-
const frontSets = group.filter((a) => layer.isFront(a) && a.chains.includes(cn));
|
|
728
|
+
const frontSets = group.filter((a) => layer.isFront(a) && a.chains.includes(cn) && a !== set0 && a !== set1);
|
|
584
729
|
const d0s = byChain.get(set0.name)?.get(cn);
|
|
585
730
|
const d1s = byChain.get(set1.name)?.get(cn);
|
|
586
731
|
for (let i = g0; i <= g1; i++) {
|
|
@@ -589,7 +734,7 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
589
734
|
if (!claimedFront) {
|
|
590
735
|
const d0 = d0s?.[i] ?? Infinity;
|
|
591
736
|
const d1 = d1s?.[i] ?? Infinity;
|
|
592
|
-
arr[i] = d0
|
|
737
|
+
arr[i] = nearerOf(cn, i, d0, d1);
|
|
593
738
|
}
|
|
594
739
|
}
|
|
595
740
|
for (let i = g0; i <= g1;) {
|
|
@@ -604,7 +749,7 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
604
749
|
for (let k = i; k <= j; k++) {
|
|
605
750
|
const d0 = d0s?.[k] ?? Infinity;
|
|
606
751
|
const d1 = d1s?.[k] ?? Infinity;
|
|
607
|
-
arr[k] = d0
|
|
752
|
+
arr[k] = nearerOf(cn, k, d0, d1);
|
|
608
753
|
}
|
|
609
754
|
} else if (before !== null && before === after && a !== before) {
|
|
610
755
|
for (let k = i; k <= j; k++) arr[k] = before;
|
|
@@ -626,6 +771,9 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
626
771
|
}
|
|
627
772
|
i = j + 1;
|
|
628
773
|
}
|
|
774
|
+
for (let i = g0; i <= g1; i++) {
|
|
775
|
+
if (arr[i] !== null) cClamp.get(cn)![i] = arr[i]!;
|
|
776
|
+
}
|
|
629
777
|
}
|
|
630
778
|
}
|
|
631
779
|
for (const s of layer.touched) {
|
|
@@ -636,7 +784,7 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
636
784
|
const a = cPick.get(cn)?.[i] ?? null;
|
|
637
785
|
if (a !== null) shown.get(s)![i] = a.slots[s] ?? null;
|
|
638
786
|
}
|
|
639
|
-
if (layer.torsoSlot !== null && shown.has(layer.torsoSlot)) {
|
|
787
|
+
if (layer.torsoSlot !== null && shown.has(layer.torsoSlot) && layer.isBehind(set0) && layer.isBehind(set1)) {
|
|
640
788
|
const picked = chains.map((cn) => cPick.get(cn)![i]);
|
|
641
789
|
if (picked.every((a) => a !== null)) shown.get(layer.torsoSlot)![i] = picked[0]!.slots[layer.torsoSlot] ?? slots.get(layer.torsoSlot)!.setup;
|
|
642
790
|
}
|
|
@@ -920,6 +1068,12 @@ export function clampArt(switches: readonly Switch[], map: JointMap, targets: Ma
|
|
|
920
1068
|
*/
|
|
921
1069
|
export const RAISED_WITHIN = 30;
|
|
922
1070
|
|
|
1071
|
+
/**
|
|
1072
|
+
* --raised-by-head (#179): a chain is placed by the head when its key skeleton's
|
|
1073
|
+
* wrist lies within this many eye distances of the head centre.
|
|
1074
|
+
*/
|
|
1075
|
+
export const RAISED_WRIST_EYES = 2;
|
|
1076
|
+
|
|
923
1077
|
/** Whether a drawn world angle, Spine degrees, is within `RAISED_WITHIN` of straight up (90°). */
|
|
924
1078
|
export const isRaised = (drawn: number): boolean => Math.abs(wrap(drawn - 90)) <= RAISED_WITHIN;
|
|
925
1079
|
|
package/src/verify.ts
CHANGED
|
@@ -561,6 +561,22 @@ export function measurePosture(posed: Array<Map<string, RawBone>>, lengths: Read
|
|
|
561
561
|
if (b.has('head')) return b.get('head')!.worldY;
|
|
562
562
|
return rigNeckY(b);
|
|
563
563
|
};
|
|
564
|
+
const headBoneName = (() => {
|
|
565
|
+
if (headChain !== undefined) {
|
|
566
|
+
const k = headChain.joints.findIndex((j, idx) => idx + 1 < headChain.joints.length && ((j === 'r_eye' && headChain.joints[idx + 1] === 'l_eye') || (j === 'l_eye' && headChain.joints[idx + 1] === 'r_eye')));
|
|
567
|
+
if (k >= 0) return headChain.bones[k]!;
|
|
568
|
+
}
|
|
569
|
+
return posed[0]?.has('head') ? 'head' : null;
|
|
570
|
+
})();
|
|
571
|
+
const maxNod = (() => {
|
|
572
|
+
if (headBoneName === null || rigNeck === null || posed.length === 0) return Infinity;
|
|
573
|
+
const b0 = posed[0]!;
|
|
574
|
+
const hb0 = b0.get(headBoneName);
|
|
575
|
+
const nk0 = pointAt(b0, lengths, rigNeck);
|
|
576
|
+
if (hb0 === undefined || nk0 === null) return Infinity;
|
|
577
|
+
const d = Math.hypot(hb0.worldX - nk0[0], hb0.worldY - nk0[1]);
|
|
578
|
+
return d > 1e-6 ? 0.022 * d : Infinity;
|
|
579
|
+
})();
|
|
564
580
|
for (const sp of r.feet.supports) {
|
|
565
581
|
const refHip: number[] = [];
|
|
566
582
|
const rigHipY: number[] = [];
|
|
@@ -568,6 +584,7 @@ export function measurePosture(posed: Array<Map<string, RawBone>>, lengths: Read
|
|
|
568
584
|
const rigNeckS: number[] = [];
|
|
569
585
|
const refEyesNeck: number[] = [];
|
|
570
586
|
const rigEyesNeck: number[] = [];
|
|
587
|
+
const rigEyeHead: number[] = [];
|
|
571
588
|
for (let i = sp.frames[0]; i <= sp.frames[1]; i++) {
|
|
572
589
|
const b = posed[i]!;
|
|
573
590
|
const rh = at(i, 'r_hip');
|
|
@@ -587,15 +604,27 @@ export function measurePosture(posed: Array<Map<string, RawBone>>, lengths: Read
|
|
|
587
604
|
if (re !== null && le !== null && ey !== null) {
|
|
588
605
|
refEyesNeck.push(-((re[1] + le[1]) / 2 - nk[1]) * s);
|
|
589
606
|
rigEyesNeck.push(ey - ny);
|
|
607
|
+
const hy = headBoneName !== null && b.has(headBoneName) ? b.get(headBoneName)!.worldY : ny;
|
|
608
|
+
rigEyeHead.push(ey - hy);
|
|
590
609
|
}
|
|
591
610
|
}
|
|
592
611
|
}
|
|
612
|
+
const boundedRefEyesNeck = (() => {
|
|
613
|
+
if (!(maxNod < Infinity) || refEyesNeck.length === 0) return refEyesNeck;
|
|
614
|
+
const dr = detrend(refEyesNeck);
|
|
615
|
+
const dgEH = detrend(rigEyeHead);
|
|
616
|
+
return refEyesNeck.map((v, i) => {
|
|
617
|
+
const base = v - dr[i]!;
|
|
618
|
+
const nod = Math.max(-maxNod, Math.min(maxNod, dr[i]! - dgEH[i]!));
|
|
619
|
+
return base + dgEH[i]! + nod;
|
|
620
|
+
});
|
|
621
|
+
})();
|
|
593
622
|
bounce.push({
|
|
594
623
|
side: sp.side,
|
|
595
624
|
frames: [sp.frames[0], sp.frames[1]],
|
|
596
625
|
hip: bounceMetric(refHip, rigHipY),
|
|
597
626
|
neck: bounceMetric(refNeck, rigNeckS),
|
|
598
|
-
eyesNeck: bounceMetric(
|
|
627
|
+
eyesNeck: bounceMetric(boundedRefEyesNeck, rigEyesNeck),
|
|
599
628
|
});
|
|
600
629
|
}
|
|
601
630
|
}
|