@rig-c/pose 0.17.0 → 0.19.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 +35 -16
- package/cli.ts +18 -12
- package/package.json +1 -1
- package/src/clean.ts +15 -4
- package/src/legs.ts +273 -68
- package/src/map.ts +49 -3
- package/src/pose.ts +86 -32
- package/src/reduce.ts +167 -7
- package/src/retarget.ts +118 -37
- package/src/run.ts +202 -27
- package/src/switch.ts +31 -8
- package/src/verify.ts +66 -16
package/src/legs.ts
CHANGED
|
@@ -24,11 +24,13 @@
|
|
|
24
24
|
* **Planted feet.** The hip is no longer the pose's mid_hip travel scaled. On
|
|
25
25
|
* every frame one foot supports: the one whose reference ankle is lower in the
|
|
26
26
|
* picture, changing only when the other ankle has been ≥ `PLANT_SWITCH` leg
|
|
27
|
-
* lengths lower for ≥ `PLANT_SWITCH_SECONDS
|
|
27
|
+
* lengths lower for ≥ `PLANT_SWITCH_SECONDS`, and taking support from the frame
|
|
28
|
+
* it crossed that margin once confirmed (rig-c/pose#102). The supporting foot is held where
|
|
28
29
|
* it was on the frame it took support, moved in y onto the one floor every span
|
|
29
30
|
* shares (rig-c/pose#85, `plantHip`), and the hip is placed so it stays there,
|
|
30
31
|
* through the posed (scaled) legs. A change of support blends the two
|
|
31
|
-
* derivations over `PLANT_BLEND` frames
|
|
32
|
+
* derivations in y over `PLANT_BLEND` frames while taking the new derivation's x
|
|
33
|
+
* immediately (rig-c/pose#102). A jump — both ankles and mid_hip more
|
|
32
34
|
* than `JUMP_RISE` leg lengths above their rolling floor (the 95th-percentile
|
|
33
35
|
* picture y over ± `FLOOR_SECONDS`), for ≥ `JUMP_FRAMES` frames — releases the
|
|
34
36
|
* plant: the hip follows the pose's travel from where it was, and re-plants on
|
|
@@ -39,6 +41,15 @@
|
|
|
39
41
|
* less than `STILL_SPEED` for `STILL_FRAMES` frames (rig-c/pose#47, round 2).
|
|
40
42
|
* The floor is rolling because an ankle's picture y drifts with depth: under
|
|
41
43
|
* one floor for the sequence, HQ's dancer standing still read as jumping.
|
|
44
|
+
*
|
|
45
|
+
* **In place** (rig-c/pose#92). A sequence declared `travel: "in-place"` is a
|
|
46
|
+
* gait that does not travel: its supporting foot moves back in the picture
|
|
47
|
+
* while the body stays, so holding it turns that slide into travel. Under the
|
|
48
|
+
* declaration each plant's anchor follows its own ankle's track (`follow`, the
|
|
49
|
+
* picture x scaled) from the frame it planted, its y still on the one floor; no
|
|
50
|
+
* step is read, since a foot that follows its track has no plant to drag; and
|
|
51
|
+
* the hip's x drift from the first frame to the last is spread back over the
|
|
52
|
+
* frames (`closeLoop`), so the loop carries no net travel.
|
|
42
53
|
*/
|
|
43
54
|
import type { Chain, JointMap, Rest } from './map.ts';
|
|
44
55
|
|
|
@@ -325,6 +336,28 @@ export interface Feet {
|
|
|
325
336
|
steps: Step[];
|
|
326
337
|
/** The frames a change of support (or a landing, or a re-plant) was keyed on: the blend starts there. */
|
|
327
338
|
changes: Array<{ frame: number; from: Side | null; to: Side }>;
|
|
339
|
+
/**
|
|
340
|
+
* rig-c/pose#113, read from the lower ankle: each run of frames whose
|
|
341
|
+
* supporting side differs from the reading before #113 (`was`, `now`; null:
|
|
342
|
+
* no foot supports). Absent where the rule does not apply (in place, or
|
|
343
|
+
* without `lowerAnkle`).
|
|
344
|
+
*/
|
|
345
|
+
lowered?: Lowered[];
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** A run of frames whose support the lower-ankle rule (rig-c/pose#113) reads differently from the reading before it. */
|
|
349
|
+
export interface Lowered {
|
|
350
|
+
frames: [number, number];
|
|
351
|
+
was: Side | null;
|
|
352
|
+
now: Side | null;
|
|
353
|
+
/**
|
|
354
|
+
* Why no foot supports, where `now` is null: `released` — the supporting foot
|
|
355
|
+
* was released on the run's first frame because the other ankle had been lower
|
|
356
|
+
* past the margin for `PLANT_SWITCH_SECONDS` and could not take support
|
|
357
|
+
* (released by a step, or moving); `higher` — the only foot free to take
|
|
358
|
+
* support was the higher one.
|
|
359
|
+
*/
|
|
360
|
+
cause?: 'released' | 'higher';
|
|
328
361
|
}
|
|
329
362
|
|
|
330
363
|
/** Each frame's rolling floor of a picture-y series: its `FLOOR_PERCENTILE` over the frames within ± `FLOOR_SECONDS`. */
|
|
@@ -339,9 +372,36 @@ export function rollingFloor(ys: readonly number[], times: readonly number[]): n
|
|
|
339
372
|
/**
|
|
340
373
|
* Which foot supports on each frame, from the reference ankles and hips (picture
|
|
341
374
|
* px, y down) — the header's rules. `times` are the key times; `secondsOf`
|
|
342
|
-
* times a run of frames as the rest of the retarget does.
|
|
375
|
+
* times a run of frames as the rest of the retarget does. `inPlace` (#92): no
|
|
376
|
+
* ankle is moving for the step rules, so nothing is released and every foot is
|
|
377
|
+
* free to take support.
|
|
378
|
+
*
|
|
379
|
+
* `lowerAnkle` (rig-c/pose#113, `--plant-feet` on a sequence not declared in
|
|
380
|
+
* place): a foot whose ankle is higher in the picture than the other's by
|
|
381
|
+
* `PLANT_SWITCH` leg lengths or more never takes support — not when the plant
|
|
382
|
+
* starts, not when a step releases the other foot. A supporting foot whose
|
|
383
|
+
* other ankle has been that much lower for `PLANT_SWITCH_SECONDS` hands support
|
|
384
|
+
* to it when it is free (as before) and is otherwise released (the hip follows
|
|
385
|
+
* the pose until a foot may take support). Steps and jumps are read as before.
|
|
386
|
+
* The frames whose support this changes are `lowered`.
|
|
343
387
|
*/
|
|
344
|
-
export function supportsOf(ankle: Record<Side, readonly Pt[]>, hip: Record<Side, readonly Pt[]>, times: readonly number[], secondsOf: (run: [number, number]) => number): Feet {
|
|
388
|
+
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
|
+
if (inPlace || !lowerAnkle) return readSupports(ankle, hip, times, secondsOf, inPlace, false).feet;
|
|
390
|
+
const { feet, released } = readSupports(ankle, hip, times, secondsOf, false, true);
|
|
391
|
+
const before = readSupports(ankle, hip, times, secondsOf, false, false).feet.support;
|
|
392
|
+
const lowered: Lowered[] = [];
|
|
393
|
+
feet.support.forEach((now, i) => {
|
|
394
|
+
const was = before[i] ?? null;
|
|
395
|
+
if (was === now) return;
|
|
396
|
+
const last = lowered[lowered.length - 1];
|
|
397
|
+
if (last !== undefined && last.frames[1] === i - 1 && last.was === was && last.now === now) last.frames[1] = i;
|
|
398
|
+
else lowered.push({ frames: [i, i], was, now, ...(now === null ? { cause: released.includes(i) ? ('released' as const) : ('higher' as const) } : {}) });
|
|
399
|
+
});
|
|
400
|
+
return { ...feet, lowered };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** The reader; `released` (#113): the frames a supporting foot was released because the lower foot could not take support. */
|
|
404
|
+
function readSupports(ankle: Record<Side, readonly Pt[]>, hip: Record<Side, readonly Pt[]>, times: readonly number[], secondsOf: (run: [number, number]) => number, inPlace: boolean, lowerAnkle: boolean): { feet: Feet; released: number[] } {
|
|
345
405
|
const n = ankle.r.length;
|
|
346
406
|
const legLength = {} as Record<Side, number>;
|
|
347
407
|
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);
|
|
@@ -364,7 +424,9 @@ export function supportsOf(ankle: Record<Side, readonly Pt[]>, hip: Record<Side,
|
|
|
364
424
|
// Ankle speeds, leg lengths a frame (0 on the first frame).
|
|
365
425
|
const speed = {} as Record<Side, number[]>;
|
|
366
426
|
for (const s of SIDES) speed[s] = ankle[s].map((a, i) => (i === 0 ? 0 : Math.hypot(a[0] - ankle[s][i - 1]![0], a[1] - ankle[s][i - 1]![1]) / legLength[s]));
|
|
367
|
-
|
|
427
|
+
// In place (rig-c/pose#92) the supporting foot follows its own track instead of being held, so a
|
|
428
|
+
// moving ankle has no plant to drag and nothing is released: no step, and every foot is free.
|
|
429
|
+
const moving = (s: Side, i: number) => !inPlace && speed[s][i]! > STEP_SPEED;
|
|
368
430
|
const still = (s: Side, i: number) => {
|
|
369
431
|
for (let k = i - STILL_FRAMES + 1; k <= i; k++) if (k >= 0 && !(speed[s][k]! < STILL_SPEED)) return false;
|
|
370
432
|
return true;
|
|
@@ -375,6 +437,8 @@ export function supportsOf(ankle: Record<Side, readonly Pt[]>, hip: Record<Side,
|
|
|
375
437
|
const open: Partial<Record<Side, Step>> = {};
|
|
376
438
|
let cur: Side | null = null;
|
|
377
439
|
let lowerFrom = -1;
|
|
440
|
+
let freeFrom = -1;
|
|
441
|
+
const released: number[] = [];
|
|
378
442
|
const lower = (cands: Side[], i: number): Side => (cands.length === 2 ? (ankle.l[i]![1] > ankle.r[i]![1] ? 'l' : 'r') : cands[0]!);
|
|
379
443
|
for (let i = 0; i < n; i++) {
|
|
380
444
|
// A released ankle re-plants once it has been still for STILL_FRAMES frames.
|
|
@@ -389,35 +453,57 @@ export function supportsOf(ankle: Record<Side, readonly Pt[]>, hip: Record<Side,
|
|
|
389
453
|
if (jump[i]) {
|
|
390
454
|
cur = null;
|
|
391
455
|
lowerFrom = -1;
|
|
456
|
+
freeFrom = -1;
|
|
392
457
|
continue;
|
|
393
458
|
}
|
|
394
459
|
const free = (s: Side) => open[s] === undefined && !moving(s, i);
|
|
460
|
+
// #113: a foot higher than the other by PLANT_SWITCH leg lengths or more does not take support.
|
|
461
|
+
const higher = (s: Side) => ankle[s === 'r' ? 'l' : 'r'][i]![1] - ankle[s][i]![1] >= PLANT_SWITCH * mean;
|
|
462
|
+
const may = (s: Side) => free(s) && !(lowerAnkle && higher(s));
|
|
395
463
|
if (cur !== null && moving(cur, i)) {
|
|
396
464
|
const other: Side = cur === 'r' ? 'l' : 'r';
|
|
397
|
-
const to: Side | null =
|
|
465
|
+
const to: Side | null = may(other) ? other : null;
|
|
398
466
|
const st: Step = { side: cur, frames: [i, n - 1], replant: null, to };
|
|
399
467
|
steps.push(st);
|
|
400
468
|
open[cur] = st;
|
|
401
469
|
if (to !== null) changes.push({ frame: i, from: cur, to });
|
|
402
470
|
cur = to;
|
|
403
471
|
lowerFrom = -1;
|
|
472
|
+
freeFrom = -1;
|
|
404
473
|
} else if (cur === null) {
|
|
405
|
-
const cands = SIDES.filter(
|
|
474
|
+
const cands = SIDES.filter(may);
|
|
406
475
|
if (cands.length > 0) {
|
|
407
476
|
cur = lower(cands, i);
|
|
408
477
|
changes.push({ frame: i, from: null, to: cur });
|
|
409
478
|
lowerFrom = -1;
|
|
479
|
+
freeFrom = -1;
|
|
410
480
|
}
|
|
411
481
|
} else {
|
|
412
482
|
const other: Side = cur === 'r' ? 'l' : 'r';
|
|
413
|
-
if (free(other)
|
|
483
|
+
if ((free(other) || lowerAnkle) && higher(cur)) {
|
|
414
484
|
if (lowerFrom < 0) lowerFrom = i;
|
|
415
|
-
if (
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
485
|
+
if (free(other)) {
|
|
486
|
+
if (freeFrom < 0) freeFrom = i;
|
|
487
|
+
// #102: once confirmed over PLANT_SWITCH_SECONDS, the incoming foot takes support from the landing frame it crossed the margin on.
|
|
488
|
+
if (secondsOf([freeFrom, i]) >= PLANT_SWITCH_SECONDS - 1e-9) {
|
|
489
|
+
for (let k = freeFrom; k < i; k++) support[k] = other;
|
|
490
|
+
changes.push({ frame: freeFrom, from: cur, to: other });
|
|
491
|
+
cur = other;
|
|
492
|
+
lowerFrom = -1;
|
|
493
|
+
freeFrom = -1;
|
|
494
|
+
}
|
|
495
|
+
} else {
|
|
496
|
+
freeFrom = -1;
|
|
497
|
+
if (secondsOf([lowerFrom, i]) >= PLANT_SWITCH_SECONDS - 1e-9) {
|
|
498
|
+
cur = null; // #113: the lower foot may not take support (released, or moving); neither foot supports
|
|
499
|
+
released.push(i);
|
|
500
|
+
lowerFrom = -1;
|
|
501
|
+
}
|
|
419
502
|
}
|
|
420
|
-
} else
|
|
503
|
+
} else {
|
|
504
|
+
lowerFrom = -1;
|
|
505
|
+
freeFrom = -1;
|
|
506
|
+
}
|
|
421
507
|
}
|
|
422
508
|
support[i] = cur;
|
|
423
509
|
}
|
|
@@ -429,7 +515,7 @@ export function supportsOf(ankle: Record<Side, readonly Pt[]>, hip: Record<Side,
|
|
|
429
515
|
if (last !== undefined && last.side === s && last.frames[1] === i - 1) last.frames[1] = i;
|
|
430
516
|
else supports.push({ side: s, frames: [i, i] });
|
|
431
517
|
}
|
|
432
|
-
return { legLength, support, supports, jumps, steps, changes };
|
|
518
|
+
return { feet: { legLength, support, supports, jumps, steps, changes }, released };
|
|
433
519
|
}
|
|
434
520
|
|
|
435
521
|
/** One support span's shift onto the floor (rig-c/pose#85): its side, its frames, and floor − its planted height, world units. */
|
|
@@ -459,23 +545,49 @@ export interface Floor {
|
|
|
459
545
|
* floor is `floor` when given (`--floor`, world units), and otherwise the median
|
|
460
546
|
* of the spans' planted heights; each span's anchor moves in y by floor − its
|
|
461
547
|
* planted height, and the hip with it, so everything above follows. A change of
|
|
462
|
-
* support still blends the two derivations over `PLANT_BLEND` frames, now
|
|
463
|
-
* between two spans on the floor
|
|
548
|
+
* support still blends the two derivations' y over `PLANT_BLEND` frames, now
|
|
549
|
+
* between two spans on the floor, while holding the planted foot's x from the
|
|
550
|
+
* frame it lands on (#102); a jump follows the pose's travel from the hip
|
|
464
551
|
* it left (on the floor) and lands on the floor. On a blend frame the hip is
|
|
465
552
|
* between the two derivations, so the supporting ankle is off its anchor:
|
|
466
553
|
* `blends` lists those frames with the anchor's y, and `holdBlend` stretches
|
|
467
554
|
* the supporting leg to keep it there. Null `floor` in the result: there is no
|
|
468
555
|
* support span.
|
|
556
|
+
*
|
|
557
|
+
* `follow(i, side)` (#92, an in-place sequence): the supporting ankle's track,
|
|
558
|
+
* world x. A plant's anchor x then moves by follow(i) − follow(the frame it
|
|
559
|
+
* planted), and the loop is closed (`closeLoop`, `closure` in the result; null
|
|
560
|
+
* without `follow`).
|
|
469
561
|
*/
|
|
470
|
-
export function plantHip(feet: Feet, offset: (i: number, s: Side) => Pt, travel: (i: number) => Pt, start: Pt, floor?: number): { hip: Pt[]; floor: Floor | null; blends: BlendFrame[] } {
|
|
471
|
-
const picture = placeHip(feet, offset, travel, start, null);
|
|
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
|
+
const picture = placeHip(feet, offset, travel, start, null, follow);
|
|
472
564
|
const heights = feet.supports.map((sp) => picture.anchors.get(sp.frames[0]));
|
|
473
|
-
if (heights.length === 0) return { hip: picture.hip, floor: null, blends: picture.blends };
|
|
565
|
+
if (heights.length === 0) return { hip: picture.hip, floor: null, blends: picture.blends, closure: null };
|
|
474
566
|
if (heights.some((h) => h === undefined)) throw new Error('plantHip: a support span starts on a frame no plant started on');
|
|
475
567
|
const y = floor ?? percentile(heights as number[], 0.5);
|
|
476
568
|
const shifts = feet.supports.map((sp, k) => ({ side: sp.side, frames: [sp.frames[0], sp.frames[1]] as [number, number], shift: y - heights[k]! }));
|
|
477
|
-
const placed = placeHip(feet, offset, travel, start, y);
|
|
478
|
-
|
|
569
|
+
const placed = placeHip(feet, offset, travel, start, y, follow);
|
|
570
|
+
const closure = follow === undefined ? null : closeLoop(placed.hip);
|
|
571
|
+
return { hip: placed.hip, floor: { y, given: floor !== undefined, shifts }, blends: placed.blends, closure };
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/** 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. */
|
|
575
|
+
export interface Closure {
|
|
576
|
+
drift: number;
|
|
577
|
+
frames: number;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Close an in-place loop (rig-c/pose#92): the hip's x on frame i moves by
|
|
582
|
+
* −drift · i / (n − 1), drift its x on the last frame less its x on the first,
|
|
583
|
+
* so the last frame's hip x is the first's and the loop carries no net travel.
|
|
584
|
+
* The y is the floor's (#85) and is not touched. In place.
|
|
585
|
+
*/
|
|
586
|
+
export function closeLoop(hip: Pt[]): Closure {
|
|
587
|
+
const n = hip.length;
|
|
588
|
+
const drift = n > 1 ? hip[n - 1]![0] - hip[0]![0] : 0;
|
|
589
|
+
if (n > 1) for (let i = 0; i < n; i++) hip[i]![0] -= (drift * i) / (n - 1);
|
|
590
|
+
return { drift, frames: n };
|
|
479
591
|
}
|
|
480
592
|
|
|
481
593
|
/** A frame inside the blend after a change of support (or a landing): the side now supporting, and the world y its plant holds the ankle at. */
|
|
@@ -490,17 +602,19 @@ export interface BlendFrame {
|
|
|
490
602
|
* picture puts it (`floorY` null) or at world y `floorY`; and each plant's
|
|
491
603
|
* picture anchor y, by the frame its support span starts on.
|
|
492
604
|
*/
|
|
493
|
-
function placeHip(feet: Feet, offset: (i: number, s: Side) => Pt, travel: (i: number) => Pt, start: Pt, floorY: number | null): { hip: Pt[]; anchors: Map<number, number>; blends: BlendFrame[] } {
|
|
605
|
+
function placeHip(feet: Feet, offset: (i: number, s: Side) => Pt, travel: (i: number) => Pt, start: Pt, floorY: number | null, follow?: (i: number, s: Side) => number): { hip: Pt[]; anchors: Map<number, number>; blends: BlendFrame[] } {
|
|
494
606
|
const n = feet.support.length;
|
|
495
607
|
const hip: Pt[] = [];
|
|
496
608
|
const anchors = new Map<number, number>();
|
|
497
609
|
const blends: BlendFrame[] = [];
|
|
498
610
|
/** How the hip is derived on a frame: held over a foot's anchor, or carried by the pose's travel from a frame's hip. */
|
|
499
|
-
type Mode = { kind: 'plant'; side: Side; anchor: Pt } | { kind: 'travel'; from: number; at: Pt };
|
|
611
|
+
type Mode = { kind: 'plant'; side: Side; anchor: Pt; from: number } | { kind: 'travel'; from: number; at: Pt };
|
|
500
612
|
const of = (mode: Mode, i: number): Pt => {
|
|
501
613
|
if (mode.kind === 'plant') {
|
|
502
614
|
const o = offset(i, mode.side);
|
|
503
|
-
|
|
615
|
+
// In place (#92) the anchor's x follows the supporting ankle's own track from the frame it planted.
|
|
616
|
+
const dx = follow === undefined ? 0 : follow(i, mode.side) - follow(mode.from, mode.side);
|
|
617
|
+
return [mode.anchor[0] + dx - o[0], mode.anchor[1] - o[1]];
|
|
504
618
|
}
|
|
505
619
|
const a = travel(i);
|
|
506
620
|
const b = travel(mode.from);
|
|
@@ -509,7 +623,7 @@ function placeHip(feet: Feet, offset: (i: number, s: Side) => Pt, travel: (i: nu
|
|
|
509
623
|
/** A plant from frame i: its anchor's x where the foot is, its y there or on the floor. */
|
|
510
624
|
const plant = (i: number, side: Side, at: Pt): Mode => {
|
|
511
625
|
anchors.set(i, at[1]);
|
|
512
|
-
return { kind: 'plant', side, anchor: [at[0], floorY ?? at[1]] };
|
|
626
|
+
return { kind: 'plant', side, anchor: [at[0], floorY ?? at[1]], from: i };
|
|
513
627
|
};
|
|
514
628
|
let mode: Mode | null = null;
|
|
515
629
|
let from: Mode | null = null;
|
|
@@ -538,7 +652,8 @@ function placeHip(feet: Feet, offset: (i: number, s: Side) => Pt, travel: (i: nu
|
|
|
538
652
|
if (from !== null && i - blendAt < PLANT_BLEND) {
|
|
539
653
|
const w = (i - blendAt + 1) / (PLANT_BLEND + 1);
|
|
540
654
|
const f = of(from, i);
|
|
541
|
-
|
|
655
|
+
// #102: blend y only; x takes the new derivation immediately so the planted foot does not slide across the blend.
|
|
656
|
+
hip.push([to[0], f[1] + (to[1] - f[1]) * w]);
|
|
542
657
|
const m = mode!;
|
|
543
658
|
if (m.kind === 'plant') blends.push({ frame: i, side: m.side, y: m.anchor[1] });
|
|
544
659
|
} else {
|
|
@@ -551,23 +666,72 @@ function placeHip(feet: Feet, offset: (i: number, s: Side) => Pt, travel: (i: nu
|
|
|
551
666
|
|
|
552
667
|
// --- stance (rig-c/pose#60) --------------------------------------------------
|
|
553
668
|
|
|
554
|
-
/**
|
|
669
|
+
/**
|
|
670
|
+
* A free leg is scaled within these factors to reach the floor (raised from below or lowered from above); past them the frame is reported, not forced.
|
|
671
|
+
* The default: a frame whose leg the picture folds further has its lower bound from the leg's own range (`stanceRangeOf`, #95).
|
|
672
|
+
*/
|
|
555
673
|
export const STANCE_BOUNDS: readonly [number, number] = [0.92, 1.08];
|
|
556
674
|
|
|
675
|
+
/** Per side, per frame: the lower bound of the leg's stretch (#95). */
|
|
676
|
+
export type StanceLower = Record<Side, readonly number[]>;
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* The leg's own range (rig-c/pose#95). Per side, r(i) = the leg's picture
|
|
680
|
+
* length hip → ankle on frame i ÷ its straight length (`STRAIGHT_PERCENTILE`
|
|
681
|
+
* of that length over the sequence, as `--leg-scale` reads a segment's), and
|
|
682
|
+
* the lower bound on frame i is min(`STANCE_BOUNDS[0]`, r(i)): the rig leg may
|
|
683
|
+
* fold as far as the picture's leg is folded on that frame, never less far
|
|
684
|
+
* than the default. A leg with no straight length keeps the default.
|
|
685
|
+
*/
|
|
686
|
+
export function stanceRangeOf(ankle: Record<Side, readonly Pt[]>, hip: Record<Side, readonly Pt[]>): StanceLower {
|
|
687
|
+
const out = {} as Record<Side, number[]>;
|
|
688
|
+
for (const s of SIDES) {
|
|
689
|
+
const len = ankle[s].map((a, i) => Math.hypot(a[0] - hip[s][i]![0], a[1] - hip[s][i]![1]));
|
|
690
|
+
const straight = percentile(len, STRAIGHT_PERCENTILE);
|
|
691
|
+
out[s] = len.map((l) => (straight > 1e-9 && Number.isFinite(l) ? Math.min(STANCE_BOUNDS[0], l / straight) : STANCE_BOUNDS[0]));
|
|
692
|
+
}
|
|
693
|
+
return out;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/** The lower bound on frame i for side s: the leg's own (#95), or the default. */
|
|
697
|
+
function lowerOf(lower: StanceLower | undefined, s: Side, i: number): number {
|
|
698
|
+
return lower?.[s][i] ?? STANCE_BOUNDS[0];
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/** The lowest lower bound per side over `frames`, where any is below the default (#95); null where none is. */
|
|
702
|
+
function rangeUsed(lower: StanceLower | undefined, frames: ReadonlyArray<{ side: Side; frame: number }>): Partial<Record<Side, number>> | null {
|
|
703
|
+
const out: Partial<Record<Side, number>> = {};
|
|
704
|
+
for (const { side, frame } of frames) {
|
|
705
|
+
const lo = lowerOf(lower, side, frame);
|
|
706
|
+
if (lo < STANCE_BOUNDS[0]) out[side] = Math.min(out[side] ?? Infinity, lo);
|
|
707
|
+
}
|
|
708
|
+
return Object.keys(out).length === 0 ? null : out;
|
|
709
|
+
}
|
|
710
|
+
|
|
557
711
|
/** A lifted free foot is planted again only when its height comes back under this fraction of the tolerance (#87): hysteresis, a stated factor, not tuned. */
|
|
558
712
|
export const STANCE_RELEASE = 0.8;
|
|
559
713
|
/** A run of one state shorter than this many seconds, between two runs of the other state, takes their state (#87): a foot cannot take a stance for one frame. */
|
|
560
714
|
export const STANCE_MIN_SECONDS = 0.1;
|
|
561
715
|
|
|
716
|
+
/**
|
|
717
|
+
* A free ankle's state on a frame (#60, #114): `both`, within the tolerance of
|
|
718
|
+
* the supporting one in either direction, so both feet are planted; `lifted`,
|
|
719
|
+
* past it above; `below`, past it below — a support error, not a stance, never
|
|
720
|
+
* planted. Past the tolerance either way the pose's height is kept.
|
|
721
|
+
*/
|
|
722
|
+
export type StanceState = 'both' | 'lifted' | 'below';
|
|
723
|
+
|
|
562
724
|
/** A run of frames with one free side in one state. */
|
|
563
725
|
export interface StanceRun {
|
|
564
|
-
/** `both`: the free ankle within the tolerance of the floor, so both feet are planted; `lifted`: above it, the pose's height kept. */
|
|
565
|
-
state:
|
|
726
|
+
/** `both`: the free ankle within the tolerance of the floor, so both feet are planted; `lifted`: past it above, `below`: past it below (#114), the pose's height kept. */
|
|
727
|
+
state: StanceState;
|
|
566
728
|
/** The side that is not supporting. */
|
|
567
729
|
side: Side;
|
|
568
730
|
frames: [number, number];
|
|
569
|
-
/** The free ankle's largest height above the supporting one in the picture, in that side's leg lengths. */
|
|
731
|
+
/** The free ankle's largest height above the supporting one in the picture, in that side's leg lengths (negative where it is below on every frame). */
|
|
570
732
|
maxHeight: number;
|
|
733
|
+
/** `below` (#114): the free ankle's largest distance below the supporting one in the picture, in that side's leg lengths. */
|
|
734
|
+
maxDepth?: number;
|
|
571
735
|
/** `both`: the smallest and largest stretch keyed (bounded); `lifted`: null. */
|
|
572
736
|
k: { min: number; max: number } | null;
|
|
573
737
|
/** #87: a run read in `state`, shorter than `STANCE_MIN_SECONDS` between two runs of the other state, which took their state; its frames are in their run too, and `k` is null. */
|
|
@@ -582,6 +746,8 @@ export interface StanceOver {
|
|
|
582
746
|
needed: number | null;
|
|
583
747
|
/** The free ankle's largest distance left from the floor after the bound, world units. */
|
|
584
748
|
left: number;
|
|
749
|
+
/** #95: the bound on the frame of `needed`, where the leg's own range set it below the default. */
|
|
750
|
+
bound?: [number, number];
|
|
585
751
|
}
|
|
586
752
|
|
|
587
753
|
export interface Stance {
|
|
@@ -592,6 +758,8 @@ export interface Stance {
|
|
|
592
758
|
/** The shortest run kept as its own state, seconds (#87). */
|
|
593
759
|
minSeconds: number;
|
|
594
760
|
bounds: readonly [number, number];
|
|
761
|
+
/** #95: per side, the lowest lower bound the leg's own range set on a planted-both frame, where any is below `bounds[0]`. */
|
|
762
|
+
range?: Partial<Record<Side, number>>;
|
|
595
763
|
runs: StanceRun[];
|
|
596
764
|
over: StanceOver[];
|
|
597
765
|
/** Per frame: the free side and the stretch keyed on it, or null (lifted, a jump, no support). */
|
|
@@ -608,26 +776,33 @@ export interface StancePosed {
|
|
|
608
776
|
* Both feet planted when the free ankle is near the floor (rig-c/pose#60). On a
|
|
609
777
|
* frame with a supporting side (so outside every jump), the free ankle's height
|
|
610
778
|
* above the supporting one is read in the picture (`ankle`, px, y down) in the
|
|
611
|
-
* free side's leg length — the unit of every FEET threshold.
|
|
612
|
-
*
|
|
779
|
+
* free side's leg length — the unit of every FEET threshold. The tolerance is a
|
|
780
|
+
* distance (#114): within `tolerance` of the supporting ankle, above it or below
|
|
781
|
+
* it (|h| ≤ tolerance), the frame is planted both: the free leg is to be stretched along its chain by
|
|
613
782
|
* k = (root y − floor y) / (root y − ankle y) on the posed rig (`posed`, world,
|
|
614
783
|
* y up), the floor being the supporting ankle's posed y; k is held within
|
|
615
784
|
* `STANCE_BOUNDS` (a free ankle below the floor shortens its leg, one above
|
|
616
|
-
* lengthens it),
|
|
617
|
-
* the
|
|
785
|
+
* lengthens it), its lower bound on the frame the leg's own (`lower`, #95)
|
|
786
|
+
* where the picture folds that leg further, and a frame past them is listed in
|
|
787
|
+
* `over`. Past the tolerance above, the foot is lifted and keeps the pose's
|
|
788
|
+
* height. Past it below (h < −tolerance), the free ankle is far below the
|
|
789
|
+
* supporting one: a support error, not a stance. That frame is never planted
|
|
790
|
+
* both, keeps the pose's height, and is read `below` (#114); before #114 the
|
|
791
|
+
* gap was signed, and a lifted foot read as supporting had the grounded leg
|
|
792
|
+
* shrunk up to it.
|
|
618
793
|
*
|
|
619
|
-
* The state is read with hysteresis (#87)
|
|
620
|
-
*
|
|
621
|
-
* `STANCE_RELEASE × tolerance`, so noise at the threshold does not flip it. A
|
|
794
|
+
* The state is read with hysteresis (#87) on |h|: a planted foot leaves the
|
|
795
|
+
* stance when |h| goes past `tolerance`, and one past it is planted again only
|
|
796
|
+
* under `STANCE_RELEASE × tolerance`, so noise at the threshold does not flip it. A
|
|
622
797
|
* run of one state shorter than `STANCE_MIN_SECONDS` (`secondsOf`, the
|
|
623
798
|
* retarget's timing of a run) between two runs of the other state, on the same
|
|
624
799
|
* free side and with no frame between, takes their state, and is listed with
|
|
625
800
|
* `absorbed`.
|
|
626
801
|
*/
|
|
627
|
-
export function stanceOf(feet: Feet, ankle: Record<Side, readonly Pt[]>, tolerance: number, posed: (i: number) => StancePosed, secondsOf: (run: [number, number]) => number): Stance {
|
|
802
|
+
export function stanceOf(feet: Feet, ankle: Record<Side, readonly Pt[]>, tolerance: number, posed: (i: number) => StancePosed, secondsOf: (run: [number, number]) => number, lower?: StanceLower): Stance {
|
|
628
803
|
const n = feet.support.length;
|
|
629
804
|
const stretch: Stance['stretch'] = new Array(n).fill(null);
|
|
630
|
-
type Read = { state:
|
|
805
|
+
type Read = { state: StanceState; side: Side; height: number };
|
|
631
806
|
const read: Array<Read | null> = new Array<Read | null>(n).fill(null);
|
|
632
807
|
for (let i = 0; i < n; i++) {
|
|
633
808
|
const s = feet.support[i] ?? null;
|
|
@@ -636,9 +811,12 @@ export function stanceOf(feet: Feet, ankle: Record<Side, readonly Pt[]>, toleran
|
|
|
636
811
|
read[i] = { state: 'both', side: f, height: (ankle[s][i]![1] - ankle[f][i]![1]) / feet.legLength[f] };
|
|
637
812
|
}
|
|
638
813
|
// Blocks of consecutive frames with one free side. Inside a block the state changes only when the
|
|
639
|
-
// other state's condition (
|
|
640
|
-
// holds for STANCE_MIN_SECONDS; a shorter crossing with the current state on
|
|
641
|
-
// absorbed. A crossing that runs to the block's end is taken: nothing of the
|
|
814
|
+
// other state's condition (out of the stance: |h| past the tolerance; planted again: |h| under
|
|
815
|
+
// STANCE_RELEASE of it) holds for STANCE_MIN_SECONDS; a shorter crossing with the current state on
|
|
816
|
+
// both sides of it is absorbed. A crossing that runs to the block's end is taken: nothing of the
|
|
817
|
+
// current state follows it. Out of the stance, a frame is `lifted` or `below` by the sign of h; a
|
|
818
|
+
// frame absorbed into it takes the state of the frame before it.
|
|
819
|
+
const away = (h: number): 'lifted' | 'below' => (h > 0 ? 'lifted' : 'below');
|
|
642
820
|
const absorbed: StanceRun[] = [];
|
|
643
821
|
for (let i = 0; i < n; ) {
|
|
644
822
|
const r0 = read[i];
|
|
@@ -648,34 +826,36 @@ export function stanceOf(feet: Feet, ankle: Record<Side, readonly Pt[]>, toleran
|
|
|
648
826
|
}
|
|
649
827
|
let end = i;
|
|
650
828
|
while (end + 1 < n && read[end + 1] !== null && read[end + 1]!.side === r0.side) end++;
|
|
651
|
-
let
|
|
829
|
+
let planted = Math.abs(r0.height) <= tolerance;
|
|
652
830
|
let j = i;
|
|
653
831
|
while (j <= end) {
|
|
654
|
-
const crosses = (k: number) => (
|
|
832
|
+
const crosses = (k: number) => (planted ? Math.abs(read[k]!.height) > tolerance : Math.abs(read[k]!.height) <= STANCE_RELEASE * tolerance);
|
|
655
833
|
if (!crosses(j)) {
|
|
656
|
-
read[j]!.state =
|
|
834
|
+
read[j]!.state = planted ? 'both' : away(read[j]!.height);
|
|
657
835
|
j++;
|
|
658
836
|
continue;
|
|
659
837
|
}
|
|
660
838
|
let k = j;
|
|
661
839
|
while (k + 1 <= end && crosses(k + 1)) k++;
|
|
662
|
-
const other = state === 'both' ? 'lifted' : 'both';
|
|
663
840
|
if (k < end && secondsOf([j, k]) < STANCE_MIN_SECONDS) {
|
|
664
841
|
let maxHeight = -Infinity;
|
|
842
|
+
let far = 0;
|
|
665
843
|
for (let x = j; x <= k; x++) {
|
|
666
|
-
read[x]!.state = state;
|
|
844
|
+
read[x]!.state = planted ? 'both' : read[x - 1]!.state;
|
|
667
845
|
maxHeight = Math.max(maxHeight, read[x]!.height);
|
|
846
|
+
if (Math.abs(read[x]!.height) > Math.abs(far)) far = read[x]!.height;
|
|
668
847
|
}
|
|
669
|
-
|
|
848
|
+
const other: StanceState = planted ? away(far) : 'both';
|
|
849
|
+
absorbed.push({ state: other, side: r0.side, frames: [j, k], maxHeight, ...(other === 'below' ? { maxDepth: -far } : {}), k: null, absorbed: true });
|
|
670
850
|
} else {
|
|
671
|
-
|
|
672
|
-
for (let x = j; x <= k; x++) read[x]!.state =
|
|
851
|
+
planted = !planted;
|
|
852
|
+
for (let x = j; x <= k; x++) read[x]!.state = planted ? 'both' : away(read[x]!.height);
|
|
673
853
|
}
|
|
674
854
|
j = k + 1;
|
|
675
855
|
}
|
|
676
856
|
i = end + 1;
|
|
677
857
|
}
|
|
678
|
-
type Row = { state:
|
|
858
|
+
type Row = { state: StanceState; side: Side; height: number; k: number | null; needed: number | null; over: boolean; left: number; lo: number };
|
|
679
859
|
const rows: Array<Row | null> = new Array<Row | null>(n).fill(null);
|
|
680
860
|
for (let i = 0; i < n; i++) {
|
|
681
861
|
const r = read[i];
|
|
@@ -683,8 +863,9 @@ export function stanceOf(feet: Feet, ankle: Record<Side, readonly Pt[]>, toleran
|
|
|
683
863
|
const s: Side = r.side === 'r' ? 'l' : 'r';
|
|
684
864
|
const f = r.side;
|
|
685
865
|
const height = r.height;
|
|
686
|
-
|
|
687
|
-
|
|
866
|
+
const lo = lowerOf(lower, f, i);
|
|
867
|
+
if (r.state !== 'both') {
|
|
868
|
+
rows[i] = { state: r.state, side: f, height, k: null, needed: null, over: false, left: 0, lo };
|
|
688
869
|
continue;
|
|
689
870
|
}
|
|
690
871
|
const p = posed(i);
|
|
@@ -693,12 +874,13 @@ export function stanceOf(feet: Feet, ankle: Record<Side, readonly Pt[]>, toleran
|
|
|
693
874
|
const a = p.ankle[f][1];
|
|
694
875
|
const ok = root - a > 1e-9 && root - floor > 1e-9;
|
|
695
876
|
const needed = ok ? (root - floor) / (root - a) : null;
|
|
696
|
-
const k = needed === null ? 1 : Math.min(Math.max(needed,
|
|
697
|
-
const over = needed === null || needed <
|
|
877
|
+
const k = needed === null ? 1 : Math.min(Math.max(needed, lo), STANCE_BOUNDS[1]);
|
|
878
|
+
const over = needed === null || needed < lo || needed > STANCE_BOUNDS[1];
|
|
698
879
|
const left = Math.abs(root - k * (root - a) - floor);
|
|
699
|
-
rows[i] = { state: 'both', side: f, height, k, needed, over, left };
|
|
880
|
+
rows[i] = { state: 'both', side: f, height, k, needed, over, left, lo };
|
|
700
881
|
if (k !== 1) stretch[i] = { side: f, k };
|
|
701
882
|
}
|
|
883
|
+
const boundOf = (lo: number): { bound?: [number, number] } => (lo < STANCE_BOUNDS[0] ? { bound: [lo, STANCE_BOUNDS[1]] } : {});
|
|
702
884
|
const runs: StanceRun[] = [];
|
|
703
885
|
const over: StanceOver[] = [];
|
|
704
886
|
for (let i = 0; i < n; i++) {
|
|
@@ -708,19 +890,26 @@ export function stanceOf(feet: Feet, ankle: Record<Side, readonly Pt[]>, toleran
|
|
|
708
890
|
if (last !== undefined && last.frames[1] === i - 1 && last.state === r.state && last.side === r.side) {
|
|
709
891
|
last.frames[1] = i;
|
|
710
892
|
last.maxHeight = Math.max(last.maxHeight, r.height);
|
|
893
|
+
if (last.maxDepth !== undefined) last.maxDepth = Math.max(last.maxDepth, -r.height);
|
|
711
894
|
if (last.k !== null && r.k !== null) last.k = { min: Math.min(last.k.min, r.k), max: Math.max(last.k.max, r.k) };
|
|
712
|
-
} else runs.push({ state: r.state, side: r.side, frames: [i, i], maxHeight: r.height, k: r.k === null ? null : { min: r.k, max: r.k } });
|
|
895
|
+
} else runs.push({ state: r.state, side: r.side, frames: [i, i], maxHeight: r.height, ...(r.state === 'below' ? { maxDepth: -r.height } : {}), k: r.k === null ? null : { min: r.k, max: r.k } });
|
|
713
896
|
if (!r.over) continue;
|
|
714
897
|
const o = over[over.length - 1];
|
|
715
898
|
if (o !== undefined && o.frames[1] === i - 1 && o.side === r.side) {
|
|
716
899
|
o.frames[1] = i;
|
|
717
|
-
|
|
900
|
+
const takes = o.needed !== null && (r.needed === null || Math.abs(r.needed - 1) > Math.abs(o.needed - 1));
|
|
901
|
+
o.needed = o.needed === null || r.needed === null ? null : takes ? r.needed : o.needed;
|
|
902
|
+
if (takes) {
|
|
903
|
+
delete o.bound;
|
|
904
|
+
Object.assign(o, boundOf(r.lo));
|
|
905
|
+
}
|
|
718
906
|
o.left = Math.max(o.left, r.left);
|
|
719
|
-
} else over.push({ side: r.side, frames: [i, i], needed: r.needed, left: r.left });
|
|
907
|
+
} else over.push({ side: r.side, frames: [i, i], needed: r.needed, left: r.left, ...boundOf(r.lo) });
|
|
720
908
|
}
|
|
721
909
|
for (const x of absorbed) runs.push(x);
|
|
722
910
|
runs.sort((a, b) => a.frames[0] - b.frames[0] || (a.absorbed === true ? 1 : 0) - (b.absorbed === true ? 1 : 0));
|
|
723
|
-
|
|
911
|
+
const range = rangeUsed(lower, rows.flatMap((r, i) => (r !== null && r.state === 'both' ? [{ side: r.side, frame: i }] : [])));
|
|
912
|
+
return { tolerance, release: STANCE_RELEASE, minSeconds: STANCE_MIN_SECONDS, bounds: STANCE_BOUNDS, ...(range === null ? {} : { range }), runs, over, stretch };
|
|
724
913
|
}
|
|
725
914
|
|
|
726
915
|
// --- the blend held on the floor (rig-c/pose#85) ------------------------------
|
|
@@ -734,10 +923,14 @@ export interface BlendRun {
|
|
|
734
923
|
frames: [number, number];
|
|
735
924
|
/** The smallest and largest stretch keyed (bounded). */
|
|
736
925
|
k: { min: number; max: number };
|
|
926
|
+
/** #95: the lowest lower bound on the run's frames, where the leg's own range set it below the default. */
|
|
927
|
+
lower?: number;
|
|
737
928
|
}
|
|
738
929
|
|
|
739
930
|
export interface BlendHold {
|
|
740
931
|
bounds: readonly [number, number];
|
|
932
|
+
/** #95: per side, the lowest lower bound the leg's own range set on a blend frame, where any is below `bounds[0]`. */
|
|
933
|
+
range?: Partial<Record<Side, number>>;
|
|
741
934
|
runs: BlendRun[];
|
|
742
935
|
/** Runs whose stretch needed a factor past the bounds: held at the bound, the remainder left to the blend. */
|
|
743
936
|
over: StanceOver[];
|
|
@@ -751,38 +944,50 @@ export interface BlendHold {
|
|
|
751
944
|
* supporting ankle is off the y its plant holds. Its leg is stretched along its
|
|
752
945
|
* chain about the chain's root, as `--stance` stretches a free leg (`stanceOf`):
|
|
753
946
|
* k = (root y − anchor y) / (root y − ankle y) on the posed rig (`posed`, world,
|
|
754
|
-
* y up), held within `STANCE_BOUNDS
|
|
947
|
+
* y up), held within `STANCE_BOUNDS` — its lower bound on the frame the leg's own
|
|
948
|
+
* (`lower`, #95) as `stanceOf`'s; a frame past them is listed in `over`, and
|
|
755
949
|
* the remainder stays with the blend. An ankle within `BLEND_HOLD_WITHIN` of the
|
|
756
950
|
* anchor is not stretched.
|
|
757
951
|
*/
|
|
758
|
-
export function holdBlend(blends: readonly BlendFrame[], n: number, posed: (i: number) => StancePosed): BlendHold {
|
|
952
|
+
export function holdBlend(blends: readonly BlendFrame[], n: number, posed: (i: number) => StancePosed, lower?: StanceLower): BlendHold {
|
|
759
953
|
const stretch: BlendHold['stretch'] = new Array(n).fill(null);
|
|
760
954
|
const runs: BlendRun[] = [];
|
|
761
955
|
const over: StanceOver[] = [];
|
|
956
|
+
const held: Array<{ side: Side; frame: number }> = [];
|
|
762
957
|
for (const b of blends) {
|
|
763
958
|
const p = posed(b.frame);
|
|
764
959
|
const root = p.root[b.side][1];
|
|
765
960
|
const a = p.ankle[b.side][1];
|
|
766
961
|
if (Math.abs(a - b.y) <= BLEND_HOLD_WITHIN) continue;
|
|
962
|
+
held.push({ side: b.side, frame: b.frame });
|
|
767
963
|
const ok = root - a > 1e-9 && root - b.y > 1e-9;
|
|
768
964
|
const needed = ok ? (root - b.y) / (root - a) : null;
|
|
769
|
-
const
|
|
965
|
+
const lo = lowerOf(lower, b.side, b.frame);
|
|
966
|
+
const k = needed === null ? 1 : Math.min(Math.max(needed, lo), STANCE_BOUNDS[1]);
|
|
770
967
|
const left = Math.abs(root - k * (root - a) - b.y);
|
|
968
|
+
const bound = lo < STANCE_BOUNDS[0] ? { bound: [lo, STANCE_BOUNDS[1]] as [number, number] } : {};
|
|
771
969
|
if (k !== 1) {
|
|
772
970
|
stretch[b.frame] = { side: b.side, k };
|
|
773
971
|
const last = runs[runs.length - 1];
|
|
774
972
|
if (last !== undefined && last.side === b.side && last.frames[1] === b.frame - 1) {
|
|
775
973
|
last.frames[1] = b.frame;
|
|
776
974
|
last.k = { min: Math.min(last.k.min, k), max: Math.max(last.k.max, k) };
|
|
777
|
-
|
|
975
|
+
if (lo < STANCE_BOUNDS[0]) last.lower = Math.min(last.lower ?? Infinity, lo);
|
|
976
|
+
} else runs.push({ side: b.side, frames: [b.frame, b.frame], k: { min: k, max: k }, ...(lo < STANCE_BOUNDS[0] ? { lower: lo } : {}) });
|
|
778
977
|
}
|
|
779
|
-
if (needed !== null && needed >=
|
|
978
|
+
if (needed !== null && needed >= lo && needed <= STANCE_BOUNDS[1]) continue;
|
|
780
979
|
const o = over[over.length - 1];
|
|
781
980
|
if (o !== undefined && o.side === b.side && o.frames[1] === b.frame - 1) {
|
|
782
981
|
o.frames[1] = b.frame;
|
|
783
|
-
|
|
982
|
+
const takes = o.needed !== null && (needed === null || Math.abs(needed - 1) > Math.abs(o.needed - 1));
|
|
983
|
+
o.needed = o.needed === null || needed === null ? null : takes ? needed : o.needed;
|
|
984
|
+
if (takes) {
|
|
985
|
+
delete o.bound;
|
|
986
|
+
Object.assign(o, bound);
|
|
987
|
+
}
|
|
784
988
|
o.left = Math.max(o.left, left);
|
|
785
|
-
} else over.push({ side: b.side, frames: [b.frame, b.frame], needed, left });
|
|
989
|
+
} else over.push({ side: b.side, frames: [b.frame, b.frame], needed, left, ...bound });
|
|
786
990
|
}
|
|
787
|
-
|
|
991
|
+
const range = rangeUsed(lower, held);
|
|
992
|
+
return { bounds: STANCE_BOUNDS, ...(range === null ? {} : { range }), runs, over, stretch };
|
|
788
993
|
}
|