@rig-c/pose 0.19.0 → 0.20.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 +10 -10
- package/cli.ts +1 -1
- package/package.json +1 -1
- package/src/clean.ts +107 -20
- package/src/legs.ts +144 -35
- package/src/pose.ts +11 -4
- package/src/retarget.ts +199 -11
- package/src/run.ts +3 -2
- package/src/switch.ts +33 -5
- package/src/verify.ts +155 -1
package/src/legs.ts
CHANGED
|
@@ -32,9 +32,11 @@
|
|
|
32
32
|
* derivations in y over `PLANT_BLEND` frames while taking the new derivation's x
|
|
33
33
|
* immediately (rig-c/pose#102). A jump — both ankles and mid_hip more
|
|
34
34
|
* than `JUMP_RISE` leg lengths above their rolling floor (the 95th-percentile
|
|
35
|
-
* picture y over ± `FLOOR_SECONDS`), for ≥ `JUMP_FRAMES` frames
|
|
36
|
-
*
|
|
37
|
-
*
|
|
35
|
+
* picture y over ± `FLOOR_SECONDS`), for ≥ `JUMP_FRAMES` frames, excluding a
|
|
36
|
+
* hand-over whose back ankle stays up and holds its height in y (`< STILL_SPEED`
|
|
37
|
+
* for `STILL_FRAMES` frames) when the front foot lands (a raised heel,
|
|
38
|
+
* rig-c/pose#101) — releases the plant: the hip follows the pose's travel from
|
|
39
|
+
* where it was, and re-plants on landing. A step does too: while the supporting ankle moves more than
|
|
38
40
|
* `STEP_SPEED` leg lengths a frame its plant is released, the other foot takes
|
|
39
41
|
* support if it is planted (not released, not moving) and otherwise the hip
|
|
40
42
|
* follows the pose; the released ankle may support again once it has moved
|
|
@@ -266,13 +268,16 @@ export interface LegScale {
|
|
|
266
268
|
high: Array<[number, number]>;
|
|
267
269
|
/** The scale keyed on each frame (clamped). */
|
|
268
270
|
scale: number[];
|
|
271
|
+
/** The scale the pose asked for on each frame before the clamp floor (`min(ratio, limits[1])`, rig-c/pose#107). */
|
|
272
|
+
asked: number[];
|
|
269
273
|
}
|
|
270
274
|
|
|
271
|
-
/** One segment's scale per frame: picture length ÷ its 95th percentile, clamped; and the runs clamped. */
|
|
275
|
+
/** One segment's scale per frame: picture length ÷ its 95th percentile, clamped; its asked scale without the clamp floor (#107); and the runs clamped. */
|
|
272
276
|
export function legScaleOf(lengths: readonly number[], limits: [number, number]): Omit<LegScale, 'chain' | 'bone' | 'joints'> {
|
|
273
277
|
const straight = percentile(lengths, STRAIGHT_PERCENTILE);
|
|
274
278
|
const ratio = lengths.map((l) => l / straight);
|
|
275
279
|
const scale = ratio.map((r) => Math.min(Math.max(r, limits[0]), limits[1]));
|
|
280
|
+
const asked = ratio.map((r) => Math.min(r, limits[1]));
|
|
276
281
|
const sorted = [...ratio].sort((a, b) => a - b);
|
|
277
282
|
const mid = sorted.length >> 1;
|
|
278
283
|
const median = sorted.length % 2 === 1 ? sorted[mid]! : (sorted[mid - 1]! + sorted[mid]!) / 2;
|
|
@@ -283,6 +288,7 @@ export function legScaleOf(lengths: readonly number[], limits: [number, number])
|
|
|
283
288
|
low: runs(ratio.map((r) => r < limits[0])),
|
|
284
289
|
high: runs(ratio.map((r) => r > limits[1])),
|
|
285
290
|
scale,
|
|
291
|
+
asked,
|
|
286
292
|
};
|
|
287
293
|
}
|
|
288
294
|
|
|
@@ -339,10 +345,18 @@ export interface Feet {
|
|
|
339
345
|
/**
|
|
340
346
|
* rig-c/pose#113, read from the lower ankle: each run of frames whose
|
|
341
347
|
* supporting side differs from the reading before #113 (`was`, `now`; null:
|
|
342
|
-
* no foot
|
|
348
|
+
* no foot is planted). Absent where the rule does not apply (in place, or
|
|
343
349
|
* without `lowerAnkle`).
|
|
344
350
|
*/
|
|
345
351
|
lowered?: Lowered[];
|
|
352
|
+
/**
|
|
353
|
+
* rig-c/pose#119: runs of frames after a jump or a release where no foot is
|
|
354
|
+
* planted because the only foot free under #113 is the higher one, and the
|
|
355
|
+
* lower foot supports as a tracked foot (its x follows its own scaled ankle
|
|
356
|
+
* track, its y sits on #85's floor) until a foot plants. Absent where the
|
|
357
|
+
* rule does not apply.
|
|
358
|
+
*/
|
|
359
|
+
tracked?: Array<{ side: Side; frames: [number, number] }>;
|
|
346
360
|
}
|
|
347
361
|
|
|
348
362
|
/** A run of frames whose support the lower-ankle rule (rig-c/pose#113) reads differently from the reading before it. */
|
|
@@ -383,7 +397,9 @@ export function rollingFloor(ys: readonly number[], times: readonly number[]): n
|
|
|
383
397
|
* other ankle has been that much lower for `PLANT_SWITCH_SECONDS` hands support
|
|
384
398
|
* to it when it is free (as before) and is otherwise released (the hip follows
|
|
385
399
|
* the pose until a foot may take support). Steps and jumps are read as before.
|
|
386
|
-
* The frames whose support this changes are `lowered`.
|
|
400
|
+
* The frames whose support this changes are `lowered`. On a frame where no foot
|
|
401
|
+
* is planted because the only foot free is the higher one (rig-c/pose#119), the
|
|
402
|
+
* lower foot is tracked (`tracked`).
|
|
387
403
|
*/
|
|
388
404
|
export function supportsOf(ankle: Record<Side, readonly Pt[]>, hip: Record<Side, readonly Pt[]>, times: readonly number[], secondsOf: (run: [number, number]) => number, inPlace = false, lowerAnkle = false): Feet {
|
|
389
405
|
if (inPlace || !lowerAnkle) return readSupports(ankle, hip, times, secondsOf, inPlace, false).feet;
|
|
@@ -407,6 +423,9 @@ function readSupports(ankle: Record<Side, readonly Pt[]>, hip: Record<Side, read
|
|
|
407
423
|
for (const s of SIDES) legLength[s] = percentile(ankle[s].map((a, i) => Math.hypot(a[0] - hip[s][i]![0], a[1] - hip[s][i]![1])), 0.5);
|
|
408
424
|
const mean = (legLength.r + legLength.l) / 2;
|
|
409
425
|
// A jump: both ankles and mid_hip above their rolling floors, for ≥ JUMP_FRAMES frames.
|
|
426
|
+
// #101: at a hand-over the front foot has not landed yet while the back ankle lifts its heel; when
|
|
427
|
+
// the front foot lands (the first frame after its lifted run) while the back ankle stays up, a back
|
|
428
|
+
// ankle that holds its height in y (< STILL_SPEED a frame for STILL_FRAMES frames) is a raised heel.
|
|
410
429
|
const midY = hip.r.map((h, i) => (h[1] + hip.l[i]![1]) / 2);
|
|
411
430
|
const rises: Array<{ ys: number[]; unit: number }> = [
|
|
412
431
|
{ ys: ankle.r.map((a) => a[1]), unit: legLength.r },
|
|
@@ -417,8 +436,23 @@ function readSupports(ankle: Record<Side, readonly Pt[]>, hip: Record<Side, read
|
|
|
417
436
|
const floor = rollingFloor(ys, times);
|
|
418
437
|
return ys.map((y, i) => floor[i]! - y > JUMP_RISE * unit);
|
|
419
438
|
});
|
|
439
|
+
const ankleRuns = { r: runs(up[0]!), l: runs(up[1]!) };
|
|
440
|
+
const stillY = (s: Side, from: number): boolean => {
|
|
441
|
+
if (from + STILL_FRAMES > n) return false;
|
|
442
|
+
for (let k = from; k < from + STILL_FRAMES; k++) {
|
|
443
|
+
if (!up[s === 'r' ? 0 : 1]![k] || Math.abs(ankle[s][k]![1] - ankle[s][k - 1]![1]) / legLength[s] >= STILL_SPEED) return false;
|
|
444
|
+
}
|
|
445
|
+
return true;
|
|
446
|
+
};
|
|
447
|
+
const raisedHeel = (a: number, b: number): boolean => {
|
|
448
|
+
const rSpan = ankleRuns.r.find(([s, e]) => s <= a && b <= e)!;
|
|
449
|
+
const lSpan = ankleRuns.l.find(([s, e]) => s <= a && b <= e)!;
|
|
450
|
+
if (rSpan[1] === lSpan[1]) return false;
|
|
451
|
+
const [fSpan, back]: [[number, number], Side] = rSpan[1] < lSpan[1] ? [rSpan, 'l'] : [lSpan, 'r'];
|
|
452
|
+
return stillY(back, fSpan[1] + 1);
|
|
453
|
+
};
|
|
420
454
|
const lifted = Array.from({ length: n }, (_, i) => up.every((u) => u[i]));
|
|
421
|
-
const jumps = runs(lifted).filter(([a, b]) => b - a + 1 >= JUMP_FRAMES);
|
|
455
|
+
const jumps = runs(lifted).filter(([a, b]) => b - a + 1 >= JUMP_FRAMES && !raisedHeel(a, b));
|
|
422
456
|
const jump = new Array<boolean>(n).fill(false);
|
|
423
457
|
for (const [a, b] of jumps) for (let i = a; i <= b; i++) jump[i] = true;
|
|
424
458
|
// Ankle speeds, leg lengths a frame (0 on the first frame).
|
|
@@ -432,6 +466,7 @@ function readSupports(ankle: Record<Side, readonly Pt[]>, hip: Record<Side, read
|
|
|
432
466
|
return true;
|
|
433
467
|
};
|
|
434
468
|
const support: Array<Side | null> = new Array<Side | null>(n).fill(null);
|
|
469
|
+
const trkAt: Array<Side | null> = new Array<Side | null>(n).fill(null);
|
|
435
470
|
const changes: Feet['changes'] = [];
|
|
436
471
|
const steps: Step[] = [];
|
|
437
472
|
const open: Partial<Record<Side, Step>> = {};
|
|
@@ -505,6 +540,9 @@ function readSupports(ankle: Record<Side, readonly Pt[]>, hip: Record<Side, read
|
|
|
505
540
|
freeFrom = -1;
|
|
506
541
|
}
|
|
507
542
|
}
|
|
543
|
+
if (lowerAnkle && cur === null && released[released.length - 1] !== i && SIDES.some((x) => free(x) && higher(x))) {
|
|
544
|
+
trkAt[i] = lower(['r', 'l'], i);
|
|
545
|
+
}
|
|
508
546
|
support[i] = cur;
|
|
509
547
|
}
|
|
510
548
|
const supports: Feet['supports'] = [];
|
|
@@ -515,7 +553,15 @@ function readSupports(ankle: Record<Side, readonly Pt[]>, hip: Record<Side, read
|
|
|
515
553
|
if (last !== undefined && last.side === s && last.frames[1] === i - 1) last.frames[1] = i;
|
|
516
554
|
else supports.push({ side: s, frames: [i, i] });
|
|
517
555
|
}
|
|
518
|
-
|
|
556
|
+
const tracked: Array<{ side: Side; frames: [number, number] }> = [];
|
|
557
|
+
for (let i = 0; i < n; i++) {
|
|
558
|
+
const s = trkAt[i];
|
|
559
|
+
if (s === null || s === undefined) continue;
|
|
560
|
+
const last = tracked[tracked.length - 1];
|
|
561
|
+
if (last !== undefined && last.side === s && last.frames[1] === i - 1) last.frames[1] = i;
|
|
562
|
+
else tracked.push({ side: s, frames: [i, i] });
|
|
563
|
+
}
|
|
564
|
+
return { feet: { legLength, support, supports, jumps, steps, changes, ...(tracked.length > 0 ? { tracked } : {}) }, released };
|
|
519
565
|
}
|
|
520
566
|
|
|
521
567
|
/** One support span's shift onto the floor (rig-c/pose#85): its side, its frames, and floor − its planted height, world units. */
|
|
@@ -532,6 +578,8 @@ export interface Floor {
|
|
|
532
578
|
shifts: FloorShift[];
|
|
533
579
|
}
|
|
534
580
|
|
|
581
|
+
let trackFn: ((i: number, s: Side) => number) | undefined;
|
|
582
|
+
|
|
535
583
|
/**
|
|
536
584
|
* The hip's world origin on every frame, from the supporting foot (header),
|
|
537
585
|
* with every support span on one floor (rig-c/pose#85). `offset(i, side)` is
|
|
@@ -548,7 +596,8 @@ export interface Floor {
|
|
|
548
596
|
* support still blends the two derivations' y over `PLANT_BLEND` frames, now
|
|
549
597
|
* between two spans on the floor, while holding the planted foot's x from the
|
|
550
598
|
* frame it lands on (#102); a jump follows the pose's travel from the hip
|
|
551
|
-
* it left (on the floor)
|
|
599
|
+
* it left (on the floor), carried so the landing frame meets the floor and no
|
|
600
|
+
* frame inside the flight puts a foot below it (#103). On a blend frame the hip is
|
|
552
601
|
* between the two derivations, so the supporting ankle is off its anchor:
|
|
553
602
|
* `blends` lists those frames with the anchor's y, and `holdBlend` stretches
|
|
554
603
|
* the supporting leg to keep it there. Null `floor` in the result: there is no
|
|
@@ -557,18 +606,24 @@ export interface Floor {
|
|
|
557
606
|
* `follow(i, side)` (#92, an in-place sequence): the supporting ankle's track,
|
|
558
607
|
* world x. A plant's anchor x then moves by follow(i) − follow(the frame it
|
|
559
608
|
* planted), and the loop is closed (`closeLoop`, `closure` in the result; null
|
|
560
|
-
* without `follow`).
|
|
609
|
+
* without `follow`). `track(i, side)` (#119): the same world-x ankle track for
|
|
610
|
+
* `feet.tracked` frames on a sequence not declared in place.
|
|
561
611
|
*/
|
|
562
|
-
export function plantHip(feet: Feet, offset: (i: number, s: Side) => Pt, travel: (i: number) => Pt, start: Pt, floor?: number, follow?: (i: number, s: Side) => number): { hip: Pt[]; floor: Floor | null; blends: BlendFrame[]; closure: Closure | null } {
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
612
|
+
export function plantHip(feet: Feet, offset: (i: number, s: Side) => Pt, travel: (i: number) => Pt, start: Pt, floor?: number, follow?: (i: number, s: Side) => number, track?: (i: number, s: Side) => number): { hip: Pt[]; floor: Floor | null; blends: BlendFrame[]; closure: Closure | null } {
|
|
613
|
+
trackFn = track;
|
|
614
|
+
try {
|
|
615
|
+
const picture = placeHip(feet, offset, travel, start, null, follow);
|
|
616
|
+
const heights = feet.supports.map((sp) => picture.anchors.get(sp.frames[0]));
|
|
617
|
+
if (heights.length === 0) return { hip: picture.hip, floor: null, blends: picture.blends, closure: null };
|
|
618
|
+
if (heights.some((h) => h === undefined)) throw new Error('plantHip: a support span starts on a frame no plant started on');
|
|
619
|
+
const y = floor ?? percentile(heights as number[], 0.5);
|
|
620
|
+
const shifts = feet.supports.map((sp, k) => ({ side: sp.side, frames: [sp.frames[0], sp.frames[1]] as [number, number], shift: y - heights[k]! }));
|
|
621
|
+
const placed = placeHip(feet, offset, travel, start, y, follow);
|
|
622
|
+
const closure = follow === undefined ? null : closeLoop(placed.hip);
|
|
623
|
+
return { hip: placed.hip, floor: { y, given: floor !== undefined, shifts }, blends: placed.blends, closure };
|
|
624
|
+
} finally {
|
|
625
|
+
trackFn = undefined;
|
|
626
|
+
}
|
|
572
627
|
}
|
|
573
628
|
|
|
574
629
|
/** In place (rig-c/pose#92): the hip's x travel from the first frame to the last before the loop was closed, world units, and the frames it was spread over. */
|
|
@@ -607,8 +662,14 @@ function placeHip(feet: Feet, offset: (i: number, s: Side) => Pt, travel: (i: nu
|
|
|
607
662
|
const hip: Pt[] = [];
|
|
608
663
|
const anchors = new Map<number, number>();
|
|
609
664
|
const blends: BlendFrame[] = [];
|
|
610
|
-
|
|
611
|
-
|
|
665
|
+
const tracked = new Array<Side | null>(n).fill(null);
|
|
666
|
+
for (const t of feet.tracked ?? []) for (let i = t.frames[0]; i <= t.frames[1]; i++) tracked[i] = t.side;
|
|
667
|
+
const trkX = trackFn ?? follow;
|
|
668
|
+
/** How the hip is derived on a frame: held over a foot's anchor, tracking the lower foot on the floor (#119), or carried by the pose's travel from a frame's hip (and for a jump against `floorY`, carried so the landing frame meets the floor and frames inside the flight never go below it, #103). */
|
|
669
|
+
type Mode =
|
|
670
|
+
| { kind: 'plant'; side: Side; anchor: Pt; from: number }
|
|
671
|
+
| { kind: 'track'; side: Side; anchor: Pt; from: number; at: Pt }
|
|
672
|
+
| { kind: 'travel'; from: number; at: Pt; end?: number; land?: number; carry?: number };
|
|
612
673
|
const of = (mode: Mode, i: number): Pt => {
|
|
613
674
|
if (mode.kind === 'plant') {
|
|
614
675
|
const o = offset(i, mode.side);
|
|
@@ -616,9 +677,40 @@ function placeHip(feet: Feet, offset: (i: number, s: Side) => Pt, travel: (i: nu
|
|
|
616
677
|
const dx = follow === undefined ? 0 : follow(i, mode.side) - follow(mode.from, mode.side);
|
|
617
678
|
return [mode.anchor[0] + dx - o[0], mode.anchor[1] - o[1]];
|
|
618
679
|
}
|
|
680
|
+
if (mode.kind === 'track') {
|
|
681
|
+
const o = offset(i, mode.side);
|
|
682
|
+
const dx = trkX === undefined ? o[0] - offset(mode.from, mode.side)[0] : trkX(i, mode.side) - trkX(mode.from, mode.side);
|
|
683
|
+
const y = floorY === null ? mode.at[1] + travel(i)[1] - travel(mode.from)[1] : mode.anchor[1] - o[1];
|
|
684
|
+
return [mode.anchor[0] + dx - o[0], y];
|
|
685
|
+
}
|
|
619
686
|
const a = travel(i);
|
|
620
687
|
const b = travel(mode.from);
|
|
621
|
-
|
|
688
|
+
let y = mode.at[1] + a[1] - b[1];
|
|
689
|
+
if (mode.carry !== undefined && mode.land !== undefined && mode.end !== undefined && floorY !== null) {
|
|
690
|
+
if (mode.land < n && i <= mode.land && mode.land > mode.from) {
|
|
691
|
+
y += mode.carry * ((i - mode.from) / (mode.land - mode.from));
|
|
692
|
+
}
|
|
693
|
+
if (i <= mode.end) {
|
|
694
|
+
y = Math.max(y, floorY - Math.min(offset(i, 'r')[1], offset(i, 'l')[1]));
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
return [mode.at[0] + a[0] - b[0], y];
|
|
698
|
+
};
|
|
699
|
+
/** A travel span from `startFrame` off frame `from`'s hip `at`: carried to meet `floorY` at landing when it contains a jump (#103). */
|
|
700
|
+
const makeTravel = (startFrame: number, from: number, at: Pt): Mode => {
|
|
701
|
+
let end = startFrame;
|
|
702
|
+
while (end + 1 < n && (feet.support[end + 1] ?? null) === null && (tracked[end + 1] ?? null) === null) end++;
|
|
703
|
+
const isJump = floorY !== null && feet.jumps.some(([ja, jb]) => ja <= end && jb >= startFrame);
|
|
704
|
+
if (!isJump) return { kind: 'travel', from, at };
|
|
705
|
+
const land = end + 1;
|
|
706
|
+
let carry = 0;
|
|
707
|
+
if (land < n) {
|
|
708
|
+
const landSide = (feet.support[land] ?? tracked[land] ?? null)!;
|
|
709
|
+
const rawLandY = at[1] + travel(land)[1] - travel(from)[1];
|
|
710
|
+
const targetLandY = floorY - offset(land, landSide)[1];
|
|
711
|
+
carry = targetLandY - rawLandY;
|
|
712
|
+
}
|
|
713
|
+
return { kind: 'travel', from, at, end, land, carry };
|
|
622
714
|
};
|
|
623
715
|
/** A plant from frame i: its anchor's x where the foot is, its y there or on the floor. */
|
|
624
716
|
const plant = (i: number, side: Side, at: Pt): Mode => {
|
|
@@ -630,23 +722,38 @@ function placeHip(feet: Feet, offset: (i: number, s: Side) => Pt, travel: (i: nu
|
|
|
630
722
|
let blendAt = -1;
|
|
631
723
|
for (let i = 0; i < n; i++) {
|
|
632
724
|
const s = feet.support[i] ?? null;
|
|
725
|
+
const tr = s === null ? (tracked[i] ?? null) : null;
|
|
633
726
|
if (i === 0) {
|
|
634
|
-
if (s
|
|
635
|
-
else {
|
|
727
|
+
if (s !== null) {
|
|
636
728
|
const o = offset(0, s);
|
|
637
729
|
mode = plant(0, s, [start[0] + o[0], start[1] + o[1]]);
|
|
730
|
+
} else if (tr !== null) {
|
|
731
|
+
const o = offset(0, tr);
|
|
732
|
+
mode = { kind: 'track', side: tr, anchor: [start[0] + o[0], floorY ?? (start[1] + o[1])], from: 0, at: start };
|
|
733
|
+
} else {
|
|
734
|
+
mode = makeTravel(0, 0, start);
|
|
638
735
|
}
|
|
639
|
-
} else if (s
|
|
640
|
-
mode
|
|
736
|
+
} else if (s !== null) {
|
|
737
|
+
if (mode!.kind !== 'plant' || mode!.side !== s) {
|
|
738
|
+
// The new support is held where it is on this frame, under the hip the old derivation gives here.
|
|
739
|
+
const here = of(mode!, i);
|
|
740
|
+
const o = offset(i, s);
|
|
741
|
+
from = mode;
|
|
742
|
+
mode = plant(i, s, [here[0] + o[0], here[1] + o[1]]);
|
|
743
|
+
blendAt = i;
|
|
744
|
+
}
|
|
745
|
+
} else if (tr !== null) {
|
|
746
|
+
if (mode!.kind !== 'track' || mode!.side !== tr) {
|
|
747
|
+
const prev = hip[i - 1]!;
|
|
748
|
+
const o = offset(i - 1, tr);
|
|
749
|
+
mode = { kind: 'track', side: tr, anchor: [prev[0] + o[0], floorY ?? (prev[1] + o[1])], from: i - 1, at: prev };
|
|
750
|
+
from = null;
|
|
751
|
+
blendAt = -1;
|
|
752
|
+
}
|
|
753
|
+
} else if (mode!.kind !== 'travel') {
|
|
754
|
+
mode = makeTravel(i, i - 1, hip[i - 1]!);
|
|
641
755
|
from = null;
|
|
642
756
|
blendAt = -1;
|
|
643
|
-
} else if (s !== null && (mode!.kind === 'travel' || (mode as { side: Side }).side !== s)) {
|
|
644
|
-
// The new support is held where it is on this frame, under the hip the old derivation gives here.
|
|
645
|
-
const here = of(mode!, i);
|
|
646
|
-
const o = offset(i, s);
|
|
647
|
-
from = mode;
|
|
648
|
-
mode = plant(i, s, [here[0] + o[0], here[1] + o[1]]);
|
|
649
|
-
blendAt = i;
|
|
650
757
|
}
|
|
651
758
|
const to = of(mode!, i);
|
|
652
759
|
if (from !== null && i - blendAt < PLANT_BLEND) {
|
|
@@ -802,10 +909,12 @@ export interface StancePosed {
|
|
|
802
909
|
export function stanceOf(feet: Feet, ankle: Record<Side, readonly Pt[]>, tolerance: number, posed: (i: number) => StancePosed, secondsOf: (run: [number, number]) => number, lower?: StanceLower): Stance {
|
|
803
910
|
const n = feet.support.length;
|
|
804
911
|
const stretch: Stance['stretch'] = new Array(n).fill(null);
|
|
912
|
+
const tracked = new Array<Side | null>(n).fill(null);
|
|
913
|
+
for (const t of feet.tracked ?? []) for (let i = t.frames[0]; i <= t.frames[1]; i++) tracked[i] = t.side;
|
|
805
914
|
type Read = { state: StanceState; side: Side; height: number };
|
|
806
915
|
const read: Array<Read | null> = new Array<Read | null>(n).fill(null);
|
|
807
916
|
for (let i = 0; i < n; i++) {
|
|
808
|
-
const s = feet.support[i] ?? null;
|
|
917
|
+
const s = feet.support[i] ?? tracked[i] ?? null;
|
|
809
918
|
if (s === null) continue;
|
|
810
919
|
const f: Side = s === 'r' ? 'l' : 'r';
|
|
811
920
|
read[i] = { state: 'both', side: f, height: (ankle[s][i]![1] - ankle[f][i]![1]) / feet.legLength[f] };
|
package/src/pose.ts
CHANGED
|
@@ -150,6 +150,7 @@ export interface HoldParams {
|
|
|
150
150
|
export interface CleanHold {
|
|
151
151
|
joint: Body18;
|
|
152
152
|
frames: [number, number];
|
|
153
|
+
excursion?: [number, number];
|
|
153
154
|
jitter: { before: number | null; after: number | null };
|
|
154
155
|
}
|
|
155
156
|
/**
|
|
@@ -432,10 +433,16 @@ function readCleaned(p: Problems, where: string, c: unknown, frames: number | nu
|
|
|
432
433
|
p.add('POSE_CLEANED', `${hw} is ${typeName(h)}; a hold is { joint, frames: [first, last], jitter: { before, after } }`);
|
|
433
434
|
return;
|
|
434
435
|
}
|
|
435
|
-
refuseUnknownKeys(p, 'POSE_CLEANED', hw, h, ['joint', 'frames', 'jitter']);
|
|
436
|
+
refuseUnknownKeys(p, 'POSE_CLEANED', hw, h, ['joint', 'frames', 'excursion', 'jitter']);
|
|
436
437
|
if (typeof h.joint !== 'string' || !isBody18(h.joint)) p.add('POSE_CLEANED', `${hw} "joint" is ${typeName(h.joint)}; it must be a body-18 joint: ${BODY18.join(', ')}`);
|
|
437
438
|
const f = h.frames;
|
|
438
|
-
|
|
439
|
+
const fOk = Array.isArray(f) && f.length === 2 && frameOk(f[0]) && frameOk(f[1]) && (f[0] as number) <= (f[1] as number);
|
|
440
|
+
if (!fOk) p.add('POSE_CLEANED', `${hw} "frames" is ${typeName(f)}; it must be [first, last], frame indices in ${frameRange}, first ≤ last`);
|
|
441
|
+
if ('excursion' in h) {
|
|
442
|
+
const ex = h.excursion;
|
|
443
|
+
const exOk = Array.isArray(ex) && ex.length === 2 && frameOk(ex[0]) && frameOk(ex[1]) && (ex[0] as number) <= (ex[1] as number) && (!fOk || ((f[0] as number) < (ex[0] as number) && (ex[1] as number) < (f[1] as number)));
|
|
444
|
+
if (!exOk) p.add('POSE_CLEANED', `${hw} "excursion" is ${typeName(ex)}; it must be [first, last], frame indices strictly inside "frames", first ≤ last`);
|
|
445
|
+
}
|
|
439
446
|
const j = h.jitter;
|
|
440
447
|
if (!isObject(j)) p.add('POSE_CLEANED', `${hw} "jitter" is ${typeName(j)}; it must be { before, after }`);
|
|
441
448
|
else {
|
|
@@ -642,7 +649,7 @@ function readHipsInferred(p: Problems, hw: string, h: unknown, frames: number |
|
|
|
642
649
|
if (undeclared.length > 0) p.add('POSE_CLEANED', `${hw}: frame(s) ${spans(undeclared)} are ${filledLabel} but not declined; only a frame the reading declined is filled, and "declined" says why`);
|
|
643
650
|
}
|
|
644
651
|
const holdParamsOf = (h: HoldParams | 'off'): HoldParams | 'off' => (h === 'off' ? 'off' : { speed: h.speed, reach: h.reach, minSeconds: h.minSeconds, blend: h.blend });
|
|
645
|
-
const copyHold = (h: CleanHold): CleanHold => ({ joint: h.joint, frames: [h.frames[0], h.frames[1]], jitter: { before: h.jitter.before, after: h.jitter.after } });
|
|
652
|
+
const copyHold = (h: CleanHold): CleanHold => ({ joint: h.joint, frames: [h.frames[0], h.frames[1]], ...(h.excursion !== undefined ? { excursion: [h.excursion[0], h.excursion[1]] as [number, number] } : {}), jitter: { before: h.jitter.before, after: h.jitter.after } });
|
|
646
653
|
const copyUnheld = (u: CleanUnheld): CleanUnheld => ({ joint: u.joint, frames: [u.frames[0], u.frames[1]], test: u.test, value: u.value });
|
|
647
654
|
|
|
648
655
|
function readJoints(p: Problems, pw: string, joints: unknown): Partial<Record<Body18, Joint>> {
|
|
@@ -836,7 +843,7 @@ export function writePoseSequence(seq: PoseSequence): string {
|
|
|
836
843
|
params: { loop: c.params.loop, legs: c.params.legs, rest: c.params.rest, ...(c.params.hipsInferred !== undefined ? { hipsInferred: copyHipsInferred(c.params.hipsInferred) } : {}), ...(c.params.hold !== undefined ? { hold: holdParamsOf(c.params.hold) } : {}) },
|
|
837
844
|
spikes: { found: c.spikes.found.map((s) => ({ frame: s.frame, joint: s.joint })), untested: [...c.spikes.untested] },
|
|
838
845
|
jitter: { before: jit(c.jitter.before), after: jit(c.jitter.after) },
|
|
839
|
-
...(c.holds !== undefined ? { holds: c.holds.map((h) => ({ joint: h.joint, frames: [h.frames[0], h.frames[1]], jitter: { before: jit(h.jitter.before), after: jit(h.jitter.after) } })) } : {}),
|
|
846
|
+
...(c.holds !== undefined ? { holds: c.holds.map((h) => ({ joint: h.joint, frames: [h.frames[0], h.frames[1]], ...(h.excursion !== undefined ? { excursion: [h.excursion[0], h.excursion[1]] } : {}), jitter: { before: jit(h.jitter.before), after: jit(h.jitter.after) } })) } : {}),
|
|
840
847
|
...(c.seams !== undefined ? { seams: c.seams.map((x) => ({ joint: x.joint, frame: x.frame, step: round(x.step, 4), gap: x.gap })) } : {}),
|
|
841
848
|
...(c.unheld !== undefined ? { unheld: c.unheld.map((u) => ({ joint: u.joint, frames: [u.frames[0], u.frames[1]], test: u.test, value: u.value === null ? null : round(u.value, 4) })) } : {}),
|
|
842
849
|
};
|