@rig-c/pose 0.39.0 → 0.41.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 +27 -5
- package/cli.ts +31 -5
- package/package.json +1 -1
- package/src/draworder.ts +377 -0
- package/src/outdir.ts +5 -5
- package/src/pelvis.ts +205 -3
- package/src/reduce.ts +40 -13
- package/src/retarget.ts +82 -3
- package/src/run.ts +171 -17
- package/src/switch.ts +64 -2
package/src/reduce.ts
CHANGED
|
@@ -48,6 +48,8 @@ export interface ReduceBone {
|
|
|
48
48
|
dense: number[];
|
|
49
49
|
/** Frame indices that must keep a key regardless (the first and last always do). */
|
|
50
50
|
keep: ReadonlySet<number>;
|
|
51
|
+
/** Its own world-aim tolerance in degrees, in place of the call's (rig-c/pose#283: its chain's). */
|
|
52
|
+
tolerance?: number;
|
|
51
53
|
}
|
|
52
54
|
export interface ReduceHip {
|
|
53
55
|
bone: string;
|
|
@@ -78,6 +80,12 @@ export interface Reduced {
|
|
|
78
80
|
rows: ReduceRow[];
|
|
79
81
|
/** Each bone's world-aim error on every frame (reduced less dense, degrees), as the poser evaluates the reduced keys. */
|
|
80
82
|
errors: Map<string, number[]>;
|
|
83
|
+
/**
|
|
84
|
+
* rig-c/pose#320: each trunk root whose smoothing (`smoothTrunk`, #116) was not kept because it left a bone it
|
|
85
|
+
* manages past that bone's own tolerance — the bone, its smoothed worst error and its tolerance (degrees); the
|
|
86
|
+
* fitted keys stand. Absent when every smoothing was kept.
|
|
87
|
+
*/
|
|
88
|
+
unsmoothed?: Array<{ root: string; bone: string; worst: number; tolerance: number }>;
|
|
81
89
|
}
|
|
82
90
|
|
|
83
91
|
// ---------------------------------------------------------------------------
|
|
@@ -328,6 +336,7 @@ export function reduceKeys(times: readonly number[], bones: readonly ReduceBone[
|
|
|
328
336
|
const byName = new Map<string, ReduceBone>();
|
|
329
337
|
for (const b of bones) {
|
|
330
338
|
if (byName.has(b.bone)) throw new Error(`reduce: bone "${b.bone}" is given twice`);
|
|
339
|
+
if (b.tolerance !== undefined && !(Number.isFinite(b.tolerance) && b.tolerance > 0)) throw new Error(`reduce: bone "${b.bone}"'s tolerance is ${b.tolerance}°, not a positive number`);
|
|
331
340
|
if (b.dense.length !== n) throw new Error(`reduce: bone "${b.bone}" has ${b.dense.length} dense key(s) for ${n} frame(s)`);
|
|
332
341
|
byName.set(b.bone, b);
|
|
333
342
|
}
|
|
@@ -349,7 +358,7 @@ export function reduceKeys(times: readonly number[], bones: readonly ReduceBone[
|
|
|
349
358
|
const keys = fitTrack(times, samples, {
|
|
350
359
|
targets,
|
|
351
360
|
keep: keptFrames(b.keep, n, `bone "${b.bone}"`),
|
|
352
|
-
tolerance,
|
|
361
|
+
tolerance: b.tolerance ?? tolerance,
|
|
353
362
|
error: (i, v) => Math.abs(v[0]! - targets[i]![0]!),
|
|
354
363
|
});
|
|
355
364
|
const e = trackAt(keys, samples, 1).map((v, i) => v[0]! - targets[i]![0]!);
|
|
@@ -604,7 +613,9 @@ function solveLimbPreservingJitter(
|
|
|
604
613
|
* above the sequence's own. Optimizing knot values and handle y-coordinates
|
|
605
614
|
* jointly within the ±tolerance world-aim tube removes those C0 breaks while
|
|
606
615
|
* keeping any limb chains hanging from the trunk within 0.04 of their landing
|
|
607
|
-
* and jitter.
|
|
616
|
+
* and jitter. The tube's bounds are a penalty, not a constraint: a root whose
|
|
617
|
+
* smoothing leaves any bone it manages past that bone's own tolerance keeps its
|
|
618
|
+
* fitted keys instead, recorded in `out.unsmoothed` (rig-c/pose#320).
|
|
608
619
|
*/
|
|
609
620
|
export function smoothTrunk(
|
|
610
621
|
times: readonly number[],
|
|
@@ -617,11 +628,12 @@ export function smoothTrunk(
|
|
|
617
628
|
if (n < 4 || trunkBones.size === 0) return;
|
|
618
629
|
const samples = samplesOf(times);
|
|
619
630
|
const byName = new Map(bones.map((b) => [b.bone, b]));
|
|
631
|
+
/** rig-c/pose#283: a bone's own tolerance (its chain's under a per-chain --reduce), else the call's. */
|
|
632
|
+
const tolOf = (b: string): number => byName.get(b)?.tolerance ?? tolerance;
|
|
620
633
|
const roots = [...trunkBones].filter((b) => {
|
|
621
634
|
const info = byName.get(b);
|
|
622
635
|
return info !== undefined && (info.above === null || !trunkBones.has(info.above));
|
|
623
636
|
});
|
|
624
|
-
const tolInner = 0.985 * tolerance;
|
|
625
637
|
|
|
626
638
|
for (const root of roots) {
|
|
627
639
|
const rootKeys = out.rotate.get(root);
|
|
@@ -646,8 +658,10 @@ export function smoothTrunk(
|
|
|
646
658
|
const targetRoot = rootInfo.dense.map((d, i) => d - eAboveRoot[i]!);
|
|
647
659
|
const yRootOld = yOldOf.get(root)!;
|
|
648
660
|
|
|
649
|
-
|
|
650
|
-
const
|
|
661
|
+
// #283: each bone's own tolerance (its chain's under a per-chain --reduce; the call's otherwise).
|
|
662
|
+
const rootInner = 0.985 * tolOf(root);
|
|
663
|
+
const rootLb = targetRoot.map((t) => t - rootInner);
|
|
664
|
+
const rootUb = targetRoot.map((t) => t + rootInner);
|
|
651
665
|
let smRoot = optimizeBasis(basisOf.get(root)!, rootLb, rootUb);
|
|
652
666
|
let eRoot = smRoot.y.map((v, i) => v - targetRoot[i]!);
|
|
653
667
|
|
|
@@ -667,14 +681,15 @@ export function smoothTrunk(
|
|
|
667
681
|
for (const h of headBones) {
|
|
668
682
|
const dH = byName.get(h)!.dense;
|
|
669
683
|
const targetH = dH.map((d, i) => d - eRoot[i]!);
|
|
670
|
-
const
|
|
684
|
+
const hInner = 0.985 * tolOf(h);
|
|
685
|
+
const smH = optimizeBasis(basisOf.get(h)!, targetH.map((t) => t - hInner), targetH.map((t) => t + hInner));
|
|
671
686
|
smSub.set(h, smH);
|
|
672
687
|
eSub.set(h, smH.y.map((v, i) => v - targetH[i]!));
|
|
673
688
|
}
|
|
674
689
|
for (const b0 of arm0Bones) {
|
|
675
690
|
const yOld = yOldOf.get(b0)!;
|
|
676
691
|
const wOld = Math.max(...out.errors.get(b0)!.map(Math.abs));
|
|
677
|
-
const cap = Math.min(0.98 *
|
|
692
|
+
const cap = Math.min(0.98 * tolOf(b0), wOld + 0.035 * tolOf(b0));
|
|
678
693
|
const targetB = byName.get(b0)!.dense.map((d, i) => d - eRoot[i]!);
|
|
679
694
|
const smB = solveLimbPreservingJitter(basisOf.get(b0)!, yOld, targetB.map((t) => t - cap), targetB.map((t) => t + cap));
|
|
680
695
|
smSub.set(b0, smB);
|
|
@@ -685,7 +700,7 @@ export function smoothTrunk(
|
|
|
685
700
|
const eUp = eSub.get(up)!;
|
|
686
701
|
const yOld = yOldOf.get(b1)!;
|
|
687
702
|
const wOld = Math.max(...out.errors.get(b1)!.map(Math.abs));
|
|
688
|
-
const cap = Math.min(0.98 *
|
|
703
|
+
const cap = Math.min(0.98 * tolOf(b1), wOld + 0.035 * tolOf(b1));
|
|
689
704
|
const targetB = byName.get(b1)!.dense.map((d, i) => d - eUp[i]!);
|
|
690
705
|
const smB = solveLimbPreservingJitter(basisOf.get(b1)!, yOld, targetB.map((t) => t - cap), targetB.map((t) => t + cap));
|
|
691
706
|
smSub.set(b1, smB);
|
|
@@ -699,17 +714,17 @@ export function smoothTrunk(
|
|
|
699
714
|
for (let i = 0; i < n; i++) {
|
|
700
715
|
for (const h of headBones) {
|
|
701
716
|
const eh = eSub.get(h)![i]!;
|
|
702
|
-
if (Math.abs(eh) > 0.99 *
|
|
703
|
-
tightenAround(i, Math.abs(eh) - 0.975 *
|
|
717
|
+
if (Math.abs(eh) > 0.99 * tolOf(h)) {
|
|
718
|
+
tightenAround(i, Math.abs(eh) - 0.975 * tolOf(h));
|
|
704
719
|
anyTightened = true;
|
|
705
720
|
}
|
|
706
721
|
}
|
|
707
722
|
for (const b of [...arm0Bones, ...arm1Bones]) {
|
|
708
723
|
const wOld = Math.max(...out.errors.get(b)!.map(Math.abs));
|
|
709
|
-
const cap = Math.min(0.985 *
|
|
724
|
+
const cap = Math.min(0.985 * tolOf(b), wOld + 0.04 * tolOf(b));
|
|
710
725
|
const eb = eSub.get(b)![i]!;
|
|
711
726
|
if (Math.abs(eb) > cap) {
|
|
712
|
-
tightenAround(i, Math.abs(eb) - (cap - 0.01 *
|
|
727
|
+
tightenAround(i, Math.abs(eb) - (cap - 0.01 * tolOf(b)));
|
|
713
728
|
anyTightened = true;
|
|
714
729
|
}
|
|
715
730
|
}
|
|
@@ -730,6 +745,16 @@ export function smoothTrunk(
|
|
|
730
745
|
}
|
|
731
746
|
};
|
|
732
747
|
|
|
748
|
+
// rig-c/pose#320: the smoothing is kept only within every managed bone's own tolerance; past it, the fitted keys
|
|
749
|
+
// (within it by construction) stand, and the run says so.
|
|
750
|
+
const past = allManaged.map((b) => {
|
|
751
|
+
const e = b === root ? eRoot : eSub.get(b)!;
|
|
752
|
+
return { b, worst: Math.max(...e.map(Math.abs)) };
|
|
753
|
+
}).find((x) => x.worst > tolOf(x.b));
|
|
754
|
+
if (past !== undefined) {
|
|
755
|
+
(out.unsmoothed ??= []).push({ root, bone: past.b, worst: past.worst, tolerance: tolOf(past.b) });
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
733
758
|
updateBone(root, smRoot.keys, eRoot);
|
|
734
759
|
for (const b of [...headBones, ...arm0Bones, ...arm1Bones]) {
|
|
735
760
|
updateBone(b, smSub.get(b)!.keys, eSub.get(b)!);
|
|
@@ -827,6 +852,8 @@ export interface UnderBone {
|
|
|
827
852
|
/** The dense rotate keys (unwrapped, as written). */
|
|
828
853
|
dense: readonly number[];
|
|
829
854
|
keep: ReadonlySet<number>;
|
|
855
|
+
/** Its own world-aim tolerance in degrees, in place of the call's (rig-c/pose#283). */
|
|
856
|
+
tolerance?: number;
|
|
830
857
|
}
|
|
831
858
|
|
|
832
859
|
/**
|
|
@@ -879,7 +906,7 @@ export function reduceUnderScale(times: readonly number[], bones: readonly Under
|
|
|
879
906
|
return Math.abs(wrap180(Math.atan2(w[1], w[0]) * DEG - b.target[i]!));
|
|
880
907
|
};
|
|
881
908
|
const keep = keptFrames(b.keep, n, `bone "${b.bone}"`);
|
|
882
|
-
const rk = fitTrack(times, samples, { targets: solved.map((k) => [k]), keep, tolerance, error: (i, v) => aimError(i, v[0]!) });
|
|
909
|
+
const rk = fitTrack(times, samples, { targets: solved.map((k) => [k]), keep, tolerance: b.tolerance ?? tolerance, error: (i, v) => aimError(i, v[0]!) });
|
|
883
910
|
const kAt = trackAt(rk, samples, 1).map((v) => v[0]!);
|
|
884
911
|
const ae = worstOf(kAt.map((_, i) => i), (i) => aimError(i, kAt[i]!))!;
|
|
885
912
|
rows.push({ bone: b.bone, kind: 'angle', before: n, after: rk.length, worst: ae.worst, frame: ae.frame });
|
package/src/retarget.ts
CHANGED
|
@@ -79,7 +79,7 @@ import { aimAngle, aimEnds, bodyYawSets, descends, faceYawSets, knownSetsOf, par
|
|
|
79
79
|
import type { Joint, Travel } from './pose.ts';
|
|
80
80
|
import { TRAVEL_IN_PLACE } from './pose.ts';
|
|
81
81
|
import { Problems, Refusal } from './problems.ts';
|
|
82
|
-
import type { ArtRange, AttachmentKey, ByHip, Raised, ReachShort, SlotTrack, Switch, SwitchOptions } from './switch.ts';
|
|
82
|
+
import type { ArtRange, AttachmentKey, ByHip, Raised, ReachShort, SetRuns, SlotTrack, Switch, SwitchOptions } from './switch.ts';
|
|
83
83
|
import { clampArt, coveredChains, isRaised, measureArt, overflow, phaseGroupProblems, RAISED_WRIST_EYES, RANGE_SLACK, reachLimit, runsOf, selectSwitches, twoBone } from './switch.ts';
|
|
84
84
|
import type { BlendHold, Closure, Feet, Floor, LegScale, LegSegmentRatio, PathPose, Posing, Side, Stance } from './legs.ts';
|
|
85
85
|
import { figureScaleOf, freeAnkleShiftOf, holdBlend, isLegSegment, legScaleOf, legsOf, pathFrom, percentile, plantHip, posePath, rootOf, scaleSupport, shiftOver, SIDES, stanceOf, stanceRangeOf, STRAIGHT_PERCENTILE, supportsOf, tipOf, unshortenStanceShin } from './legs.ts';
|
|
@@ -369,6 +369,34 @@ export interface ReachReading {
|
|
|
369
369
|
ratio: number | null;
|
|
370
370
|
}
|
|
371
371
|
|
|
372
|
+
/**
|
|
373
|
+
* rig-c/pose#282: where a `reach` chain's bone starts against the source joint
|
|
374
|
+
* it re-aims from. The pose's pair starts at the translated joint, which the
|
|
375
|
+
* rig puts at the translated bone's posed origin; the reach aims from its own
|
|
376
|
+
* bone's posed origin. The distance between the two, at setup and on the
|
|
377
|
+
* frames, is the lever #236 names — a reading, with a WARN past
|
|
378
|
+
* `REACH_ORIGIN_WARN` of the segment. One per reach chain, in map order.
|
|
379
|
+
*/
|
|
380
|
+
export interface ReachOrigin {
|
|
381
|
+
chain: string;
|
|
382
|
+
bone: string;
|
|
383
|
+
/** The pair: the chain's first joint (the translated joint) and its second. */
|
|
384
|
+
joints: [string, string];
|
|
385
|
+
/** The translated bone, whose posed origin is where the rig puts the source joint. */
|
|
386
|
+
anchor: string;
|
|
387
|
+
/** The distance at setup, world units. */
|
|
388
|
+
setup: number;
|
|
389
|
+
/** The largest distance as posed on the frames, world units, and its frame. */
|
|
390
|
+
posed: { max: number; frame: number };
|
|
391
|
+
/** The reached length: the pair's median picture length, scaled (world units). */
|
|
392
|
+
segment: number;
|
|
393
|
+
/** max(setup, posed.max) ÷ segment. */
|
|
394
|
+
ofSegment: number;
|
|
395
|
+
/** ofSegment > threshold. */
|
|
396
|
+
warn: boolean;
|
|
397
|
+
threshold: number;
|
|
398
|
+
}
|
|
399
|
+
|
|
372
400
|
/**
|
|
373
401
|
* Mean |second difference| of a series of angles, degrees: |wrap(v[i+1] − v[i])
|
|
374
402
|
* − wrap(v[i] − v[i−1])| over every i whose three frames `ok` admits (every
|
|
@@ -419,6 +447,8 @@ export interface Retarget {
|
|
|
419
447
|
scaleFit: ScaleFit | null;
|
|
420
448
|
/** The spans each alternate set shows, with their art turn. */
|
|
421
449
|
switches: Switch[];
|
|
450
|
+
/** rig-c/pose#279: per set without a `phase`, the runs it was nearest on and the runs the minimum span suppressed. */
|
|
451
|
+
switchRuns: SetRuns[];
|
|
422
452
|
/** Per switched or hidden slot, what it shows on each frame (null: nothing). */
|
|
423
453
|
shown: Map<string, Array<string | null>>;
|
|
424
454
|
/** Per set, its distance to the pose on each frame, degrees. */
|
|
@@ -475,6 +505,8 @@ export interface Retarget {
|
|
|
475
505
|
nodSkipped: NodSkipped | null;
|
|
476
506
|
/** Each `reach` chain hanging below a bone the animation turns or translates (#236): its aim's departure from the pose pair's angle. */
|
|
477
507
|
reach: ReachReading[];
|
|
508
|
+
/** rig-c/pose#282: each `reach` chain's origin against the source joint it re-aims from; empty when the map has none. */
|
|
509
|
+
reachOrigin: ReachOrigin[];
|
|
478
510
|
/** rig-c/pose#288: the pelvis bone's source; null when the map has no pelvis chain and the sequence no track. */
|
|
479
511
|
pelvis: PelvisReading | null;
|
|
480
512
|
}
|
|
@@ -1486,6 +1518,8 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1486
1518
|
const reaches = map.chains.filter((c) => c.reach).sort((x, y) => depth(x.bones[0]!) - depth(y.bones[0]!));
|
|
1487
1519
|
let aimReach: ((c: Chain) => void) | null = null;
|
|
1488
1520
|
const reachAims = new Map<string, { aim: number[]; pair: number[]; length: number }>();
|
|
1521
|
+
/** #282: per reach chain, the distance from its bone's posed origin to the anchor's on each frame (the last aim's). */
|
|
1522
|
+
const reachGaps = new Map<string, number[]>();
|
|
1489
1523
|
if (reaches.length > 0 && t !== null && scale !== null) {
|
|
1490
1524
|
const s = scale.factor;
|
|
1491
1525
|
const tb = rest.get(t.bone)!;
|
|
@@ -1494,8 +1528,10 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1494
1528
|
const anchor = series.get(c.joints[0]!)!;
|
|
1495
1529
|
const end = series.get(c.joints[1]!)!;
|
|
1496
1530
|
const angles: number[] = [];
|
|
1531
|
+
const gaps: number[] = [];
|
|
1497
1532
|
for (let i = 0; i < n; i++) {
|
|
1498
1533
|
const o = posedOrigin(b, i);
|
|
1534
|
+
gaps.push(Math.hypot(o[0] - (tb.worldX + travel[i]![0]), o[1] - (tb.worldY + travel[i]![1])));
|
|
1499
1535
|
const tx = tb.worldX + travel[i]![0] + (end[i]![0] - anchor[i]![0]) * s;
|
|
1500
1536
|
const ty = tb.worldY + travel[i]![1] - (end[i]![1] - anchor[i]![1]) * s;
|
|
1501
1537
|
const d = Math.hypot(tx - o[0], ty - o[1]);
|
|
@@ -1503,6 +1539,7 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1503
1539
|
}
|
|
1504
1540
|
fillLeading(angles, () => p.add('POSE_SEGMENT_ZERO', `chain "${c.name}" — bone "${b}" reaches for ${c.joints[1]}, which the pose puts on the bone's posed origin on every frame; it has no direction`));
|
|
1505
1541
|
targets.set(b, angles);
|
|
1542
|
+
reachGaps.set(c.name, gaps);
|
|
1506
1543
|
// #236: the aim as reached, before its own range clamp, beside the pair's own angle (a reading).
|
|
1507
1544
|
reachAims.set(c.name, { aim: [...angles], pair: Array.from({ length: n }, (_, i) => pictureAngle(anchor[i]!, end[i]!)), length: median(Array.from({ length: n }, (_, i) => Math.hypot(end[i]![0] - anchor[i]![0], end[i]![1] - anchor[i]![1]) * s)) });
|
|
1508
1545
|
};
|
|
@@ -3087,6 +3124,7 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
3087
3124
|
if (headNodTrack !== null) tracks.push(headNodTrack);
|
|
3088
3125
|
tracks.push(...sw.offsetTracks, ...sw.slotTracks);
|
|
3089
3126
|
const reach = reachReadings(rest, map, tracks, reachAims, t?.bone ?? null);
|
|
3127
|
+
const reachOrigin = reachOrigins(rest, map, reachAims, reachGaps, t?.bone ?? null);
|
|
3090
3128
|
|
|
3091
3129
|
const pivot = pivotOf(rest, t?.bone ?? null, legs);
|
|
3092
3130
|
const pelvis = pelvisReadingOf(pelvisChain, opts.pelvis, track, fromTrack, tracks);
|
|
@@ -3099,8 +3137,8 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
3099
3137
|
return {
|
|
3100
3138
|
animation: { duration: keyTimes[n - 1]!, tracks },
|
|
3101
3139
|
times: keyTimes, targets, origin, holds, bends, short, endOn, flips, scale, scaleFit,
|
|
3102
|
-
switches: sw.switches, shown: sw.shown, distances: sw.distances, turns, turnsUnread, range, artRange,
|
|
3103
|
-
legScale, segments, legStraight, rigLeg, scales, feet, planted: plantOn, stance, floor, blendHold, closure, openDrift, ankles, pivot, raised, byHip, face, body, figureScale, headNod: headNodReading, nodSkipped, reach, pelvis,
|
|
3140
|
+
switches: sw.switches, switchRuns: sw.runs, shown: sw.shown, distances: sw.distances, turns, turnsUnread, range, artRange,
|
|
3141
|
+
legScale, segments, legStraight, rigLeg, scales, feet, planted: plantOn, stance, floor, blendHold, closure, openDrift, ankles, pivot, raised, byHip, face, body, figureScale, headNod: headNodReading, nodSkipped, reach, reachOrigin, pelvis,
|
|
3104
3142
|
};
|
|
3105
3143
|
}
|
|
3106
3144
|
|
|
@@ -3209,6 +3247,47 @@ function reachReadings(rest: Rest, map: JointMap, tracks: ReadonlyArray<MotionTr
|
|
|
3209
3247
|
return out;
|
|
3210
3248
|
}
|
|
3211
3249
|
|
|
3250
|
+
/**
|
|
3251
|
+
* #282: a reach bone whose origin sits more than this fraction of the segment
|
|
3252
|
+
* from the source joint is warned. The aim from an origin a fraction f up the
|
|
3253
|
+
* pair turns about 1 ÷ (1 − f) times the pair's turn (#236's lever: the example
|
|
3254
|
+
* wave's trunk, f = 0.5, measured 2.00×); 0.2 is 1.25×, a quarter more turn
|
|
3255
|
+
* than the pose has.
|
|
3256
|
+
*/
|
|
3257
|
+
export const REACH_ORIGIN_WARN = 0.2;
|
|
3258
|
+
|
|
3259
|
+
/** #282: each reach chain's origin against the anchor's, at setup and as posed, in map order. */
|
|
3260
|
+
function reachOrigins(rest: Rest, map: JointMap, aims: ReadonlyMap<string, { length: number }>, gaps: ReadonlyMap<string, number[]>, anchor: string | null): ReachOrigin[] {
|
|
3261
|
+
const out: ReachOrigin[] = [];
|
|
3262
|
+
if (anchor === null) return out;
|
|
3263
|
+
const a0 = rest.get(anchor)!;
|
|
3264
|
+
for (const c of map.chains) {
|
|
3265
|
+
const got = aims.get(c.name);
|
|
3266
|
+
const g = gaps.get(c.name);
|
|
3267
|
+
if (!c.reach || got === undefined || g === undefined) continue;
|
|
3268
|
+
const b = c.bones[0]!;
|
|
3269
|
+
const bo = rest.get(b)!;
|
|
3270
|
+
const setup = r3(Math.hypot(bo.worldX - a0.worldX, bo.worldY - a0.worldY));
|
|
3271
|
+
let frame = 0;
|
|
3272
|
+
for (let i = 1; i < g.length; i++) if (g[i]! > g[frame]!) frame = i;
|
|
3273
|
+
const max = r3(g[frame] ?? 0);
|
|
3274
|
+
const segment = r3(got.length);
|
|
3275
|
+
const ofSegment = got.length > 0 ? r3(Math.max(setup, max) / got.length) : 0;
|
|
3276
|
+
out.push({ chain: c.name, bone: b, joints: [c.joints[0]!, c.joints[1]!], anchor, setup, posed: { max, frame }, segment, ofSegment, warn: ofSegment > REACH_ORIGIN_WARN, threshold: REACH_ORIGIN_WARN });
|
|
3277
|
+
}
|
|
3278
|
+
return out;
|
|
3279
|
+
}
|
|
3280
|
+
|
|
3281
|
+
/** #282: the `REACH-ORIGIN` line, one per reach chain. */
|
|
3282
|
+
export function reachOriginLine(e: ReachOrigin, times: readonly number[]): string {
|
|
3283
|
+
return `chain "${e.chain}" bone "${e.bone}" (${e.joints[0]} → ${e.joints[1]}): its origin ${e.setup.toFixed(2)} units from the source joint ${e.joints[0]} (bone "${e.anchor}"'s origin) at setup, up to ${e.posed.max.toFixed(2)} on the frames (frame ${e.posed.frame}, t=${times[e.posed.frame]!.toFixed(3)}s): ${e.ofSegment.toFixed(3)} of the ${e.segment.toFixed(2)}-unit segment`;
|
|
3284
|
+
}
|
|
3285
|
+
|
|
3286
|
+
/** #282: the WARN of a reach chain whose origin sits past REACH_ORIGIN_WARN of the segment. */
|
|
3287
|
+
export function reachOriginWarn(e: ReachOrigin): string {
|
|
3288
|
+
return `REACH_ORIGIN: chain "${e.chain}" bone "${e.bone}" starts ${e.ofSegment.toFixed(3)} of the segment from ${e.joints[0]}, past ${e.threshold.toFixed(2)}: re-aimed from there, it turns more than the pair (JITTER bone "${e.bone}"); "reach": false follows the measured angle`;
|
|
3289
|
+
}
|
|
3290
|
+
|
|
3212
3291
|
/** The `REACH` line of a reach chain whose aim departs from its pair (#236). */
|
|
3213
3292
|
export function reachCostLine(e: ReachReading, times: readonly number[]): string {
|
|
3214
3293
|
const f = (v: number | null) => (v === null ? 'not measured' : `${v.toFixed(4)}°/f²`);
|