@rig-c/pose 0.38.0 → 0.40.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 +64 -9
- package/cli.ts +64 -10
- package/package.json +1 -1
- package/src/clean.ts +24 -2
- package/src/depth.ts +190 -0
- package/src/draworder.ts +290 -0
- package/src/legs.ts +189 -0
- package/src/outdir.ts +54 -0
- package/src/pelvis.ts +100 -15
- package/src/pose.ts +150 -18
- package/src/reduce.ts +21 -12
- package/src/retarget.ts +82 -3
- package/src/run.ts +162 -35
- package/src/runcompare.ts +6 -7
- package/src/switch.ts +64 -2
- package/src/verify.ts +90 -9
package/src/run.ts
CHANGED
|
@@ -10,19 +10,22 @@ 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';
|
|
14
|
+
import { drawOrderFromDepth, drawOrderKeys, drawOrderLines, type DrawOrderReading } from './draworder.ts';
|
|
13
15
|
import { convertCoco17, personTrack, readPoseSequence, TRAVEL_IN_PLACE, type CameraFrame } from './pose.ts';
|
|
14
16
|
import { isObject, lineOf, parseJson, Problems, Refusal, type Problem } from './problems.ts';
|
|
15
17
|
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, pelvisLine, rangeLine, reachCostLine, retarget, shortLine, turnLine, type MotionTrack, type RetargetOptions, type ScaleBand, type ScaleFit } from './retarget.ts';
|
|
18
|
+
import { bendGroupLine, bendGroups, bendLine, endOnLine, flipLine, holdLine, nodLine, pelvisLine, rangeLine, reachCostLine, reachOriginLine, reachOriginWarn, retarget, shortLine, turnLine, type MotionTrack, type RetargetOptions, type ScaleBand, type ScaleFit } from './retarget.ts';
|
|
17
19
|
import { attachmentBones, baseMotionOf, buildInto, rigcVersion, folds, gate, readBuild, resolveRig, restOf, seamRender, slotsOf } from './rigc.ts';
|
|
18
20
|
import { partsPath, projectSeams, readSeamDoc, readSeamPairs, seamLines, type SeamPair } from './seams.ts';
|
|
19
|
-
import { artRangeLine, chainsMoving, phaseLines, raisedLine, RAISED_WITHIN, reachLine, switchLine, uncoveredFolds } from './switch.ts';
|
|
21
|
+
import { artRangeLine, chainsMoving, phaseLines, raisedLine, RAISED_WITHIN, reachLine, switchLine, switchMinLines, uncoveredFolds } from './switch.ts';
|
|
20
22
|
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';
|
|
23
|
+
import { FRONTAL_DISAGREE, legsOf, PLANT_SWITCH, STANCE_BOUNDS } from './legs.ts';
|
|
22
24
|
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';
|
|
25
|
+
import type { BlendHold, Closure, Feet, Floor, FreeAnkleShift, LegScale, LegSegmentRatio, Lowered, Side, Stance, SupportScaleRun, SupportSignal } from './legs.ts';
|
|
24
26
|
import { reportRenderDomain, writeReport, type ReportInput } from './report.ts';
|
|
25
27
|
import { PROVENANCE_FILE, provenanceText } from './provenance.ts';
|
|
28
|
+
import { madeLine, planOutDirs } from './outdir.ts';
|
|
26
29
|
import { bodyLine, faceLine } from './face.ts';
|
|
27
30
|
|
|
28
31
|
export interface RetargetArgs extends RetargetOptions {
|
|
@@ -62,6 +65,8 @@ export interface RetargetArgs extends RetargetOptions {
|
|
|
62
65
|
merge?: string;
|
|
63
66
|
/** Thin the mapped bones' rotate keys within this many degrees (#32). */
|
|
64
67
|
reduce?: number;
|
|
68
|
+
/** With reduce: a map chain's own tolerance in degrees, by chain name, in place of `reduce` on its bones (#283). */
|
|
69
|
+
reduceChains?: Record<string, number>;
|
|
65
70
|
/** With reduce: thin the hip's translate keys within this many rig units. */
|
|
66
71
|
reduceHip?: number;
|
|
67
72
|
/** With reduce and plantFeet: keep each support span's frames on the supporting leg at its dense keys (#251). */
|
|
@@ -74,6 +79,8 @@ export interface RetargetArgs extends RetargetOptions {
|
|
|
74
79
|
* same in all three and the gate is the same gate.
|
|
75
80
|
*/
|
|
76
81
|
output?: 'default' | 'verbose' | 'summary';
|
|
82
|
+
/** `--draw-order depth` (rig-c/pose#304): key the animation's draw order from the sequence's depth. Absent: no draw-order key, the output as before. */
|
|
83
|
+
drawOrder?: 'depth';
|
|
77
84
|
}
|
|
78
85
|
|
|
79
86
|
export interface RunResult {
|
|
@@ -104,15 +111,9 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
104
111
|
const report = a.report ?? defaultReportPath(a.out);
|
|
105
112
|
// A file this run writes goes into a directory the user named: one that is not there yet is made
|
|
106
113
|
// 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
|
-
}
|
|
114
|
+
const makeDirs = planOutDirs([['--out', a.out], ['--report', report], ['--camera', a.camera]], args);
|
|
115
|
+
// #284: --build and --render make their own directories (rigc, the copy); a file in the way is refused here, up front.
|
|
116
|
+
planOutDirs([['--build', a.build], ['--render', a.render]], args);
|
|
116
117
|
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
118
|
args.throwIfAny();
|
|
118
119
|
const input = resolveRig(a.rig, a.baseMotion);
|
|
@@ -132,9 +133,13 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
132
133
|
const fragment = a.merge === undefined ? [] : readFragment(parseJson(readFileSync(a.merge, 'utf8'), a.merge, 'MERGE_JSON'), a.merge);
|
|
133
134
|
const mapSnapshot = readReportInput(a.map), poseSnapshot = readReportInput(a.pose);
|
|
134
135
|
const map = readMap(mapSnapshot.bytes.toString('utf8'), a.map);
|
|
136
|
+
// #283: each chain --reduce names must be the map's; a bone takes its chain's tolerance (the tighter, if two chains name it).
|
|
137
|
+
const reduceOf = a.reduce === undefined ? null : reduceTolerances(map, a.reduce, a.reduceChains, a.map);
|
|
135
138
|
const poseText = poseSnapshot.bytes.toString('utf8');
|
|
136
139
|
const seq = a.coco ? convertCoco17(poseText, a.pose) : readPoseSequence(poseText, a.pose);
|
|
137
140
|
if (seq.cleaned === undefined) refuse([{ code: 'POSE_NOT_CLEANED', text: notCleaned(a.pose, a.coco) }]);
|
|
141
|
+
// #304: the draw order is read off the sequence's depth; asked for on a sequence without it, refused by name.
|
|
142
|
+
if (a.drawOrder === 'depth' && !seq.frames.some((x) => x.depth !== undefined)) refuse([{ code: 'DRAWORDER_NO_DEPTH', text: `--draw-order depth keys the draw order from the sequence's depth, and ${a.pose} carries none (rig-pose-sequence/${seq.cleaned!.params.depth === undefined ? '1 or 1.1, no "depth" on any frame' : '1.2 with no frame naming a segment'}); clean it with --depth walk|run, or retarget without --draw-order` }]);
|
|
138
143
|
const frames = personTrack(seq, a.person, a.pose);
|
|
139
144
|
const times = seq.frames.map((f) => f.t);
|
|
140
145
|
|
|
@@ -168,7 +173,7 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
168
173
|
}
|
|
169
174
|
}
|
|
170
175
|
}
|
|
171
|
-
tell(`rig-pose retarget: ${seq.frames.length} frame(s), ${r.animation.duration.toFixed(3)}s, onto "${input.name}" as "${a.name}" — ${r.animation.tracks.length} track(s); min-score ${a.minScore}, flip ${a.flip === 'off' ? 'off' : `${a.flip}°`}, short ${a.short}, bends ${a.bends} (tolerance ${a.bendTolerance}°)${a.legScale !== undefined && a.legScale !== 'off' ? `, leg-scale ${a.legScale[0]}–${a.legScale[1]}` : ''}${a.plantFeet === true ? ', plant-feet' : ''}${a.stance !== undefined && a.stance !== 'off' ? `, stance ${a.stance}` : ''}${a.raisedByHead === true ? ', raised-by-head' : ''}${a.carryIdle === true ? ', carry-idle' : ''}${a.merge !== undefined ? `, merge ${a.merge}` : ''}${a.reduce !== undefined ? `, reduce ${a.reduce}°${a.reduceHip !== undefined ? ` (hip ${a.reduceHip} units)` : ''}${a.reduceKeepPlanted === true ? ', planted legs kept dense' : ''}` : ''}`);
|
|
176
|
+
tell(`rig-pose retarget: ${seq.frames.length} frame(s), ${r.animation.duration.toFixed(3)}s, onto "${input.name}" as "${a.name}" — ${r.animation.tracks.length} track(s); min-score ${a.minScore}, flip ${a.flip === 'off' ? 'off' : `${a.flip}°`}, short ${a.short}, bends ${a.bends} (tolerance ${a.bendTolerance}°)${a.legScale !== undefined && a.legScale !== 'off' ? `, leg-scale ${a.legScale[0]}–${a.legScale[1]}` : ''}${a.plantFeet === true ? ', plant-feet' : ''}${a.stance !== undefined && a.stance !== 'off' ? `, stance ${a.stance}` : ''}${a.raisedByHead === true ? ', raised-by-head' : ''}${a.carryIdle === true ? ', carry-idle' : ''}${a.merge !== undefined ? `, merge ${a.merge}` : ''}${a.reduce !== undefined ? `, reduce ${a.reduce}°${chainsText(a.reduceChains)}${a.reduceHip !== undefined ? ` (hip ${a.reduceHip} units)` : ''}${a.reduceKeepPlanted === true ? ', planted legs kept dense' : ''}` : ''}`);
|
|
172
177
|
say(` RIGC rig-c ${rigc.version} (${rigc.entry}) gates and poses this run`);
|
|
173
178
|
// #250: the gate was green without it, so nothing else would say the rig's own animations are not in --out.
|
|
174
179
|
const unread = unreadMotionLine(a.rig, input.motion);
|
|
@@ -195,6 +200,8 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
195
200
|
if (map.alternates.length > 0) {
|
|
196
201
|
if (r.switches.length === 0) say(` SWITCH none — no set within ${a.switchTolerance}° of the pose for ${a.switchMin}s`);
|
|
197
202
|
for (const s of r.switches) say(` SWITCH ${switchLine(s, r.times)}`);
|
|
203
|
+
// #279: only for a set the minimum span held back, so a run where nothing was suppressed prints what it always has.
|
|
204
|
+
for (const l of switchMinLines(r.switchRuns, a.switchMin)) say(` SWITCH-MIN ${l}`);
|
|
198
205
|
for (const l of windowLines(map, r.switches, r.distances, r.times, a.switchTolerance)) say(` FRAMES ${l}`);
|
|
199
206
|
for (const l of phaseLines(map, r.switches)) say(` PHASE ${l}`);
|
|
200
207
|
}
|
|
@@ -206,10 +213,23 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
206
213
|
for (const e of r.byHip) say(` BY-HIP ${raisedLine(e, r.times)}`);
|
|
207
214
|
for (const e of r.byHip) if (e.short.length > 0) say(` REACH ${reachLine(e, r.times)}`);
|
|
208
215
|
for (const e of r.reach) say(` REACH ${reachCostLine(e, r.times)}`);
|
|
216
|
+
// rig-c/pose#282: each reach chain's origin against its source joint; a map without one prints none.
|
|
217
|
+
for (const e of r.reachOrigin) {
|
|
218
|
+
say(` REACH-ORIGIN ${reachOriginLine(e, r.times)}`);
|
|
219
|
+
if (e.warn) say(` WARN ${reachOriginWarn(e)}`);
|
|
220
|
+
}
|
|
209
221
|
for (const e of r.artRange) say(` ART-RANGE ${artRangeLine(e, r.times)}`);
|
|
210
|
-
|
|
222
|
+
// #287: a range that clamps no frame says so once, on the last segment's line (the line count stays one per segment).
|
|
223
|
+
const noClamp = legScaleNoClamp(r.legScale);
|
|
224
|
+
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}` : ''}`));
|
|
211
225
|
for (const l of r.legScale) for (const s of l.support ?? []) say(` LEG-SCALE ${supportScaleLine(l, s, r.times)}`);
|
|
212
226
|
if (r.pelvis !== null) say(` ${pelvisLine(r.pelvis, seq.frames.length)}`);
|
|
227
|
+
// rig-c/pose#281: the limbs' front-to-back order from the sequence's depth, only when it carries depth.
|
|
228
|
+
const layers: FrameLayers[] | null = seq.frames.some((x) => x.depth !== undefined) ? frames.map((j, i) => layersOf(j, seq.frames[i]!.depth ?? {})) : null;
|
|
229
|
+
if (layers !== null) say(` ${depthLine(layers, seq.frames, seq.cleaned!.params.depth?.gait ?? null)}`);
|
|
230
|
+
// rig-c/pose#304: with --draw-order depth, the legs' draw order keyed from those layers.
|
|
231
|
+
const drawOrder: DrawOrderReading | null = a.drawOrder === 'depth' && layers !== null ? drawOrderFromDepth(layers, frames, seq.frames.map((x) => x.depth), r.times, map, rest, doc0.slots, input.sets, seq.cleaned!.params.loop) : null;
|
|
232
|
+
if (drawOrder !== null) for (const l of drawOrderLines(drawOrder)) say(` ${l}`);
|
|
213
233
|
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`);
|
|
214
234
|
if (r.feet !== null) {
|
|
215
235
|
const f = r.feet;
|
|
@@ -217,6 +237,7 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
217
237
|
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)`);
|
|
218
238
|
for (const sg of r.segments) say(` FEET ${segmentLine(sg)}`);
|
|
219
239
|
say(` FEET supports: ${f.supports.map((x) => `${x.side} ${span(x.frames)}`).join(', ') || 'none'}`);
|
|
240
|
+
if (f.signal !== undefined) for (const l of signalLines(f.signal, f.supports)) say(` FEET ${l}`);
|
|
220
241
|
say(` FEET jumps (both ankles and mid_hip > 0.05 leg lengths above their rolling floor, ≥ 2 frames): ${f.jumps.map(span).join(', ') || 'none'}`);
|
|
221
242
|
if (f.lowered !== undefined) say(` FEET ${loweredLine(f.lowered)}`);
|
|
222
243
|
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(', ')}`);
|
|
@@ -236,15 +257,16 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
236
257
|
if (r.body !== null) say(` BODY ${bodyLine(r.body)}`);
|
|
237
258
|
|
|
238
259
|
// --reduce (#32): the dense keys thinned top-down, before anything reads the tracks.
|
|
239
|
-
const reduced = a.reduce === undefined ? null : reduceTracks(r, map, rest, a.reduce, a.reduceHip, a.reduceKeepPlanted === true);
|
|
260
|
+
const reduced = a.reduce === undefined ? null : a.reduceChains === undefined ? reduceTracks(r, map, rest, a.reduce, a.reduceHip, a.reduceKeepPlanted === true) : reduceTracks(r, map, rest, a.reduce, a.reduceHip, a.reduceKeepPlanted === true, reduceOf!);
|
|
240
261
|
if (reduced !== null) {
|
|
241
262
|
for (const row of reduced.rows) {
|
|
242
263
|
const [who, track, what, unit] = row.kind === 'angle' ? ['bone', 'rotate', 'aim', '°'] : row.kind === 'origin' ? ['hip', 'translate', 'origin', ' units'] : ['bone', 'scalex', 'scale', ' of its setup length'];
|
|
243
|
-
|
|
264
|
+
const own = row.kind === 'angle' && reduceOf!(row.bone) !== a.reduce ? ` — within its chain's ${reduceOf!(row.bone)}°` : '';
|
|
265
|
+
say(` REDUCE ${who} "${row.bone}" ${track}: ${row.before} → ${row.after} key(s); worst ${what} error ${row.worst.toPrecision(3)}${unit} at frame ${row.frame} (t=${r.times[row.frame]!.toFixed(3)}s)${own}`);
|
|
244
266
|
}
|
|
245
267
|
const sum = (kind: ReduceRow['kind'], k: 'before' | 'after') => reduced.rows.filter((x) => x.kind === kind).reduce((t, x) => t + x[k], 0);
|
|
246
268
|
const scales = reduced.rows.some((x) => x.kind === 'scale') ? `; scalex keys ${sum('scale', 'before')} → ${sum('scale', 'after')} within ${SCALE_REDUCE} of the setup length` : '';
|
|
247
|
-
say(` REDUCE rotate keys ${sum('angle', 'before')} → ${sum('angle', 'after')} (${(sum('angle', 'before') / sum('angle', 'after')).toFixed(2)}×) within ${a.reduce}°${scales}${a.reduceHip === undefined ? '; the hip stays dense (no --reduce-hip)' : ''}`);
|
|
269
|
+
say(` REDUCE rotate keys ${sum('angle', 'before')} → ${sum('angle', 'after')} (${(sum('angle', 'before') / sum('angle', 'after')).toFixed(2)}×) within ${a.reduce}°${chainsText(a.reduceChains)}${scales}${a.reduceHip === undefined ? '; the hip stays dense (no --reduce-hip)' : ''}`);
|
|
248
270
|
}
|
|
249
271
|
|
|
250
272
|
// The idle against the new animation (#43 item 2): what it drops, and with --carry-idle, looped back in.
|
|
@@ -293,7 +315,9 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
293
315
|
for (const tr of fragment) say(` MERGE ${a.merge}: ${trackLabel(tr, groups)}, ${tr.keys.length} key(s) at t=${tr.keys[0]!.t}${tr.keys.length > 1 ? `–${tr.keys[tr.keys.length - 1]!.t}` : ''}s`);
|
|
294
316
|
}
|
|
295
317
|
const added = [...carried, ...fragment];
|
|
296
|
-
const
|
|
318
|
+
const tracked = added.length === 0 ? r.animation : { ...r.animation, tracks: [...own, ...added] };
|
|
319
|
+
// #304: rig-c's one draw-order timeline sits on the animation beside its tracks; written only when keyed.
|
|
320
|
+
const animation = drawOrder === null || drawOrder.keys.length === 0 ? tracked : { ...tracked, drawOrder: drawOrderKeys(drawOrder) };
|
|
297
321
|
const motion = { ...base, animations: { ...animations, [a.name]: animation } };
|
|
298
322
|
const g1 = gate(input.rig, motion, join(scratch, 'with'), a.profile, a.images);
|
|
299
323
|
if (!g1.green) {
|
|
@@ -312,11 +336,12 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
312
336
|
const support = a.reduce !== undefined && a.reduceKeepPlanted === true && r.feet !== null ? supportFrames(r.feet.supports) : undefined;
|
|
313
337
|
const landings = measureLanding(doc, a.name, map, r, posed, support).map((l) => {
|
|
314
338
|
if (a.reduce === undefined) return l;
|
|
315
|
-
|
|
339
|
+
// #283: a chain's own tolerance is added as the run's is; the sum is rounded only under a per-chain --reduce.
|
|
340
|
+
if (l.kind === 'angle') return { ...l, tolerance: a.reduceChains === undefined ? l.tolerance + a.reduce : Number((l.tolerance + reduceOf!(l.bone)).toPrecision(12)) };
|
|
316
341
|
if (l.kind === 'origin' && a.reduceHip !== undefined) return { ...l, tolerance: l.tolerance + a.reduceHip };
|
|
317
342
|
if (l.kind !== 'foot') return l;
|
|
318
343
|
const ankle = l.bone.replace(/:tip$/, '');
|
|
319
|
-
const by = footReduce(rest, r, ankle, a.reduce, a.reduceHip);
|
|
344
|
+
const by = footReduce(rest, r, ankle, a.reduce, a.reduceHip, a.reduceChains === undefined ? undefined : reduceOf!);
|
|
320
345
|
// #251: on its support frames every mapped bone from the hip down was kept at its target, so only the hip's tolerance moves it.
|
|
321
346
|
const hip = a.reduceHip ?? 0;
|
|
322
347
|
if (!drift.some((d) => d.bone === l.bone)) {
|
|
@@ -324,7 +349,9 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
324
349
|
const side = lb?.r.ankle === ankle ? 'r' : lb?.l.ankle === ankle ? 'l' : null;
|
|
325
350
|
const legs = (u: number) => (side === null || r.rigLeg === null ? null : u / r.rigLeg[side]);
|
|
326
351
|
const held = support === undefined ? {} : { support: side === null || support[side].size === 0 ? null : { reduce: hip, units: l.tolerance + hip, legLengths: legs(l.tolerance + hip) } };
|
|
327
|
-
|
|
352
|
+
// #283: under a per-chain --reduce, the tolerance its leg was reduced within.
|
|
353
|
+
const leg = a.reduceChains === undefined ? {} : { legTolerance: legTolerance(rest, r, ankle, reduceOf!) };
|
|
354
|
+
drift.push({ bone: l.bone, side, plant: l.tolerance, reduce: by, units: l.tolerance + by, legLengths: legs(l.tolerance + by), ...held, ...leg });
|
|
328
355
|
}
|
|
329
356
|
return { ...l, tolerance: l.tolerance + (l.on === 'support' ? hip : by) };
|
|
330
357
|
});
|
|
@@ -355,7 +382,7 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
355
382
|
|
|
356
383
|
const lengths = new Map(doc.bones.map((b) => [b.name, b.length ?? 0]));
|
|
357
384
|
const hips = seq.cleaned?.params.hipsInferred;
|
|
358
|
-
const posture = measurePosture(posed, lengths, map, r, frames, a.minScore, inferredSource(seq.cleaned), hips?.by === 'measured' ? hips : null);
|
|
385
|
+
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);
|
|
359
386
|
if (posture !== null) for (const l of postureLines(posture)) say(` POSTURE ${l}`);
|
|
360
387
|
const legBones = legBonesOf(map);
|
|
361
388
|
const feetReading = legBones === null ? null : measureFeet(posed, lengths, r, legBones);
|
|
@@ -387,7 +414,7 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
387
414
|
|
|
388
415
|
assertReportInputs([rigSnapshot,mapSnapshot,poseSnapshot]);
|
|
389
416
|
// Every retarget option as resolved: the report's `options`, and with the name the build record's `flags` (#270).
|
|
390
|
-
const options = { minScore: a.minScore, flip: a.flip, short: a.short, bends: a.bends, bendTolerance: a.bendTolerance, switchTolerance: a.switchTolerance, switchMin: a.switchMin, switchAt: a.switchAt, profile: a.profile, legScale: a.legScale ?? 'off', plantFeet: a.plantFeet === true, stance: a.stance ?? 'off', floor: a.floor ?? null, raisedByHead: a.raisedByHead === true, idle: idleName, carryIdle: a.carryIdle === true, merge: a.merge ?? null, reduce: a.reduce ?? 'off', reduceHip: a.reduceHip ?? 'off', ...(a.reduceKeepPlanted === true ? { reduceKeepPlanted: true } : {}) };
|
|
417
|
+
const options = { minScore: a.minScore, flip: a.flip, short: a.short, bends: a.bends, bendTolerance: a.bendTolerance, switchTolerance: a.switchTolerance, switchMin: a.switchMin, switchAt: a.switchAt, profile: a.profile, legScale: a.legScale ?? 'off', plantFeet: a.plantFeet === true, stance: a.stance ?? 'off', floor: a.floor ?? null, raisedByHead: a.raisedByHead === true, idle: idleName, carryIdle: a.carryIdle === true, merge: a.merge ?? null, reduce: a.reduce ?? 'off', ...(a.reduceChains === undefined ? {} : { reduceChains: a.reduceChains }), reduceHip: a.reduceHip ?? 'off', ...(a.reduceKeepPlanted === true ? { reduceKeepPlanted: true } : {}), ...(a.drawOrder !== undefined ? { drawOrder: a.drawOrder } : {}) };
|
|
391
418
|
const reportText = writeReport({
|
|
392
419
|
spec: 'rig-pose-report/1',
|
|
393
420
|
rigc,
|
|
@@ -412,9 +439,12 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
412
439
|
seams: seamRead.seams,
|
|
413
440
|
seamPairs: seamRead.seamPairs,
|
|
414
441
|
switches: r.switches,
|
|
442
|
+
switchRuns: r.switchRuns,
|
|
415
443
|
uncovered,
|
|
416
444
|
turns: r.turns,
|
|
417
445
|
reach: r.reach,
|
|
446
|
+
// Only when the map has a reach chain (#282): a map without one writes the report it always has.
|
|
447
|
+
...(r.reachOrigin.length > 0 ? { reachOrigin: r.reachOrigin } : {}),
|
|
418
448
|
range: r.range,
|
|
419
449
|
artRange: r.artRange,
|
|
420
450
|
raised: r.raised,
|
|
@@ -444,13 +474,17 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
444
474
|
legScale: r.legScale.map(({ scale: _scale, asked: _asked, picture: _picture, ...rest }) => rest),
|
|
445
475
|
pivot: r.pivot,
|
|
446
476
|
pelvis: r.pelvis,
|
|
477
|
+
// Only when the sequence carries depth (#281): a sequence without it writes the report it always has.
|
|
478
|
+
...(layers !== null ? { layers: layers.map((l, frame) => ({ frame, order: l.order, depth: l.depth })) } : {}),
|
|
479
|
+
// Only with --draw-order depth (#304).
|
|
480
|
+
...(drawOrder !== null ? { drawOrder: { source: 'depth', pairs: drawOrder.pairs, keys: drawOrder.keys, spans: drawOrder.spans } } : {}),
|
|
447
481
|
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 },
|
|
448
482
|
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 },
|
|
449
483
|
shear,
|
|
450
484
|
idleDropped: idle === null ? null : idle.dropped,
|
|
451
485
|
idleKeptOut: idle === null ? null : idle.keptOut,
|
|
452
486
|
merged: fragment.map((tr) => ({ track: trackLabel(tr, groups), keys: tr.keys.length })),
|
|
453
|
-
reduce: reduced === null ? null : { tolerance: a.reduce, hipTolerance: a.reduceHip ?? null, rows: reduced.rows, ankleDrift: drift },
|
|
487
|
+
reduce: reduced === null ? null : { tolerance: a.reduce, ...(a.reduceChains === undefined ? {} : { chains: a.reduceChains }), hipTolerance: a.reduceHip ?? null, rows: reduced.rows, ankleDrift: drift },
|
|
454
488
|
}, map, { frameTimes: r.times }, f.meshes !== null && doc.slots.length > 0 ? reportRenderDomain(JSON.parse(readFileSync(join(f.frames, '..', 'frames.json'), 'utf8')), a.name) : seamRead.frames === null ? null : reportRenderDomain(seamRead.frames, a.name), {rig:rigSnapshot.identity,map:mapSnapshot.identity,pose:poseSnapshot.identity});
|
|
455
489
|
if (a.build !== undefined) {
|
|
456
490
|
// Built again where it will live, not copied: rigc's page paths are relative to the build (#18).
|
|
@@ -622,7 +656,7 @@ const spanText = (x: [number, number]) => (x[0] === x[1] ? `${x[0]}` : `${x[0]}
|
|
|
622
656
|
* aim and length it lands on (`reduceUnderScale`). With none, this is #32's
|
|
623
657
|
* reduction, unchanged.
|
|
624
658
|
*/
|
|
625
|
-
function reduceTracks(r: ReturnType<typeof retarget>, map: JointMap, rest: Rest, tolerance: number, hipTolerance: number | undefined, keepPlanted: boolean): Reduced {
|
|
659
|
+
function reduceTracks(r: ReturnType<typeof retarget>, map: JointMap, rest: Rest, tolerance: number, hipTolerance: number | undefined, keepPlanted: boolean, tolOf: (bone: string) => number = () => tolerance): Reduced {
|
|
626
660
|
const n = r.times.length;
|
|
627
661
|
const keep = new Map<string, Set<number>>();
|
|
628
662
|
const add = (bone: string, ...frames: number[]) => {
|
|
@@ -661,7 +695,9 @@ function reduceTracks(r: ReturnType<typeof retarget>, map: JointMap, rest: Rest,
|
|
|
661
695
|
return false;
|
|
662
696
|
};
|
|
663
697
|
const dense = new Map([...rotate].map(([b, tr]) => [b, tr.keys.map((k) => k.v[0]!)]));
|
|
664
|
-
|
|
698
|
+
// #283: a bone whose chain --reduce names carries its own tolerance; every other takes the run's.
|
|
699
|
+
const own = (b: string): { tolerance?: number } => (tolOf(b) === tolerance ? {} : { tolerance: tolOf(b) });
|
|
700
|
+
const bones: ReduceBone[] = [...rotate.values()].filter((tr) => !under(tr.bone)).map((tr) => ({ bone: tr.bone, above: above(tr.bone), dense: dense.get(tr.bone)!, keep: keep.get(tr.bone) ?? new Set<number>(), ...own(tr.bone) }));
|
|
665
701
|
const hipTrack = r.origin === null || hipTolerance === undefined ? undefined : r.animation.tracks.find((tr): tr is MotionTrack => 'bone' in tr && tr.property === 'translate' && tr.bone === r.origin!.bone && tr.keys.every((k) => k.ease === undefined));
|
|
666
702
|
const parent = hipTrack === undefined ? null : rest.get(hipTrack.bone)!.parent;
|
|
667
703
|
const pm = parent === null ? null : rest.get(parent)!;
|
|
@@ -754,6 +790,7 @@ function reduceTracks(r: ReturnType<typeof retarget>, map: JointMap, rest: Rest,
|
|
|
754
790
|
asked: r.scales.get(b) ?? new Array<number>(n).fill(1),
|
|
755
791
|
dense: dense.get(b)!,
|
|
756
792
|
keep: keepOf(b),
|
|
793
|
+
...own(b),
|
|
757
794
|
};
|
|
758
795
|
});
|
|
759
796
|
const red = reduceUnderScale(r.times, given, tolerance, SCALE_REDUCE);
|
|
@@ -778,7 +815,7 @@ function reduceTracks(r: ReturnType<typeof retarget>, map: JointMap, rest: Rest,
|
|
|
778
815
|
* world aim is within the angle tolerance of its dense one, top-down, so the
|
|
779
816
|
* segments' errors are each bounded and the sum bounds the ankle's.
|
|
780
817
|
*/
|
|
781
|
-
export function footReduce(rest: Rest, r: ReturnType<typeof retarget>, ankle: string, tolerance: number, hipTolerance: number | undefined): number {
|
|
818
|
+
export function footReduce(rest: Rest, r: ReturnType<typeof retarget>, ankle: string, tolerance: number, hipTolerance: number | undefined, tolOf?: (bone: string) => number): number {
|
|
782
819
|
const top = r.origin?.bone ?? null;
|
|
783
820
|
let sMax = 1;
|
|
784
821
|
for (const s of r.scales.values()) for (const v of s) sMax = Math.max(sMax, v);
|
|
@@ -790,7 +827,8 @@ export function footReduce(rest: Rest, r: ReturnType<typeof retarget>, ankle: st
|
|
|
790
827
|
if (p === null) break;
|
|
791
828
|
lever += Math.hypot(x.worldX - p.worldX, x.worldY - p.worldY);
|
|
792
829
|
}
|
|
793
|
-
|
|
830
|
+
const deg = tolOf === undefined ? tolerance : legTolerance(rest, r, ankle, tolOf);
|
|
831
|
+
return (hipTolerance ?? 0) + lever * sMax * ((deg * Math.PI) / 180 + SCALE_REDUCE);
|
|
794
832
|
}
|
|
795
833
|
|
|
796
834
|
function spans(ts: number[], step: number): string {
|
|
@@ -916,6 +954,48 @@ function loweredLine(lowered: readonly Lowered[]): string {
|
|
|
916
954
|
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(', ')}`;
|
|
917
955
|
}
|
|
918
956
|
|
|
957
|
+
/**
|
|
958
|
+
* The support-signal lines (rig-c/pose#280). On a clip read as frontal: two
|
|
959
|
+
* lines — how often the ankle's vertical speed contradicts ankle height against
|
|
960
|
+
* `FRONTAL_DISAGREE`, and where the stride shows (in depth) — then one line per
|
|
961
|
+
* support span (a stride) naming the signal that decided its frames and what
|
|
962
|
+
* ankle height alone would have read. On any other clip ankle height decided as
|
|
963
|
+
* before and nothing is printed: the output of a clip the second signal does not
|
|
964
|
+
* decide stays as it was, in every mode (`Feet.signal` still carries the figures).
|
|
965
|
+
*/
|
|
966
|
+
export function signalLines(sig: SupportSignal, supports: Feet['supports']): string[] {
|
|
967
|
+
const span = (x: [number, number]) => (x[0] === x[1] ? `${x[0]}` : `${x[0]}–${x[1]}`);
|
|
968
|
+
const n = Math.round(FRONTAL_DISAGREE * 100);
|
|
969
|
+
const pct = sig.read === 0 ? 0 : (100 * sig.contradict.length) / sig.read;
|
|
970
|
+
if (!sig.frontal) return [];
|
|
971
|
+
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`;
|
|
972
|
+
const contra = `${sig.contradict.length} of ${sig.read} frame(s) (${pct.toFixed(1)} %`;
|
|
973
|
+
const out = [
|
|
974
|
+
`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`,
|
|
975
|
+
`support: read as a frontal view — ankles move more in y than x (${ranges}); ankle height only where neither foot moves with the ground`,
|
|
976
|
+
];
|
|
977
|
+
for (const sp of supports) {
|
|
978
|
+
let speed = 0;
|
|
979
|
+
let height = 0;
|
|
980
|
+
let both = 0;
|
|
981
|
+
let differ = 0;
|
|
982
|
+
const was = new Set<string>();
|
|
983
|
+
for (let i = sp.frames[0]; i <= sp.frames[1]; i++) {
|
|
984
|
+
if (sig.by[i] === 'speed') speed++;
|
|
985
|
+
else if (sig.by[i] === 'height') height++;
|
|
986
|
+
if (sig.contact[i] === 'both') both++;
|
|
987
|
+
if (sig.height[i] !== sp.side) {
|
|
988
|
+
differ++;
|
|
989
|
+
was.add(sig.height[i] ?? 'none');
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
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)`;
|
|
993
|
+
const alone = differ === 0 ? 'ankle height alone reads the same' : `ankle height alone would read ${[...was].join(' or ')} on ${differ} of them`;
|
|
994
|
+
out.push(`stride ${sp.side} ${span(sp.frames)}: ${by} (both feet down on ${both}); ${alone}`);
|
|
995
|
+
}
|
|
996
|
+
return out;
|
|
997
|
+
}
|
|
998
|
+
|
|
919
999
|
/**
|
|
920
1000
|
* The in-place line without --plant-feet (rig-c/pose#242): what the declaration still changes, and the one thing it
|
|
921
1001
|
* cannot without planted feet — the loop closure, which spreads back the planted hip's drift. `drift` is the
|
|
@@ -1052,6 +1132,21 @@ export function legScaleLine(l: LegScale, times: readonly number[], legLength: R
|
|
|
1052
1132
|
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}`;
|
|
1053
1133
|
}
|
|
1054
1134
|
|
|
1135
|
+
/**
|
|
1136
|
+
* rig-c/pose#287: when `--leg-scale`'s range clamps no frame on any leg segment
|
|
1137
|
+
* (no run under the floor, none over the ceiling, no supporting frame scaled past
|
|
1138
|
+
* the floor), the clause naming the range and the picture's own spread across
|
|
1139
|
+
* every segment, so a reader learns the range never bound and what floor would.
|
|
1140
|
+
* Null when any frame was clamped, or with no segment scaled.
|
|
1141
|
+
*/
|
|
1142
|
+
export function legScaleNoClamp(ls: readonly LegScale[]): string | null {
|
|
1143
|
+
if (ls.length === 0 || ls.some((l) => l.low.length > 0 || l.high.length > 0 || (l.support ?? []).length > 0)) return null;
|
|
1144
|
+
const [floor, ceiling] = ls[0]!.limits;
|
|
1145
|
+
const min = Math.min(...ls.map((l) => l.ratio.min));
|
|
1146
|
+
const max = Math.max(...ls.map((l) => l.ratio.max));
|
|
1147
|
+
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`;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1055
1150
|
/** 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. */
|
|
1056
1151
|
export function supportScaleLine(l: LegScale, s: SupportScaleRun, times: readonly number[]): string {
|
|
1057
1152
|
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)`);
|
|
@@ -1136,11 +1231,6 @@ export function defaultReportPath(out: string): string {
|
|
|
1136
1231
|
return `${out.endsWith('.json') ? out.slice(0, -'.json'.length) : out}.report.json`;
|
|
1137
1232
|
}
|
|
1138
1233
|
|
|
1139
|
-
/** rig-c/pose#239: the line a directory this run made prints — the directory, and the paths that named it. */
|
|
1140
|
-
export function madeLine(dir: string, flags: readonly string[]): string {
|
|
1141
|
-
return ` made directory ${dir} — it did not exist; ${flags.join(' and ')} ${flags.length === 1 ? 'is' : 'are'} written into it`;
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
1234
|
/** rig-c/pose#239: the report's `wrote` line — whether it was the default, and the mtime of the report it replaced. */
|
|
1145
1235
|
export function reportLine(report: string, byDefault: boolean, previous: Date | null): string {
|
|
1146
1236
|
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()}`}`;
|
|
@@ -1234,6 +1324,8 @@ export interface AnkleDrift {
|
|
|
1234
1324
|
* Absent without the flag.
|
|
1235
1325
|
*/
|
|
1236
1326
|
support?: { reduce: number; units: number; legLengths: number | null } | null;
|
|
1327
|
+
/** rig-c/pose#283: under a per-chain --reduce, the largest tolerance a bone from the hip down to it was reduced within (degrees). */
|
|
1328
|
+
legTolerance?: number;
|
|
1237
1329
|
}
|
|
1238
1330
|
|
|
1239
1331
|
/** #251: each side's support frames, from the spans --plant-feet read. */
|
|
@@ -1245,11 +1337,46 @@ function supportFrames(supports: ReadonlyArray<{ side: 'r' | 'l'; frames: [numbe
|
|
|
1245
1337
|
|
|
1246
1338
|
/** The drift line: the allowance on one frame, and what it lets a planted ankle slide between two. */
|
|
1247
1339
|
export function ankleDriftLine(d: AnkleDrift): string {
|
|
1340
|
+
const chainDeg = d.legTolerance;
|
|
1248
1341
|
const legs = (v: number) => (d.legLengths === null ? '' : ` = ${(d.legLengths * v / d.units).toPrecision(3)} leg lengths`);
|
|
1249
1342
|
const line = `ankle drift allowed: bone "${d.bone}" ${d.units.toPrecision(3)} units${legs(d.units)} on any one frame (plant ${d.plant.toPrecision(3)} + reduce ${d.reduce.toPrecision(3)}); between two frames up to ${(2 * d.units).toPrecision(3)} units${legs(2 * d.units)}`;
|
|
1250
|
-
if (d.support === undefined) return `${line} — a tighter slide bar needs a smaller --reduce`;
|
|
1343
|
+
if (d.support === undefined) return `${line} — ${chainDeg === undefined ? 'a tighter slide bar needs a smaller --reduce' : `its leg reduced within ${chainDeg}° (--reduce <deg>,<chain>=<deg>); a tighter slide bar needs a smaller one on its chain`}`;
|
|
1251
1344
|
if (d.support === null) return `${line}; its side never supports, so --reduce-keep-planted keeps no frame of it`;
|
|
1252
1345
|
const s = d.support;
|
|
1253
1346
|
const sLegs = (v: number) => (s.legLengths === null ? '' : ` = ${(s.legLengths * v / s.units).toPrecision(3)} leg lengths`);
|
|
1254
1347
|
return `${line} off its support frames; on them, kept dense (--reduce-keep-planted), ${s.units.toPrecision(3)} units${sLegs(s.units)} (plant ${d.plant.toPrecision(3)} + hip ${s.reduce.toPrecision(3)}), between two frames of a support span up to ${(2 * s.units).toPrecision(3)} units${sLegs(2 * s.units)}`;
|
|
1255
1348
|
}
|
|
1349
|
+
|
|
1350
|
+
/** rig-c/pose#283: the largest tolerance any bone from `ankle` up to the legs' top was reduced within. */
|
|
1351
|
+
export function legTolerance(rest: Rest, r: ReturnType<typeof retarget>, ankle: string, tolOf: (bone: string) => number): number {
|
|
1352
|
+
const top = r.origin?.bone ?? null;
|
|
1353
|
+
let deg = tolOf(ankle);
|
|
1354
|
+
for (let b: string | null = rest.get(ankle)!.parent; b !== null && b !== top; b = rest.get(b)!.parent) deg = Math.max(deg, tolOf(b));
|
|
1355
|
+
return deg;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
/**
|
|
1359
|
+
* rig-c/pose#283: the reduction's tolerance per bone. Every bone takes `tolerance`
|
|
1360
|
+
* but a bone of a chain `chains` names, which takes that chain's (the smallest,
|
|
1361
|
+
* when two named chains list it). A name the map has no chain for is refused by
|
|
1362
|
+
* name, beside the chains it has.
|
|
1363
|
+
*/
|
|
1364
|
+
export function reduceTolerances(map: JointMap, tolerance: number, chains: Record<string, number> | undefined, mapPath: string): (bone: string) => number {
|
|
1365
|
+
if (chains === undefined) return () => tolerance;
|
|
1366
|
+
const names = map.chains.map((c) => c.name);
|
|
1367
|
+
const missing = Object.keys(chains).filter((c) => !names.includes(c));
|
|
1368
|
+
if (missing.length > 0) throw new Refusal(missing.map((c) => ({ code: 'POSE_REDUCE', text: `--reduce names chain "${c}", which ${mapPath} does not have; its chains are ${names.map((x) => `"${x}"`).join(', ')}` })));
|
|
1369
|
+
const by = new Map<string, number>();
|
|
1370
|
+
for (const c of map.chains) {
|
|
1371
|
+
const t = Object.hasOwn(chains, c.name) ? chains[c.name] : undefined;
|
|
1372
|
+
if (t === undefined) continue;
|
|
1373
|
+
for (const b of c.bones) by.set(b, Math.min(by.get(b) ?? t, t));
|
|
1374
|
+
}
|
|
1375
|
+
return (bone) => by.get(bone) ?? tolerance;
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
/** rig-c/pose#283: the per-chain tolerances as the run and REDUCE lines print them; empty without. */
|
|
1379
|
+
export function chainsText(chains: Record<string, number> | undefined): string {
|
|
1380
|
+
if (chains === undefined) return '';
|
|
1381
|
+
return ` (${Object.entries(chains).map(([c, t]) => `chain "${c}" ${t}°`).join(', ')})`;
|
|
1382
|
+
}
|
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
|
@@ -180,8 +180,28 @@ export interface Uncovered {
|
|
|
180
180
|
times: number[];
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
/** One run of frames (rig-c/pose#279): first and last frame, how many, and the seconds the minimum span measured it by (first frame's time to the next frame's, the animation's end for the last). */
|
|
184
|
+
export interface FrameRun {
|
|
185
|
+
frames: [number, number];
|
|
186
|
+
count: number;
|
|
187
|
+
seconds: number;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* rig-c/pose#279: per set selected by angle (not `phase`), the runs of frames on which it was the nearest set within
|
|
192
|
+
* the tolerance (inside its `frames` window, before bridging, hysteresis and the minimum span), and the runs the
|
|
193
|
+
* minimum span dropped — shorter than `switchMin` where they stood — on frames the set did not show in the end.
|
|
194
|
+
*/
|
|
195
|
+
export interface SetRuns {
|
|
196
|
+
set: string;
|
|
197
|
+
nearest: FrameRun[];
|
|
198
|
+
suppressed: FrameRun[];
|
|
199
|
+
}
|
|
200
|
+
|
|
183
201
|
export interface Switches {
|
|
184
202
|
switches: Switch[];
|
|
203
|
+
/** rig-c/pose#279: one per set without a `phase`, in the map's order. */
|
|
204
|
+
runs: SetRuns[];
|
|
185
205
|
/** The `attachment` tracks, one per slot whose picture changes. */
|
|
186
206
|
slotTracks: SlotTrack[];
|
|
187
207
|
/** The offsets' `translate` tracks, one per bone a shown set offsets. */
|
|
@@ -490,6 +510,11 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
490
510
|
const distances = setDistances(map, targets, n, opts.shortInfo);
|
|
491
511
|
const hyst = opts.switchHysteresis ?? SWITCH_LEAVE_HYSTERESIS;
|
|
492
512
|
const switches: Switch[] = [];
|
|
513
|
+
// rig-c/pose#279: per angle set, the frames it was nearest on and the frames the minimum span dropped it from.
|
|
514
|
+
const nearestOn = new Map<string, boolean[]>();
|
|
515
|
+
const droppedOn = new Map<string, boolean[]>();
|
|
516
|
+
const runOf = (i: number, j: number): FrameRun => ({ frames: [i, j], count: j - i + 1, seconds: r3((j + 1 < n ? times[j + 1]! : duration) - times[i]!) });
|
|
517
|
+
const runsOfFlags = (flag: readonly boolean[]): FrameRun[] => runsOf(flag, 0).map(([i, j]) => runOf(i, j));
|
|
493
518
|
// 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
519
|
const supportAt: Array<Side | null> = Array.from({ length: n }, () => null);
|
|
495
520
|
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;
|
|
@@ -546,6 +571,10 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
546
571
|
}
|
|
547
572
|
pick.push(best);
|
|
548
573
|
}
|
|
574
|
+
for (const a of group) {
|
|
575
|
+
nearestOn.set(a.name, pick.map((p) => p === a));
|
|
576
|
+
droppedOn.set(a.name, Array.from({ length: n }, () => false));
|
|
577
|
+
}
|
|
549
578
|
const bridgeSame = (arr: Array<AlternateSet | null>) => {
|
|
550
579
|
for (let i = 0; i < n;) {
|
|
551
580
|
const a = arr[i];
|
|
@@ -604,7 +633,13 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
604
633
|
let j = i;
|
|
605
634
|
while (j + 1 < n && pick[j + 1] === a) j++;
|
|
606
635
|
const to = j + 1 < n ? times[j + 1]! : duration;
|
|
607
|
-
if (a !== null && a !== undefined && to - times[i]! < opts.switchMin - 1e-9)
|
|
636
|
+
if (a !== null && a !== undefined && to - times[i]! < opts.switchMin - 1e-9) {
|
|
637
|
+
const dropped = droppedOn.get(a.name)!;
|
|
638
|
+
for (let k = i; k <= j; k++) {
|
|
639
|
+
pick[k] = null;
|
|
640
|
+
dropped[k] = true;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
608
643
|
i = j + 1;
|
|
609
644
|
}
|
|
610
645
|
if (hyst > 0) {
|
|
@@ -658,8 +693,14 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
658
693
|
}
|
|
659
694
|
if (dropped) bridgeSame(pick);
|
|
660
695
|
}
|
|
696
|
+
// rig-c/pose#279: a dropped frame the set came back to (hysteresis, bridging) was not suppressed.
|
|
697
|
+
for (const a of group) {
|
|
698
|
+
const dropped = droppedOn.get(a.name)!;
|
|
699
|
+
for (let k = 0; k < n; k++) if (pick[k] === a) dropped[k] = false;
|
|
700
|
+
}
|
|
661
701
|
spans(pick, opts);
|
|
662
702
|
}
|
|
703
|
+
const runs: SetRuns[] = map.alternates.filter((a) => nearestOn.has(a.name)).map((a) => ({ set: a.name, nearest: runsOfFlags(nearestOn.get(a.name)!), suppressed: runsOfFlags(droppedOn.get(a.name)!) }));
|
|
663
704
|
const order = (name: string) => map.alternates.findIndex((a) => a.name === name);
|
|
664
705
|
switches.sort((x, y) => x.frames[0] - y.frames[0] || order(x.set) - order(y.set));
|
|
665
706
|
|
|
@@ -1094,7 +1135,7 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
1094
1135
|
for (let k = 0; k + 1 < keys.length; k++) keys[k]!.ease = 'stepped';
|
|
1095
1136
|
offsetTracks.push({ bone: b, property: 'translate', keys });
|
|
1096
1137
|
}
|
|
1097
|
-
return { switches, slotTracks, offsetTracks, shown, distances, chainHolds };
|
|
1138
|
+
return { switches, runs, slotTracks, offsetTracks, shown, distances, chainHolds };
|
|
1098
1139
|
}
|
|
1099
1140
|
|
|
1100
1141
|
/** Per chain, the frames on which a switch of a set depicting it shows: its art was painted for that pose. */
|
|
@@ -1281,6 +1322,27 @@ export function phaseLines(map: JointMap, switches: readonly Switch[]): string[]
|
|
|
1281
1322
|
return out;
|
|
1282
1323
|
}
|
|
1283
1324
|
|
|
1325
|
+
/** How many runs a `SWITCH-MIN` line lists before it points at the report for the rest. */
|
|
1326
|
+
export const SWITCH_MIN_LISTED = 4;
|
|
1327
|
+
|
|
1328
|
+
/**
|
|
1329
|
+
* rig-c/pose#279: one `SWITCH-MIN` line per set the minimum span held back — the runs it dropped (frames, count,
|
|
1330
|
+
* seconds), and the set's nearest runs (how many, and the longest) — so a set that matched but was held back reads
|
|
1331
|
+
* apart from one that never matched. A set with nothing suppressed prints no line.
|
|
1332
|
+
*/
|
|
1333
|
+
export function switchMinLines(runs: readonly SetRuns[], switchMin: number): string[] {
|
|
1334
|
+
const runText = (r: FrameRun) => `${r.count === 1 ? `frame ${r.frames[0]}` : `frames ${r.frames[0]}–${r.frames[1]}`} (${r.count} frame${r.count === 1 ? '' : 's'}, ${r.seconds.toFixed(3)}s)`;
|
|
1335
|
+
const out: string[] = [];
|
|
1336
|
+
for (const s of runs) {
|
|
1337
|
+
if (s.suppressed.length === 0) continue;
|
|
1338
|
+
const listed = s.suppressed.slice(0, SWITCH_MIN_LISTED).map(runText).join(', ');
|
|
1339
|
+
const more = s.suppressed.length > SWITCH_MIN_LISTED ? `, and ${s.suppressed.length - SWITCH_MIN_LISTED} more (the report's switchRuns lists every one)` : '';
|
|
1340
|
+
const longest = s.nearest.reduce((m, r) => (r.count > m.count ? r : m), s.nearest[0]!);
|
|
1341
|
+
out.push(`set "${s.set}": ${s.suppressed.length} run(s) shorter than --switch-min ${switchMin}s not shown: ${listed}${more}; nearest on ${s.nearest.length} run(s), the longest ${runText(longest)}`);
|
|
1342
|
+
}
|
|
1343
|
+
return out;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1284
1346
|
const spd = (v: number) => `${v.toFixed(1)}°/frame`;
|
|
1285
1347
|
const win = (w: [number, number]) => (w[0] === w[1] ? `window frame ${w[0]}` : `window frames ${w[0]}–${w[1]}`);
|
|
1286
1348
|
function moveText(m: Move): string {
|