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