@rig-c/pose 0.37.1 → 0.39.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 +73 -6
- package/cli.ts +73 -6
- package/package.json +1 -1
- package/src/clean.ts +72 -3
- package/src/depth.ts +190 -0
- package/src/legs.ts +189 -0
- package/src/map.ts +24 -3
- package/src/outdir.ts +54 -0
- package/src/pelvis.ts +498 -0
- package/src/pose.ts +258 -23
- package/src/problems.ts +6 -3
- package/src/retarget.ts +98 -3
- package/src/run.ts +80 -21
- package/src/runcompare.ts +6 -7
- package/src/switch.ts +109 -21
- package/src/verify.ts +90 -9
package/src/retarget.ts
CHANGED
|
@@ -80,7 +80,7 @@ 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
82
|
import type { ArtRange, AttachmentKey, ByHip, Raised, ReachShort, SlotTrack, Switch, SwitchOptions } from './switch.ts';
|
|
83
|
-
import { clampArt, coveredChains, isRaised, measureArt, overflow, RAISED_WRIST_EYES, RANGE_SLACK, reachLimit, runsOf, selectSwitches, twoBone } from './switch.ts';
|
|
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';
|
|
86
86
|
import type { BodyReading, FaceReading, YawSetSwitch } from './face.ts';
|
|
@@ -174,6 +174,33 @@ export interface RetargetOptions extends SwitchOptions {
|
|
|
174
174
|
travel?: Travel;
|
|
175
175
|
/** The sequence was cleaned with `--loop` (its `cleaned.params.loop`, rig-c/pose#241): the head nod's detrend window wraps, the last frame running into the first. */
|
|
176
176
|
loop?: boolean;
|
|
177
|
+
/**
|
|
178
|
+
* rig-c/pose#288: the sequence's pelvis track (`pelvisTilt` per frame, null where
|
|
179
|
+
* declined, and its `pelvisSource`). The pelvis chain's bone (a chain whose
|
|
180
|
+
* joints are r_hip → l_hip) is keyed from it on every frame it reads, and from
|
|
181
|
+
* the hip line on the frames it does not; absent, from the hip line.
|
|
182
|
+
*/
|
|
183
|
+
pelvis?: { source: string; tilt: ReadonlyArray<number | null> };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* rig-c/pose#288: where the pelvis bone's rotation came from. `source` is the
|
|
188
|
+
* track's (`silhouette`, `waistband`, `keys`) or "hip line"; `bone`/`chain` the
|
|
189
|
+
* map's pelvis chain (null without one, when nothing is keyed); `swing` the
|
|
190
|
+
* peak-to-peak of its rotate keys, degrees (null when not keyed); `track` and
|
|
191
|
+
* `hipLine` how many frames each keyed; `trackSource` the track's source when the
|
|
192
|
+
* sequence carries one (also when it read no frame and the hip line was keyed).
|
|
193
|
+
*/
|
|
194
|
+
export interface PelvisReading {
|
|
195
|
+
source: string;
|
|
196
|
+
trackSource: string | null;
|
|
197
|
+
bone: string | null;
|
|
198
|
+
chain: string | null;
|
|
199
|
+
swing: number | null;
|
|
200
|
+
track: number;
|
|
201
|
+
hipLine: number;
|
|
202
|
+
/** The frames keyed from the hip line on a sequence that carries a track: the frames its reading declined. */
|
|
203
|
+
declined: Array<[number, number]>;
|
|
177
204
|
}
|
|
178
205
|
|
|
179
206
|
export interface Hold {
|
|
@@ -448,6 +475,8 @@ export interface Retarget {
|
|
|
448
475
|
nodSkipped: NodSkipped | null;
|
|
449
476
|
/** Each `reach` chain hanging below a bone the animation turns or translates (#236): its aim's departure from the pose pair's angle. */
|
|
450
477
|
reach: ReachReading[];
|
|
478
|
+
/** rig-c/pose#288: the pelvis bone's source; null when the map has no pelvis chain and the sequence no track. */
|
|
479
|
+
pelvis: PelvisReading | null;
|
|
451
480
|
}
|
|
452
481
|
|
|
453
482
|
export interface FigureScaleFrame {
|
|
@@ -809,6 +838,10 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
809
838
|
// Each mapped bone's target world angle per frame, reaches last.
|
|
810
839
|
const targets = new Map<string, number[]>();
|
|
811
840
|
const restOf = new Map<string, number>();
|
|
841
|
+
// rig-c/pose#288: the pelvis chain (one bone, joints r_hip → l_hip either way) keyed from the track where it reads.
|
|
842
|
+
const pelvisChain = map.chains.find((c) => !c.reach && c.bones.length === 1 && ((c.joints[0] === 'r_hip' && c.joints[1] === 'l_hip') || (c.joints[0] === 'l_hip' && c.joints[1] === 'r_hip'))) ?? null;
|
|
843
|
+
const track = opts.pelvis !== undefined && opts.pelvis.tilt.length === n ? opts.pelvis.tilt : null;
|
|
844
|
+
const fromTrack = pelvisChain !== null && track !== null && track.some((v) => v !== null) ? pelvisChain.bones[0]! : null;
|
|
812
845
|
for (const c of map.chains) {
|
|
813
846
|
c.bones.forEach((b, k) => {
|
|
814
847
|
restOf.set(b, restAngle(rest, b, c.aim[k]!));
|
|
@@ -817,6 +850,12 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
817
850
|
const to = series.get(c.joints[k + 1]!)!;
|
|
818
851
|
const angles: number[] = [];
|
|
819
852
|
for (let i = 0; i < n; i++) {
|
|
853
|
+
const v = b === fromTrack ? track![i] : null;
|
|
854
|
+
if (v !== null && v !== undefined) {
|
|
855
|
+
// The track is the r_hip → l_hip direction; a chain written l_hip → r_hip aims the other way.
|
|
856
|
+
angles.push(c.joints[0] === 'r_hip' ? v : wrap(v + 180));
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
820
859
|
const d = Math.hypot(to[i]![0] - from[i]![0], to[i]![1] - from[i]![1]);
|
|
821
860
|
angles.push(d > 1e-9 ? pictureAngle(from[i]!, to[i]!) : (angles[i - 1] ?? NaN));
|
|
822
861
|
}
|
|
@@ -842,6 +881,8 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
842
881
|
const joints: [string, string] = [c.joints[k]!, c.joints[k + 1]!];
|
|
843
882
|
const across = isAcross(joints[0], joints[1]);
|
|
844
883
|
if (!across && opts.short === 'off') return;
|
|
884
|
+
// rig-c/pose#288: a pelvis keyed from its track is not read off the hip pair's length, so it is not held across it.
|
|
885
|
+
if (b === fromTrack) return;
|
|
845
886
|
const from = series.get(joints[0])!;
|
|
846
887
|
const to = series.get(joints[1])!;
|
|
847
888
|
const lengths = from.map((f, i) => Math.hypot(to[i]![0] - f[0], to[i]![1] - f[1]));
|
|
@@ -1211,6 +1252,16 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1211
1252
|
if (opts.stance !== undefined && opts.stance !== 'off' && !plantOn) p.add('STANCE', `--stance ${opts.stance} plants the free foot beside the supporting one that --plant-feet holds, and --plant-feet is not given`);
|
|
1212
1253
|
if (opts.floor !== undefined && !plantOn) p.add('FLOOR', `--floor ${opts.floor} is the floor --plant-feet plants every support span on, and --plant-feet is not given`);
|
|
1213
1254
|
if (opts.floor !== undefined && !Number.isFinite(opts.floor)) p.add('FLOOR', `--floor is ${opts.floor}; it must be a finite world y`);
|
|
1255
|
+
// rig-c/pose#286: a set with a `phase` shows on the report's support spans, read with or without --plant-feet; a run that
|
|
1256
|
+
// reads no feet has none to show it on, and a group of competing sets mixing phase and angle sets has no rule.
|
|
1257
|
+
const phased = map.alternates.filter((a) => a.phase !== undefined);
|
|
1258
|
+
if (phased.length > 0) {
|
|
1259
|
+
if (feet === null) {
|
|
1260
|
+
const why = legs !== null && 'problems' in legs ? legs.problems.join('; ') : `the feet are read from both hips and both ankles, and the map's chains use ${['r_hip', 'l_hip', 'r_ankle', 'l_ankle'].filter((j) => !series.has(j)).join(', ')} nowhere`;
|
|
1261
|
+
p.add('PHASE_FEET', `set(s) ${phased.map((a) => `"${a.name}" ("phase": "${a.phase}")`).join(', ')} show by the supporting foot (left-step while the right foot supports), and this run reads no supports: ${why}`);
|
|
1262
|
+
}
|
|
1263
|
+
for (const x of phaseGroupProblems(map.alternates)) p.add('PHASE_GROUP', x);
|
|
1264
|
+
}
|
|
1214
1265
|
p.throwIfAny();
|
|
1215
1266
|
const askedScales = new Map<string, number[]>();
|
|
1216
1267
|
for (const ls of legScale) askedScales.set(ls.bone, ls.asked);
|
|
@@ -1485,7 +1536,7 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1485
1536
|
}
|
|
1486
1537
|
}
|
|
1487
1538
|
const rawTargets = new Map<string, number[]>([...targets].map(([b, arr]) => [b, [...arr]]));
|
|
1488
|
-
const sw = selectSwitches(map, targets, keyTimes, keyTimes[n - 1]!, slots, { ...opts, shortInfo, handOffsets });
|
|
1539
|
+
const sw = selectSwitches(map, targets, keyTimes, keyTimes[n - 1]!, slots, { ...opts, shortInfo, handOffsets, ...(phased.length > 0 && feet !== null ? { phaseSupports: feet.supports } : {}) });
|
|
1489
1540
|
|
|
1490
1541
|
// The range clamp, outside the spans of a set depicting the bone's chain. Parents
|
|
1491
1542
|
// first, so a bone below takes its clamped parent into its key; a reach is aimed
|
|
@@ -3038,6 +3089,7 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
3038
3089
|
const reach = reachReadings(rest, map, tracks, reachAims, t?.bone ?? null);
|
|
3039
3090
|
|
|
3040
3091
|
const pivot = pivotOf(rest, t?.bone ?? null, legs);
|
|
3092
|
+
const pelvis = pelvisReadingOf(pelvisChain, opts.pelvis, track, fromTrack, tracks);
|
|
3041
3093
|
const scaleFit = t === null || scale === null || origin === null ? null : scaleFitOf({
|
|
3042
3094
|
joints: t.scale.pose, joint: t.joint, bone: t.bone, lengths: scaleLengths, factor: scale.factor,
|
|
3043
3095
|
series, good, legLength: feet?.legLength ?? null, legStraight, rigLeg,
|
|
@@ -3048,10 +3100,53 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
3048
3100
|
animation: { duration: keyTimes[n - 1]!, tracks },
|
|
3049
3101
|
times: keyTimes, targets, origin, holds, bends, short, endOn, flips, scale, scaleFit,
|
|
3050
3102
|
switches: sw.switches, shown: sw.shown, distances: sw.distances, turns, turnsUnread, range, artRange,
|
|
3051
|
-
legScale, segments, legStraight, rigLeg, scales, feet, planted: plantOn, stance, floor, blendHold, closure, openDrift, ankles, pivot, raised, byHip, face, body, figureScale, headNod: headNodReading, nodSkipped, reach,
|
|
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,
|
|
3052
3104
|
};
|
|
3053
3105
|
}
|
|
3054
3106
|
|
|
3107
|
+
/** rig-c/pose#288: the pelvis bone's source, from the chain, the track and the keys written. */
|
|
3108
|
+
function pelvisReadingOf(chain: Chain | null, given: RetargetOptions['pelvis'], track: ReadonlyArray<number | null> | null, fromTrack: string | null, tracks: ReadonlyArray<MotionTrack | SlotTrack>): PelvisReading | null {
|
|
3109
|
+
if (chain === null && given === undefined) return null;
|
|
3110
|
+
const bone = chain?.bones[0] ?? null;
|
|
3111
|
+
const keys = bone === null ? null : tracks.find((tr): tr is MotionTrack => 'bone' in tr && tr.bone === bone && tr.property === 'rotate');
|
|
3112
|
+
const vs = keys === null || keys === undefined ? [] : keys.keys.map((k) => k.v[0]!);
|
|
3113
|
+
const swing = vs.length === 0 ? null : r3(Math.max(...vs) - Math.min(...vs));
|
|
3114
|
+
const n = vs.length;
|
|
3115
|
+
const declined: Array<[number, number]> = [];
|
|
3116
|
+
(track ?? []).forEach((v, i) => {
|
|
3117
|
+
if (v !== null) return;
|
|
3118
|
+
const last = declined[declined.length - 1];
|
|
3119
|
+
if (last !== undefined && last[1] === i - 1) last[1] = i;
|
|
3120
|
+
else declined.push([i, i]);
|
|
3121
|
+
});
|
|
3122
|
+
const onTrack = fromTrack === null ? 0 : (track ?? []).filter((v) => v !== null).length;
|
|
3123
|
+
return {
|
|
3124
|
+
source: fromTrack !== null ? given!.source : 'hip line',
|
|
3125
|
+
trackSource: given?.source ?? null,
|
|
3126
|
+
bone,
|
|
3127
|
+
chain: chain?.name ?? null,
|
|
3128
|
+
swing,
|
|
3129
|
+
track: onTrack,
|
|
3130
|
+
hipLine: bone === null ? 0 : n - onTrack,
|
|
3131
|
+
declined: given === undefined ? [] : declined,
|
|
3132
|
+
};
|
|
3133
|
+
}
|
|
3134
|
+
|
|
3135
|
+
/**
|
|
3136
|
+
* The PELVIS line (rig-c/pose#288): where the pelvis bone's rotation came from and
|
|
3137
|
+
* its swing — "from <source>, swing N°", or "from hip line (no track)".
|
|
3138
|
+
*/
|
|
3139
|
+
export function pelvisLine(r: PelvisReading, frames: number): string {
|
|
3140
|
+
const spans = (rs: Array<[number, number]>) => rs.map(([a, b]) => (a === b ? `${a}` : `${a}–${b}`)).join(', ');
|
|
3141
|
+
const where = r.bone === null ? '' : `bone "${r.bone}", chain "${r.chain}"`;
|
|
3142
|
+
if (r.bone === null) return `PELVIS: not keyed — the sequence carries a ${r.trackSource} track (${frames - r.declined.reduce((s, [a, b]) => s + b - a + 1, 0)} of ${frames} frames read) and the map has no pelvis chain (one bone, joints r_hip → l_hip); no bone turns with the pelvis`;
|
|
3143
|
+
const swing = r.swing === null ? '' : `, swing ${r.swing.toFixed(2)}°`;
|
|
3144
|
+
if (r.trackSource === null) return `PELVIS: from hip line (no track)${swing} (${where}; every frame from r_hip → l_hip)`;
|
|
3145
|
+
if (r.track === 0) return `PELVIS: from hip line (the ${r.trackSource} track read none of the ${frames} frames)${swing} (${where}; every frame from r_hip → l_hip)`;
|
|
3146
|
+
const rest = r.hipLine === 0 ? `all ${frames} frames from the track` : `${r.track} of ${frames} frames from the track, the hip line on the ${r.hipLine} it declined (frames ${spans(r.declined)})`;
|
|
3147
|
+
return `PELVIS: from ${r.source}${swing} (${where}; ${rest})`;
|
|
3148
|
+
}
|
|
3149
|
+
|
|
3055
3150
|
/** A reach bone's origin within this many world units of the anchor's, with nothing above it moved, is aimed along the pair: no REACH line. */
|
|
3056
3151
|
export const REACH_OFFSET = 1e-6;
|
|
3057
3152
|
|
package/src/run.ts
CHANGED
|
@@ -10,19 +10,21 @@ import { tmpdir } from 'node:os';
|
|
|
10
10
|
import { dirname, join, relative, resolve } from 'node:path';
|
|
11
11
|
import { knownSetsOf, readMap, resolveMap, type JointMap, type Rest } from './map.ts';
|
|
12
12
|
import { cleanLine, cleanSummaryLine, inferredSource } from './clean.ts';
|
|
13
|
+
import { depthLine, layersOf, type FrameLayers } from './depth.ts';
|
|
13
14
|
import { convertCoco17, personTrack, readPoseSequence, TRAVEL_IN_PLACE, type CameraFrame } from './pose.ts';
|
|
14
15
|
import { isObject, lineOf, parseJson, Problems, Refusal, type Problem } from './problems.ts';
|
|
15
16
|
import { boneIdentity, droppedLine, keptOutLine, mergeProblems, readFragment, readIdle, targetOf, trackLabel, type IdleReading, type RigcTrack, type Target } from './idle.ts';
|
|
16
|
-
import { bendGroupLine, bendGroups, bendLine, endOnLine, flipLine, holdLine, nodLine, rangeLine, reachCostLine, retarget, shortLine, turnLine, type MotionTrack, type RetargetOptions, type ScaleBand, type ScaleFit } from './retarget.ts';
|
|
17
|
+
import { bendGroupLine, bendGroups, bendLine, endOnLine, flipLine, holdLine, nodLine, pelvisLine, rangeLine, reachCostLine, retarget, shortLine, turnLine, type MotionTrack, type RetargetOptions, type ScaleBand, type ScaleFit } from './retarget.ts';
|
|
17
18
|
import { attachmentBones, baseMotionOf, buildInto, rigcVersion, folds, gate, readBuild, resolveRig, restOf, seamRender, slotsOf } from './rigc.ts';
|
|
18
19
|
import { partsPath, projectSeams, readSeamDoc, readSeamPairs, seamLines, type SeamPair } from './seams.ts';
|
|
19
|
-
import { artRangeLine, chainsMoving, raisedLine, RAISED_WITHIN, reachLine, switchLine, uncoveredFolds } from './switch.ts';
|
|
20
|
+
import { artRangeLine, chainsMoving, phaseLines, raisedLine, RAISED_WITHIN, reachLine, switchLine, uncoveredFolds } from './switch.ts';
|
|
20
21
|
import { jitterLine, landed, type FeetReading, type JitterRow, type Landing, measureFeet, measureJitter, measureLanding, measurePosture, measureShear, poseFrames, postureLines } from './verify.ts';
|
|
21
|
-
import { legsOf, PLANT_SWITCH, STANCE_BOUNDS } from './legs.ts';
|
|
22
|
+
import { FRONTAL_DISAGREE, legsOf, PLANT_SWITCH, STANCE_BOUNDS } from './legs.ts';
|
|
22
23
|
import { M2_IDENTITY, mulM2, reduceKeys, reduceScale, reduceUnderScale, SCALE_REDUCE, setupLocal, smoothTrunk, type M2, type ReduceBone, type Reduced, type ReduceRow, type UnderBone } from './reduce.ts';
|
|
23
|
-
import type { BlendHold, Closure, Floor, FreeAnkleShift, LegScale, LegSegmentRatio, Lowered, Side, Stance, SupportScaleRun } from './legs.ts';
|
|
24
|
+
import type { BlendHold, Closure, Feet, Floor, FreeAnkleShift, LegScale, LegSegmentRatio, Lowered, Side, Stance, SupportScaleRun, SupportSignal } from './legs.ts';
|
|
24
25
|
import { reportRenderDomain, writeReport, type ReportInput } from './report.ts';
|
|
25
26
|
import { PROVENANCE_FILE, provenanceText } from './provenance.ts';
|
|
27
|
+
import { madeLine, planOutDirs } from './outdir.ts';
|
|
26
28
|
import { bodyLine, faceLine } from './face.ts';
|
|
27
29
|
|
|
28
30
|
export interface RetargetArgs extends RetargetOptions {
|
|
@@ -104,15 +106,9 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
104
106
|
const report = a.report ?? defaultReportPath(a.out);
|
|
105
107
|
// A file this run writes goes into a directory the user named: one that is not there yet is made
|
|
106
108
|
// after the run is green (one line says so), as --build and --render make theirs; a file in the way is refused.
|
|
107
|
-
const makeDirs =
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const dir = dirname(path);
|
|
111
|
-
let up = dir;
|
|
112
|
-
while (!existsSync(up) && dirname(up) !== up) up = dirname(up);
|
|
113
|
-
if (!statSync(up).isDirectory()) args.add('ARG_PATH', up === dir ? `${flag} ${path} — ${dir} is not a directory` : `${flag} ${path} — ${up} is a file, so directory ${dir} cannot be made under it`);
|
|
114
|
-
else if (up !== dir) makeDirs.set(dir, [...(makeDirs.get(dir) ?? []), `${flag} ${path}`]);
|
|
115
|
-
}
|
|
109
|
+
const makeDirs = planOutDirs([['--out', a.out], ['--report', report], ['--camera', a.camera]], args);
|
|
110
|
+
// #284: --build and --render make their own directories (rigc, the copy); a file in the way is refused here, up front.
|
|
111
|
+
planOutDirs([['--build', a.build], ['--render', a.render]], args);
|
|
116
112
|
if (a.reference !== undefined && a.build === undefined) args.add('ARG_REFERENCE', `--reference ${a.reference} needs --build: the record it goes in is written beside the build (${PROVENANCE_FILE})`);
|
|
117
113
|
args.throwIfAny();
|
|
118
114
|
const input = resolveRig(a.rig, a.baseMotion);
|
|
@@ -151,7 +147,7 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
151
147
|
const slots = slotsOf(doc0);
|
|
152
148
|
resolveMap(map, rest, a.map, slots, input.sets);
|
|
153
149
|
|
|
154
|
-
const r = retarget(rest, map, frames, times, { ...a, ...(seq.travel !== undefined ? { travel: seq.travel } : {}), ...(seq.cleaned!.params.loop ? { loop: true } : {}) }, slots, input.sets);
|
|
150
|
+
const r = retarget(rest, map, frames, times, { ...a, ...(seq.travel !== undefined ? { travel: seq.travel } : {}), ...(seq.cleaned!.params.loop ? { loop: true } : {}), ...(seq.pelvisSource !== undefined ? { pelvis: { source: seq.pelvisSource, tilt: seq.frames.map((f) => f.pelvisTilt ?? null) } } : {}) }, slots, input.sets);
|
|
155
151
|
if (fragment.length > 0 && ((r.face?.sets?.length ?? 0) > 0 || (r.body?.sets?.length ?? 0) > 0)) {
|
|
156
152
|
const known = knownSetsOf(map, input.sets);
|
|
157
153
|
for (const s of [...(r.face?.sets ?? []), ...(r.body?.sets ?? [])]) {
|
|
@@ -196,6 +192,7 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
196
192
|
if (r.switches.length === 0) say(` SWITCH none — no set within ${a.switchTolerance}° of the pose for ${a.switchMin}s`);
|
|
197
193
|
for (const s of r.switches) say(` SWITCH ${switchLine(s, r.times)}`);
|
|
198
194
|
for (const l of windowLines(map, r.switches, r.distances, r.times, a.switchTolerance)) say(` FRAMES ${l}`);
|
|
195
|
+
for (const l of phaseLines(map, r.switches)) say(` PHASE ${l}`);
|
|
199
196
|
}
|
|
200
197
|
for (const e of r.range) say(` RANGE ${rangeLine(e, r.times)}`);
|
|
201
198
|
if (a.raisedByHead === true && r.raised.length === 0) say(` RAISED none — no set drawing an arm within ${RAISED_WITHIN}° of straight up showed; no key moved`);
|
|
@@ -206,8 +203,14 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
206
203
|
for (const e of r.byHip) if (e.short.length > 0) say(` REACH ${reachLine(e, r.times)}`);
|
|
207
204
|
for (const e of r.reach) say(` REACH ${reachCostLine(e, r.times)}`);
|
|
208
205
|
for (const e of r.artRange) say(` ART-RANGE ${artRangeLine(e, r.times)}`);
|
|
209
|
-
|
|
206
|
+
// #287: a range that clamps no frame says so once, on the last segment's line (the line count stays one per segment).
|
|
207
|
+
const noClamp = legScaleNoClamp(r.legScale);
|
|
208
|
+
r.legScale.forEach((l, k) => say(` LEG-SCALE ${legScaleLine(l, r.times, r.feet?.legLength ?? null, r.rigLeg)}${k === r.legScale.length - 1 && noClamp !== null ? `; ${noClamp}` : ''}`));
|
|
210
209
|
for (const l of r.legScale) for (const s of l.support ?? []) say(` LEG-SCALE ${supportScaleLine(l, s, r.times)}`);
|
|
210
|
+
if (r.pelvis !== null) say(` ${pelvisLine(r.pelvis, seq.frames.length)}`);
|
|
211
|
+
// rig-c/pose#281: the limbs' front-to-back order from the sequence's depth, only when it carries depth.
|
|
212
|
+
const layers: FrameLayers[] | null = seq.frames.some((x) => x.depth !== undefined) ? frames.map((j, i) => layersOf(j, seq.frames[i]!.depth ?? {})) : null;
|
|
213
|
+
if (layers !== null) say(` ${depthLine(layers, seq.frames, seq.cleaned!.params.depth?.gait ?? null)}`);
|
|
211
214
|
if (r.pivot !== null) say(` PIVOT bone "${r.pivot.bone}"'s origin is ${r.pivot.distance.toFixed(2)} units (${r.pivot.hipWidths.toFixed(3)} hip widths) from its thigh roots' midpoint (${r.pivot.roots.join(' + ')}): dx ${r.pivot.offset[0].toFixed(2)}, dy ${r.pivot.offset[1].toFixed(2)}, at setup — the pivot every hip turn swings the legs about`);
|
|
212
215
|
if (r.feet !== null) {
|
|
213
216
|
const f = r.feet;
|
|
@@ -215,6 +218,7 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
215
218
|
say(` FEET ${r.planted ? 'planted' : 'read, not planted (no --plant-feet)'}: leg length (median hip → ankle) r ${f.legLength.r.toFixed(1)}px, l ${f.legLength.l.toFixed(1)}px; ${f.supports.length} support span(s), ${f.changes.filter((c) => c.from !== null).length} change(s) of support, ${f.jumps.length} jump(s), ${f.steps.length} step(s)`);
|
|
216
219
|
for (const sg of r.segments) say(` FEET ${segmentLine(sg)}`);
|
|
217
220
|
say(` FEET supports: ${f.supports.map((x) => `${x.side} ${span(x.frames)}`).join(', ') || 'none'}`);
|
|
221
|
+
if (f.signal !== undefined) for (const l of signalLines(f.signal, f.supports)) say(` FEET ${l}`);
|
|
218
222
|
say(` FEET jumps (both ankles and mid_hip > 0.05 leg lengths above their rolling floor, ≥ 2 frames): ${f.jumps.map(span).join(', ') || 'none'}`);
|
|
219
223
|
if (f.lowered !== undefined) say(` FEET ${loweredLine(f.lowered)}`);
|
|
220
224
|
if (f.tracked !== undefined) say(` FEET tracked (the lower foot supports on the floor, following its scaled ankle track until still, rig-c/pose#119): ${f.tracked.map((x) => `${x.side} ${span(x.frames)}`).join(', ')}`);
|
|
@@ -353,7 +357,7 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
353
357
|
|
|
354
358
|
const lengths = new Map(doc.bones.map((b) => [b.name, b.length ?? 0]));
|
|
355
359
|
const hips = seq.cleaned?.params.hipsInferred;
|
|
356
|
-
const posture = measurePosture(posed, lengths, map, r, frames, a.minScore, inferredSource(seq.cleaned), hips?.by === 'measured' ? hips : null);
|
|
360
|
+
const posture = measurePosture(posed, lengths, map, r, frames, a.minScore, inferredSource(seq.cleaned), hips?.by === 'measured' ? hips : null, seq.pelvisSource !== undefined ? { source: seq.pelvisSource, tilt: seq.frames.map((f) => f.pelvisTilt ?? null) } : null);
|
|
357
361
|
if (posture !== null) for (const l of postureLines(posture)) say(` POSTURE ${l}`);
|
|
358
362
|
const legBones = legBonesOf(map);
|
|
359
363
|
const feetReading = legBones === null ? null : measureFeet(posed, lengths, r, legBones);
|
|
@@ -441,6 +445,9 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
441
445
|
},
|
|
442
446
|
legScale: r.legScale.map(({ scale: _scale, asked: _asked, picture: _picture, ...rest }) => rest),
|
|
443
447
|
pivot: r.pivot,
|
|
448
|
+
pelvis: r.pelvis,
|
|
449
|
+
// Only when the sequence carries depth (#281): a sequence without it writes the report it always has.
|
|
450
|
+
...(layers !== null ? { layers: layers.map((l, frame) => ({ frame, order: l.order, depth: l.depth })) } : {}),
|
|
444
451
|
stance: r.stance === null ? null : { tolerance: r.stance.tolerance, release: r.stance.release, minSeconds: r.stance.minSeconds, bounds: r.stance.bounds, ...(r.stance.range === undefined ? {} : { range: r.stance.range }), runs: r.stance.runs, over: r.stance.over },
|
|
445
452
|
feet: r.feet === null ? null : { planted: r.planted, legLength: r.feet.legLength, straight: r.legStraight, rigLeg: r.rigLeg, segments: r.segments, supports: r.feet.supports, jumps: r.feet.jumps, steps: r.feet.steps, changes: r.feet.changes, ...(r.feet.lowered === undefined ? {} : { lowered: r.feet.lowered }), ...(r.feet.tracked === undefined ? {} : { tracked: r.feet.tracked }), floor: r.floor?.y ?? null, shifts: r.floor?.shifts ?? [], blend: r.blendHold === null ? null : { bounds: r.blendHold.bounds, ...(r.blendHold.range === undefined ? {} : { range: r.blendHold.range }), runs: r.blendHold.runs, over: r.blendHold.over }, reading: feetReading, closure: r.closure },
|
|
446
453
|
shear,
|
|
@@ -913,6 +920,48 @@ function loweredLine(lowered: readonly Lowered[]): string {
|
|
|
913
920
|
return `${head}: ${frames} frame(s) read differently from before — ${lowered.map((x) => `frames ${span(x.frames)} ${side(x.was)} → ${side(x.now)}${x.cause === undefined ? '' : ` (${CAUSE[x.cause]})`}`).join(', ')}`;
|
|
914
921
|
}
|
|
915
922
|
|
|
923
|
+
/**
|
|
924
|
+
* The support-signal lines (rig-c/pose#280). On a clip read as frontal: two
|
|
925
|
+
* lines — how often the ankle's vertical speed contradicts ankle height against
|
|
926
|
+
* `FRONTAL_DISAGREE`, and where the stride shows (in depth) — then one line per
|
|
927
|
+
* support span (a stride) naming the signal that decided its frames and what
|
|
928
|
+
* ankle height alone would have read. On any other clip ankle height decided as
|
|
929
|
+
* before and nothing is printed: the output of a clip the second signal does not
|
|
930
|
+
* decide stays as it was, in every mode (`Feet.signal` still carries the figures).
|
|
931
|
+
*/
|
|
932
|
+
export function signalLines(sig: SupportSignal, supports: Feet['supports']): string[] {
|
|
933
|
+
const span = (x: [number, number]) => (x[0] === x[1] ? `${x[0]}` : `${x[0]}–${x[1]}`);
|
|
934
|
+
const n = Math.round(FRONTAL_DISAGREE * 100);
|
|
935
|
+
const pct = sig.read === 0 ? 0 : (100 * sig.contradict.length) / sig.read;
|
|
936
|
+
if (!sig.frontal) return [];
|
|
937
|
+
const ranges = `x · y against the hips: r ${sig.range.r.x.toFixed(1)} · ${sig.range.r.y.toFixed(1)} px, l ${sig.range.l.x.toFixed(1)} · ${sig.range.l.y.toFixed(1)} px`;
|
|
938
|
+
const contra = `${sig.contradict.length} of ${sig.read} frame(s) (${pct.toFixed(1)} %`;
|
|
939
|
+
const out = [
|
|
940
|
+
`support: ankle vertical speed (rig-c/pose#280) — it contradicts ankle height on ${contra}, past ${n} %): the lower ankle moving while the higher moves with the ground`,
|
|
941
|
+
`support: read as a frontal view — ankles move more in y than x (${ranges}); ankle height only where neither foot moves with the ground`,
|
|
942
|
+
];
|
|
943
|
+
for (const sp of supports) {
|
|
944
|
+
let speed = 0;
|
|
945
|
+
let height = 0;
|
|
946
|
+
let both = 0;
|
|
947
|
+
let differ = 0;
|
|
948
|
+
const was = new Set<string>();
|
|
949
|
+
for (let i = sp.frames[0]; i <= sp.frames[1]; i++) {
|
|
950
|
+
if (sig.by[i] === 'speed') speed++;
|
|
951
|
+
else if (sig.by[i] === 'height') height++;
|
|
952
|
+
if (sig.contact[i] === 'both') both++;
|
|
953
|
+
if (sig.height[i] !== sp.side) {
|
|
954
|
+
differ++;
|
|
955
|
+
was.add(sig.height[i] ?? 'none');
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
const by = speed > 0 ? `ankle vertical speed on ${speed} frame(s)${height > 0 ? `, ankle height on ${height} (neither foot moved with the ground)` : ''}` : `ankle height on ${height} frame(s) (neither foot moved with the ground)`;
|
|
959
|
+
const alone = differ === 0 ? 'ankle height alone reads the same' : `ankle height alone would read ${[...was].join(' or ')} on ${differ} of them`;
|
|
960
|
+
out.push(`stride ${sp.side} ${span(sp.frames)}: ${by} (both feet down on ${both}); ${alone}`);
|
|
961
|
+
}
|
|
962
|
+
return out;
|
|
963
|
+
}
|
|
964
|
+
|
|
916
965
|
/**
|
|
917
966
|
* The in-place line without --plant-feet (rig-c/pose#242): what the declaration still changes, and the one thing it
|
|
918
967
|
* cannot without planted feet — the loop closure, which spreads back the planted hip's drift. `drift` is the
|
|
@@ -1049,6 +1098,21 @@ export function legScaleLine(l: LegScale, times: readonly number[], legLength: R
|
|
|
1049
1098
|
return `chain "${l.chain}" bone "${l.bone}" (${l.joints[0]} → ${l.joints[1]}): straight length ${l.straight.toFixed(1)}px (larger 95th percentile of ${times.length} frames, both legs)${share}; picture length ÷ straight ${l.ratio.min.toFixed(3)} … ${l.ratio.max.toFixed(3)} (median ${l.ratio.median.toFixed(3)}); scalex keyed in [${l.limits[0]}, ${l.limits[1]}] of the straight length (${limitsPx}): ${clamp(l.low, l.limits[0], 'under', 'low')}; ${clamp(l.high, l.limits[1], 'over', 'high')}${free}`;
|
|
1050
1099
|
}
|
|
1051
1100
|
|
|
1101
|
+
/**
|
|
1102
|
+
* rig-c/pose#287: when `--leg-scale`'s range clamps no frame on any leg segment
|
|
1103
|
+
* (no run under the floor, none over the ceiling, no supporting frame scaled past
|
|
1104
|
+
* the floor), the clause naming the range and the picture's own spread across
|
|
1105
|
+
* every segment, so a reader learns the range never bound and what floor would.
|
|
1106
|
+
* Null when any frame was clamped, or with no segment scaled.
|
|
1107
|
+
*/
|
|
1108
|
+
export function legScaleNoClamp(ls: readonly LegScale[]): string | null {
|
|
1109
|
+
if (ls.length === 0 || ls.some((l) => l.low.length > 0 || l.high.length > 0 || (l.support ?? []).length > 0)) return null;
|
|
1110
|
+
const [floor, ceiling] = ls[0]!.limits;
|
|
1111
|
+
const min = Math.min(...ls.map((l) => l.ratio.min));
|
|
1112
|
+
const max = Math.max(...ls.map((l) => l.ratio.max));
|
|
1113
|
+
return `no frame clamped on any leg segment: the range [${floor}, ${ceiling}] holds the picture's whole spread, picture length ÷ straight ${min.toFixed(3)} … ${max.toFixed(3)} over the ${ls.length} segment(s), so each segment is scaled to the picture on every frame; a floor above ${min.toFixed(3)} would clamp`;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1052
1116
|
/** A LEG-SCALE past the floor (support) line (rig-c/pose#133): the supporting leg segment scaled below `--leg-scale`'s floor down to #95's bound. */
|
|
1053
1117
|
export function supportScaleLine(l: LegScale, s: SupportScaleRun, times: readonly number[]): string {
|
|
1054
1118
|
const span = (x: [number, number]) => (x[0] === x[1] ? `frame ${x[0]} (t=${times[x[0]]!.toFixed(3)}s)` : `frames ${x[0]}–${x[1]} (t=${times[x[0]]!.toFixed(3)}–${times[x[1]]!.toFixed(3)}s)`);
|
|
@@ -1133,11 +1197,6 @@ export function defaultReportPath(out: string): string {
|
|
|
1133
1197
|
return `${out.endsWith('.json') ? out.slice(0, -'.json'.length) : out}.report.json`;
|
|
1134
1198
|
}
|
|
1135
1199
|
|
|
1136
|
-
/** rig-c/pose#239: the line a directory this run made prints — the directory, and the paths that named it. */
|
|
1137
|
-
export function madeLine(dir: string, flags: readonly string[]): string {
|
|
1138
|
-
return ` made directory ${dir} — it did not exist; ${flags.join(' and ')} ${flags.length === 1 ? 'is' : 'are'} written into it`;
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
1200
|
/** rig-c/pose#239: the report's `wrote` line — whether it was the default, and the mtime of the report it replaced. */
|
|
1142
1201
|
export function reportLine(report: string, byDefault: boolean, previous: Date | null): string {
|
|
1143
1202
|
return ` wrote ${report} — the run's report (rig-pose-report/1)${byDefault ? ', beside --out by default; name another path with --report' : ''}${previous === null ? '' : `; it replaced the report there last written ${previous.toISOString()}`}`;
|
package/src/runcompare.ts
CHANGED
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
* not a failure: the run exits 0 whenever it compared.
|
|
6
6
|
*/
|
|
7
7
|
import { existsSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
8
|
-
import { dirname } from 'node:path';
|
|
9
8
|
import { compare, findingLine, readCloth, readHands, readSpans, summaryLine, writeComparison, type ClothWindow, type CompareInput, type Comparison } from './compare.ts';
|
|
10
9
|
import { readMap } from './map.ts';
|
|
11
10
|
import { personTrack, readPoseSequence, type PoseSequence } from './pose.ts';
|
|
12
11
|
import { Problems, Refusal } from './problems.ts';
|
|
13
12
|
import { drawable, drawSheet } from './sheet.ts';
|
|
13
|
+
import { makeOutDirs, planOutDirs } from './outdir.ts';
|
|
14
14
|
|
|
15
15
|
export interface CompareArgs {
|
|
16
16
|
reference: string;
|
|
@@ -37,12 +37,9 @@ export function runCompare(a: CompareArgs, log: (line: string) => void): Compari
|
|
|
37
37
|
if (!existsSync(path)) p.add('ARG_PATH', `${flag} ${path} does not exist`);
|
|
38
38
|
else if (!statSync(path).isFile()) p.add('ARG_PATH', `${flag} ${path} is not a file`);
|
|
39
39
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (!existsSync(dir)) p.add('ARG_PATH', `${flag} ${path} — directory ${dir} does not exist`);
|
|
44
|
-
else if (!statSync(dir).isDirectory()) p.add('ARG_PATH', `${flag} ${path} — ${dir} is not a directory`);
|
|
45
|
-
}
|
|
40
|
+
// #284: a directory --report or --sheet names that is not there yet is made just before that file is written.
|
|
41
|
+
const reportDirs = planOutDirs([['--report', a.report]], p);
|
|
42
|
+
const sheetDirs = planOutDirs([['--sheet', a.sheet]], p);
|
|
46
43
|
p.throwIfAny();
|
|
47
44
|
|
|
48
45
|
// Every reader runs, and every refusal is reported together.
|
|
@@ -98,6 +95,7 @@ export function runCompare(a: CompareArgs, log: (line: string) => void): Compari
|
|
|
98
95
|
if (c.cloth.windows.length > 0) log(` CLOTH the render's stroke threshold ${c.cloth.stroke.render.toFixed(4)} of its hip widths = ${c.cloth.stroke.carried}`);
|
|
99
96
|
}
|
|
100
97
|
if (a.report !== undefined) {
|
|
98
|
+
makeOutDirs(reportDirs, log);
|
|
101
99
|
writeFileSync(a.report, writeComparison(c, {
|
|
102
100
|
reference: a.reference, render: a.render, map: a.map, retargetReport: a.retargetReport ?? null,
|
|
103
101
|
referenceHands: a.referenceHands ?? null, renderHands: a.renderHands ?? null, clothReference: a.clothReference ?? null, clothRender: a.clothRender ?? null,
|
|
@@ -108,6 +106,7 @@ export function runCompare(a: CompareArgs, log: (line: string) => void): Compari
|
|
|
108
106
|
const plate = drawSheet({ frames: ref!.frames, width: ref!.seq.width, height: ref!.seq.height }, { frames: ren!.frames, width: ren!.seq.width, height: ren!.seq.height }, c.findings);
|
|
109
107
|
if (plate === null) log(` SHEET no finding has a frame to draw (${c.findings.length} finding(s), none outside cloth); ${a.sheet} not written`);
|
|
110
108
|
else {
|
|
109
|
+
makeOutDirs(sheetDirs, log);
|
|
111
110
|
plate.writePng(a.sheet);
|
|
112
111
|
log(` SHEET ${drawable(c.findings).length} finding(s), reference | render at each worst frame: wrote ${a.sheet}`);
|
|
113
112
|
}
|
package/src/switch.ts
CHANGED
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
* span is dropped, and the base art shows there; a gap shorter than the minimum
|
|
13
13
|
* span between two runs of one set is bridged by that set. No other hysteresis.
|
|
14
14
|
*
|
|
15
|
+
* A set with a `phase` (rig-c/pose#286) is not selected by distance: it shows
|
|
16
|
+
* exactly on its supporting foot's spans (`phaseSupports`, the
|
|
17
|
+
* report's `feet.supports`) and switches on the hand-over, with none of the
|
|
18
|
+
* rules here or below applied to it.
|
|
19
|
+
*
|
|
15
20
|
* Sets compete only with the sets they share a slot with (shown or hidden,
|
|
16
21
|
* transitively), or a bone they offset: two that touch nothing in common — a
|
|
17
22
|
* drawing of each arm — can show at once, and two that do never overlap, so
|
|
@@ -52,8 +57,9 @@
|
|
|
52
57
|
* selection put it; it is the CLI's default, because on HQ's dancer the bounded
|
|
53
58
|
* peak still raised the sets' ART-RANGE runs from 24 to 31 (#31, round 2).
|
|
54
59
|
*/
|
|
55
|
-
import type {
|
|
56
|
-
import {
|
|
60
|
+
import type { Side } from './legs.ts';
|
|
61
|
+
import type { AlternateSet, JointMap, Phase, Rest, RigSlots } from './map.ts';
|
|
62
|
+
import { descends, PHASE_SIDE } from './map.ts';
|
|
57
63
|
import type { MotionKey, MotionTrack } from './retarget.ts';
|
|
58
64
|
import { pictureAngle, wrap } from './retarget.ts';
|
|
59
65
|
import type { MeshFolds } from './rigc.ts';
|
|
@@ -80,6 +86,19 @@ export interface SwitchOptions {
|
|
|
80
86
|
shortInfo?: ShortSwitchInfo;
|
|
81
87
|
/** Per chain, the reference pose's normalised wrist-from-shoulder offset on each frame, `(wrist - shoulder) / scale`, y down (#180). */
|
|
82
88
|
handOffsets?: ReadonlyMap<string, ReadonlyArray<readonly [number, number]>>;
|
|
89
|
+
/** The report's `feet.supports` (rig-c/pose#286): the spans a set with a `phase` shows on. Given only when a set declares one. */
|
|
90
|
+
phaseSupports?: ReadonlyArray<{ side: Side; frames: [number, number] }>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** A phase set's span (rig-c/pose#286): its step side, and the support hand-overs it was switched on and off at. */
|
|
94
|
+
export interface PhaseSpan {
|
|
95
|
+
step: Phase;
|
|
96
|
+
/** The supporting foot it shows on (left-step: the left foot is stepping, so the right foot supports). */
|
|
97
|
+
side: Side;
|
|
98
|
+
/** The hand-over it starts on: the frame, and the side that supported on the frame before (null: no foot). Null from the first frame. */
|
|
99
|
+
on: { frame: number; from: Side | null } | null;
|
|
100
|
+
/** The hand-over it ends on: the first frame after it, and the side that supports there (null: no foot). Null to the end. */
|
|
101
|
+
off: { frame: number; to: Side | null } | null;
|
|
83
102
|
}
|
|
84
103
|
|
|
85
104
|
/** One boundary of a span: where the selection put it, where it is keyed, and how fast the chains it changes move on each. */
|
|
@@ -115,6 +134,8 @@ export interface Switch {
|
|
|
115
134
|
artTurn: Record<string, { max: number; median: number }>;
|
|
116
135
|
/** The boundary it starts on (null from the first frame) and the one it ends on (null to the end); a boundary between two sets is in both. */
|
|
117
136
|
moved: { on: Move | null; off: Move | null };
|
|
137
|
+
/** Only on a set with a `phase` (rig-c/pose#286): the step side and the hand-overs the span was switched on. */
|
|
138
|
+
phase?: PhaseSpan;
|
|
118
139
|
}
|
|
119
140
|
|
|
120
141
|
export interface AttachmentKey {
|
|
@@ -245,6 +266,28 @@ function touches(a: AlternateSet): string[] {
|
|
|
245
266
|
return [...Object.keys(a.slots), ...a.hides].map((s) => `slot:${s}`).concat(Object.keys(a.offsets).map((b) => `bone:${b}`));
|
|
246
267
|
}
|
|
247
268
|
|
|
269
|
+
/**
|
|
270
|
+
* rig-c/pose#286: what the retarget refuses about the map's phase sets, one text each. A group of competing sets (a
|
|
271
|
+
* common slot or offset bone, transitively) that holds a phase set must hold only phase sets, and at most one of each
|
|
272
|
+
* side: the hand-over decides which of them shows, and nothing would decide between two of one side, or between a phase
|
|
273
|
+
* set and an angle set over the frames they share.
|
|
274
|
+
*/
|
|
275
|
+
export function phaseGroupProblems(sets: readonly AlternateSet[]): string[] {
|
|
276
|
+
const out: string[] = [];
|
|
277
|
+
for (const group of competing(sets)) {
|
|
278
|
+
const phased = group.filter((a) => a.phase !== undefined);
|
|
279
|
+
if (phased.length === 0) continue;
|
|
280
|
+
const angle = group.filter((a) => a.phase === undefined);
|
|
281
|
+
const named = (xs: readonly AlternateSet[]) => xs.map((a) => `"${a.name}"`).join(', ');
|
|
282
|
+
if (angle.length > 0) out.push(`set(s) ${named(phased)} declare "phase" and compete with set(s) ${named(angle)}, which declare none (they switch a common slot or offset bone); a phase set shows on its supporting foot's spans and an angle set on its distance, and nothing decides between them — give every set of the group a "phase", or none`);
|
|
283
|
+
for (const step of Object.keys(PHASE_SIDE) as Phase[]) {
|
|
284
|
+
const same = phased.filter((a) => a.phase === step);
|
|
285
|
+
if (same.length > 1) out.push(`set(s) ${named(same)} all declare "phase": "${step}" and compete (they switch a common slot or offset bone); one drawing shows per support span — keep one "${step}" set per group`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return out;
|
|
289
|
+
}
|
|
290
|
+
|
|
248
291
|
/** Groups of sets that switch a common slot or offset a common bone, transitively; each in map order. */
|
|
249
292
|
function competing(sets: readonly AlternateSet[]): AlternateSet[][] {
|
|
250
293
|
const parent = sets.map((_, i) => i);
|
|
@@ -447,8 +490,41 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
447
490
|
const distances = setDistances(map, targets, n, opts.shortInfo);
|
|
448
491
|
const hyst = opts.switchHysteresis ?? SWITCH_LEAVE_HYSTERESIS;
|
|
449
492
|
const switches: Switch[] = [];
|
|
493
|
+
// rig-c/pose#286: the supporting side on each frame, from the report's support spans (null: no foot, or no set declares a phase).
|
|
494
|
+
const supportAt: Array<Side | null> = Array.from({ length: n }, () => null);
|
|
495
|
+
for (const sp of opts.phaseSupports ?? []) for (let i = sp.frames[0]; i <= Math.min(sp.frames[1], n - 1); i++) supportAt[i] = sp.side;
|
|
496
|
+
/** One group's pick, as segments between boundaries, then each boundary moved (#31), pushed as switches. */
|
|
497
|
+
const spans = (pick: ReadonlyArray<AlternateSet | null>, moveOpts: SwitchOptions): void => {
|
|
498
|
+
const vals: Array<AlternateSet | null> = [pick[0] ?? null];
|
|
499
|
+
const pos: number[] = [];
|
|
500
|
+
for (let i = 1; i < n; i++) {
|
|
501
|
+
if (pick[i] === pick[i - 1]) continue;
|
|
502
|
+
pos.push(i);
|
|
503
|
+
vals.push(pick[i] ?? null);
|
|
504
|
+
}
|
|
505
|
+
const moves = moveBoundaries(vals, pos, map, targets, distances, times, duration, moveOpts);
|
|
506
|
+
for (let s = 0; s < vals.length; s++) {
|
|
507
|
+
const a = vals[s];
|
|
508
|
+
if (a === null || a === undefined) continue;
|
|
509
|
+
const i = s === 0 ? 0 : moves[s - 1]!.frame[1];
|
|
510
|
+
const j = s < pos.length ? moves[s]!.frame[1] - 1 : n - 1;
|
|
511
|
+
const from = times[i]!;
|
|
512
|
+
const to = j + 1 < n ? times[j + 1]! : duration;
|
|
513
|
+
const ds = distances.get(a.name)!.slice(i, j + 1);
|
|
514
|
+
const phase: PhaseSpan | undefined = a.phase === undefined ? undefined : { step: a.phase, side: PHASE_SIDE[a.phase], on: i === 0 ? null : { frame: i, from: supportAt[i - 1] ?? null }, off: j + 1 < n ? { frame: j + 1, to: supportAt[j + 1] ?? null } : null };
|
|
515
|
+
switches.push({ set: a.name, chains: a.chains, slots: a.slots, hides: a.hides, offsets: a.offsets, from, to, frames: [i, j], distance: { max: r3(Math.max(...ds)), median: r3(median(ds)) }, artTurn: {}, moved: { on: s === 0 ? null : moves[s - 1]!, off: s < pos.length ? moves[s]! : null }, ...(phase === undefined ? {} : { phase }) });
|
|
516
|
+
}
|
|
517
|
+
};
|
|
450
518
|
const groups = competing(map.alternates);
|
|
451
519
|
for (const group of groups) {
|
|
520
|
+
// rig-c/pose#286: a group of phase sets shows each exactly on its supporting foot's spans — no tolerance, no minimum run,
|
|
521
|
+
// no hysteresis, no bridging, and its boundaries stay on the hand-overs (no --switch-at peak move). The retarget refuses
|
|
522
|
+
// a group mixing phase and angle sets, or two sets of one side (`phaseGroupProblems`), before it gets here.
|
|
523
|
+
if (group.some((a) => a.phase !== undefined)) {
|
|
524
|
+
const pick = supportAt.map((side) => (side === null ? null : group.find((a) => a.phase !== undefined && PHASE_SIDE[a.phase] === side) ?? null));
|
|
525
|
+
spans(pick, { ...opts, switchAt: 'first' });
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
452
528
|
const layer = groupLayering(group, map, slots);
|
|
453
529
|
const pick: Array<AlternateSet | null> = [];
|
|
454
530
|
for (let i = 0; i < n; i++) {
|
|
@@ -582,25 +658,7 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
582
658
|
}
|
|
583
659
|
if (dropped) bridgeSame(pick);
|
|
584
660
|
}
|
|
585
|
-
|
|
586
|
-
const vals: Array<AlternateSet | null> = [pick[0] ?? null];
|
|
587
|
-
const pos: number[] = [];
|
|
588
|
-
for (let i = 1; i < n; i++) {
|
|
589
|
-
if (pick[i] === pick[i - 1]) continue;
|
|
590
|
-
pos.push(i);
|
|
591
|
-
vals.push(pick[i] ?? null);
|
|
592
|
-
}
|
|
593
|
-
const moves = moveBoundaries(vals, pos, map, targets, distances, times, duration, opts);
|
|
594
|
-
for (let s = 0; s < vals.length; s++) {
|
|
595
|
-
const a = vals[s];
|
|
596
|
-
if (a === null || a === undefined) continue;
|
|
597
|
-
const i = s === 0 ? 0 : moves[s - 1]!.frame[1];
|
|
598
|
-
const j = s < pos.length ? moves[s]!.frame[1] - 1 : n - 1;
|
|
599
|
-
const from = times[i]!;
|
|
600
|
-
const to = j + 1 < n ? times[j + 1]! : duration;
|
|
601
|
-
const ds = distances.get(a.name)!.slice(i, j + 1);
|
|
602
|
-
switches.push({ set: a.name, chains: a.chains, slots: a.slots, hides: a.hides, offsets: a.offsets, from, to, frames: [i, j], distance: { max: r3(Math.max(...ds)), median: r3(median(ds)) }, artTurn: {}, moved: { on: s === 0 ? null : moves[s - 1]!, off: s < pos.length ? moves[s]! : null } });
|
|
603
|
-
}
|
|
661
|
+
spans(pick, opts);
|
|
604
662
|
}
|
|
605
663
|
const order = (name: string) => map.alternates.findIndex((a) => a.name === name);
|
|
606
664
|
switches.sort((x, y) => x.frames[0] - y.frames[0] || order(x.set) - order(y.set));
|
|
@@ -632,6 +690,7 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
632
690
|
// and in a one-sided gesture set keep an arm whose wrist stays behind the torso on its previous behind set.
|
|
633
691
|
const chainHolds: Array<{ set: string; chain: string; frames: [number, number] }> = [];
|
|
634
692
|
for (const group of groups) {
|
|
693
|
+
if (group.some((a) => a.phase !== undefined)) continue; // #286: a phase group shows exactly its support spans
|
|
635
694
|
const layer = groupLayering(group, map, slots);
|
|
636
695
|
if (!layer.baseInFront) continue;
|
|
637
696
|
const names = new Set(group.map((a) => a.name));
|
|
@@ -1193,6 +1252,35 @@ export function switchLine(s: Switch, times: readonly number[]): string {
|
|
|
1193
1252
|
return parts.join('; ');
|
|
1194
1253
|
}
|
|
1195
1254
|
|
|
1255
|
+
/**
|
|
1256
|
+
* rig-c/pose#286: one `PHASE` line per set with a `phase` — the support side it shows on, every span it showed, and the
|
|
1257
|
+
* hand-over each span was switched on and off at (the side before → the side after; "none" where no foot supports).
|
|
1258
|
+
*/
|
|
1259
|
+
export function phaseLines(map: JointMap, switches: readonly Switch[]): string[] {
|
|
1260
|
+
const out: string[] = [];
|
|
1261
|
+
const sideText = (x: Side | null) => x ?? 'none';
|
|
1262
|
+
for (const set of map.alternates) {
|
|
1263
|
+
if (set.phase === undefined) continue;
|
|
1264
|
+
const side = PHASE_SIDE[set.phase];
|
|
1265
|
+
const foot = (x: Side) => (x === 'l' ? 'left' : 'right');
|
|
1266
|
+
const head = `set "${set.name}" "${set.phase}" — the ${foot(side === 'l' ? 'r' : 'l')} foot is stepping; shown while the ${foot(side)} foot supports (feet.supports)`;
|
|
1267
|
+
const own = switches.filter((s) => s.set === set.name && s.phase !== undefined);
|
|
1268
|
+
if (own.length === 0) {
|
|
1269
|
+
out.push(`${head}: on no frame — no ${side} support span`);
|
|
1270
|
+
continue;
|
|
1271
|
+
}
|
|
1272
|
+
const count = own.reduce((t, s) => t + s.frames[1] - s.frames[0] + 1, 0);
|
|
1273
|
+
const spansText = own.map((s) => {
|
|
1274
|
+
const ph = s.phase!;
|
|
1275
|
+
const on = ph.on === null ? 'on the first frame' : `on frame ${ph.on.frame} ${sideText(ph.on.from)} → ${side}`;
|
|
1276
|
+
const off = ph.off === null ? 'off at the end' : `off frame ${ph.off.frame} ${side} → ${sideText(ph.off.to)}`;
|
|
1277
|
+
return `${s.frames[0]}–${s.frames[1]} (${on}, ${off})`;
|
|
1278
|
+
});
|
|
1279
|
+
out.push(`${head}: ${own.length} span(s), ${count} frame(s): ${spansText.join(', ')}`);
|
|
1280
|
+
}
|
|
1281
|
+
return out;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1196
1284
|
const spd = (v: number) => `${v.toFixed(1)}°/frame`;
|
|
1197
1285
|
const win = (w: [number, number]) => (w[0] === w[1] ? `window frame ${w[0]}` : `window frames ${w[0]}–${w[1]}`);
|
|
1198
1286
|
function moveText(m: Move): string {
|