@rig-c/pose 0.40.0 → 0.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/package.json +1 -1
- package/src/draworder.ts +97 -10
- package/src/outdir.ts +5 -5
- package/src/pelvis.ts +205 -3
- package/src/reduce.ts +19 -1
- package/src/run.ts +89 -6
package/README.md
CHANGED
|
@@ -392,6 +392,7 @@ The retarget writes one key per pose frame on every mapped bone. That is faithfu
|
|
|
392
392
|
- A mapped bone under a scaled bone (keyed through its parent's posed matrix by `--leg-scale`) is solved again through its parent as reduced. Its rotate is then thinned with the error read as the world aim it lands on, and its `scalex` with the error read as the world length it lands on. Its world error is its own fit error, as down any other chain.
|
|
393
393
|
- Each ankle is held to the plant's tolerance plus the most the reduction may move it: the hip's tolerance (with `--reduce-hip`), plus the leg from the hip to the ankle (each bone's setup offset from its parent and the last bone's length) times the largest scale asked, times the angle tolerance in radians plus 5e-5. The ankle's `LAND` line prints that tolerance, and at the end of the run `REDUCE ankle drift allowed: bone "…:tip" X units = Y leg lengths on any one frame` prints the same figure, split `(plant … + reduce …)` (`reduce.ankleDrift` in the report, `{ bone, side, plant, reduce, units, legLengths }`, the row's `units` the landing's `tolerance` itself), with the slide between two frames it lets through (2X) — so a slide bar is read against the reduction before it is run, not after ([#240](https://github.com/rig-c/pose/issues/240)). On the wave with `--leg-scale 0.8,1.05 --plant-feet --reduce 0.5 --reduce-hip 0.5`, that is 1.97 units = 0.0132 leg lengths a frame, 0.0263 between two.
|
|
394
394
|
- **Per chain: `--reduce <deg>,<chain>=<deg>,…`** ([#283](https://github.com/rig-c/pose/issues/283)). The first item is every chain's tolerance; each `<chain>=<deg>` after it gives a chain the map names its own, on each of its bones (the smaller, when two named chains list one bone). One tolerance on every bone trades the ankle's slide against the trunk's jitter; this holds the legs tight while the trunk is smoothed. A chain the map does not have is refused by name (`POSE_REDUCE`, beside the chains it has), and a malformed item, a tolerance outside (0, 180) or a chain named twice by the command line. Each bone's `LAND` line holds it to 0.01° plus its chain's tolerance, #116's trunk smoothing keeps each bone in its own, and each ankle's allowance above is taken at the largest tolerance of the bones from the hip down to it. The run line and the `REDUCE` total name each chain's tolerance, each bone reduced within its chain's says so (`— within its chain's 0.2°`), and each `ankle drift allowed` line names the tolerance its leg was reduced within. The report carries them as `reduce.chains` and `options.reduceChains` (`{ chain: deg }`) and `reduce.ankleDrift[].legTolerance`, written only when given; without them a run's bytes are unchanged. Measured on the tests' synthetic travelling walk (`examples/walk`'s cycle three times, 72 frames, the swinging ankle 12 px fore and aft, on the wave's rig with the walk's map and its trunk given `reach`, `--plant-feet --leg-scale 0.8,1.05`), against slide (the FEET worst horizontal drift) ≤ 0.005 leg lengths and jitter (the worst `JITTER` ratio) ≤ 1.1: `--reduce 0.2` gives 0.0042 and 1.62, `--reduce 3` 0.0547 and 1.01; with `--reduce-keep-planted`, 0.0000 and 1.62 at 0.2, 0.0000 and 1.42 at 3 (the supporting leg's seam where its kept frames meet its reduced ones); `--reduce 3,leg_r=0.2,leg_l=0.2` gives 0.0042 and 0.99.
|
|
395
|
+
- **A parent held to its tightest child** ([#320](https://github.com/rig-c/pose/issues/320)). Under a per-chain `--reduce`, every mapped bone above a planted or aimed chain the command names is reduced within that chain's tolerance when its own is looser (the tightest such chain, when several hang below it). A chain is planted when it carries an ankle the landing check holds, aimed when the map gives it an `aim` other than `"x"`; a bent chain (an arm, a leg with no ankle check) holds nothing above it. The lever: a turn error θ on a bone moves a chain below it by θ·L (L the distance from the bone's origin to the chain's first bone) and turns it by θ. The reduction refits the chain against the turn, so its aim stays its own, but the move it cannot undo, and the ankle allowance counts L at the leg's own degrees — θ·L ≤ θ_leg·L is θ ≤ θ_leg whatever L is. An aimed chain is refitted, and on the trunk smoothed, against its parent's error, which held to the chain's degrees never asks it to absorb more than its own tolerance. Each held bone prints `REDUCE held: bone "hip" (chain "pelvis") reduced within 0.2°, not 3° — for chain "leg_r" (a planted ankle; L = 15 units to bone "leg_r0"), …`, its `REDUCE` row ends `— held to 0.2° for chain "leg_r", chain "leg_l" below it`, its `LAND` line takes 0.01° plus the held tolerance, and the report carries `reduce.held` (`[{ bone, chain, from, to, for: [{ chain, by, bone, lever }] }]`, `by` is `ankle` or `aim`), written only when a bone was held. A plain `--reduce` names no chain, so nothing is held and its bytes are unchanged. #116's trunk smoothing is kept only where it leaves every bone it manages within that bone's own tolerance; past it, the fitted keys stand and the run prints `REDUCE smoothing (rig-c/pose#116) from bone "chest" down not kept: …` (`reduce.unsmoothed`, `[{ root, bone, worst, tolerance }]`, written only then). Measured on the tests' synthetic walk with a drawn pelvis tilt (`examples/walk/make-pelvis.ts`, drawn, read by the silhouette, cleaned with `--loop`, on the wave's rig with the walk's map and a pelvis chain on `hip`, `--plant-feet`): `--reduce 3,leg_r=0.2,leg_l=0.2` without the hold missed both ankles (0.697 and 0.677 units against an allowance of 0.628); with it, bone `hip` is held to 0.2° and the ankles land at 0.248 and 0.232, the head at 2.63° within 3.01°, the same motion bytes as a hand `pelvis=0.2` entry.
|
|
395
396
|
- **`--reduce-keep-planted`** (only with `--reduce` and `--plant-feet`, refused by name otherwise; [#251](https://github.com/rig-c/pose/issues/251)) adds every frame of each support span to the frames kept regardless, on every mapped bone from the hip down to that side's ankle (rotate, and `scalex` where scaled). A kept frame is keyed at its target, so on its support frames the planted ankle lands as dense but for the hip: its `LAND` line there is `ankle on its support frames (kept dense)`, held to the plant's tolerance plus `--reduce-hip`'s (0 without it), and off them `ankle off its support frames`, held to the allowance above. `reduce.ankleDrift[].support` carries the tighter figure (`{ reduce, units, legLengths }`, `reduce` being the hip's share; `null` for a side that never supports). The tolerance has no separate rounding term: on the tests' synthetic walk (72 frames, six support spans) the supporting ankle on its support frames sits 1.22e-3 units from the dense run's, against the plant's 0.0253. Measured on that walk with `--reduce 1`, the FEET slide (the worst horizontal drift of a supporting ankle over a support span) goes from 0.0226 leg lengths to 0.00002, the dense run's; with `--reduce-hip 0.5` beside it, 0.0048, since the hip is not kept. The price is the keys: the supporting leg keeps one per support frame (the walk's four leg rotate tracks, 47 → 135 keys).
|
|
396
397
|
- When `--leg-scale` is active, each trunk bone (`chest` and `head`, not an ancestor of any scaled bone) has its kept knots and Bézier handles smoothed within `deg` of its dense world aim to minimise the mean absolute second difference of its evaluated local angle and its child's compensated local angle, holding any limb children within 0.03° of their fitted world aim (rig-c/pose#116).
|
|
397
398
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/pose",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.41.0",
|
|
4
4
|
"description": "Puts motion from a body-18 pose sequence onto a rig built with rig-c: bone keys in rig-c's motion format, checked through rig-c's gate before anything is written. A CLI for agents that cannot see the rig.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/draworder.ts
CHANGED
|
@@ -19,7 +19,13 @@
|
|
|
19
19
|
* together they fill one unbroken run of the setup order. A slot of anything
|
|
20
20
|
* else drawn between them would be re-layered by the swap — that is changing
|
|
21
21
|
* the setup order, not keying the pair — so the pair is not keyed and the line
|
|
22
|
-
* names the
|
|
22
|
+
* counts the slots between them first, then names them as far as the line's
|
|
23
|
+
* cap allows (#324; the report's `why` and `between` carry every one).
|
|
24
|
+
* - **The gap (#324).** Per pair, keyed or not, the leg depth gap |depth a −
|
|
25
|
+
* depth b| over the frames both limbs read — its median and max, in px and as
|
|
26
|
+
* a fraction of the leg (upper + lower rest length of the limb the tie is read
|
|
27
|
+
* from) — against the tie, and the frames that passed it: the `DRAWGAP` line
|
|
28
|
+
* and the report's `drawOrder.gaps`.
|
|
23
29
|
* - **The rule.** On each frame the limb whose depth is the lower (farther from
|
|
24
30
|
* the camera) is drawn behind: its slots first in the pair's run, in their
|
|
25
31
|
* setup order, then the other's — when the two depths differ by at least
|
|
@@ -67,6 +73,25 @@ export interface DrawOrderPair {
|
|
|
67
73
|
tie: number | null;
|
|
68
74
|
/** Why the pair is not keyed. */
|
|
69
75
|
why?: string;
|
|
76
|
+
/** The slots of anything else drawn between the pair's slots, in setup order (#324: the line counts them first and cuts this list). */
|
|
77
|
+
between?: string[];
|
|
78
|
+
}
|
|
79
|
+
/** One pair's leg depth gap against its tie (#324): the figures that say why a pair keyed little or nothing. */
|
|
80
|
+
export interface DrawOrderGap {
|
|
81
|
+
a: string;
|
|
82
|
+
b: string;
|
|
83
|
+
/** |depth a − depth b| over the frames both limbs read, px: the median and the largest. */
|
|
84
|
+
median: number | null;
|
|
85
|
+
max: number | null;
|
|
86
|
+
/** The same as a fraction of the leg (upper + lower rest length of the limb the tie is read from); null where a rest length is unread. */
|
|
87
|
+
medianLegs: number | null;
|
|
88
|
+
maxLegs: number | null;
|
|
89
|
+
/** The pair's tie, px and as a fraction of the leg. */
|
|
90
|
+
tie: number | null;
|
|
91
|
+
tieLegs: number | null;
|
|
92
|
+
/** Frames whose gap passed the tie (decided an order), of the frames both limbs read. */
|
|
93
|
+
passed: number;
|
|
94
|
+
frames: number;
|
|
70
95
|
}
|
|
71
96
|
export interface DrawOrderSpan {
|
|
72
97
|
/** The chain drawn behind, and the one in front. */
|
|
@@ -82,6 +107,8 @@ export interface DrawOrderReading {
|
|
|
82
107
|
keys: Array<DrawOrderKey & { frame: number }>;
|
|
83
108
|
/** Every span of one decided order, setup or keyed, in frame order per pair. */
|
|
84
109
|
spans: DrawOrderSpan[];
|
|
110
|
+
/** Per pair, the leg depth gap against the tie (#324); keyed or not. */
|
|
111
|
+
gaps?: DrawOrderGap[];
|
|
85
112
|
}
|
|
86
113
|
|
|
87
114
|
interface Resolved {
|
|
@@ -164,10 +191,19 @@ function resolvePair(pairLimbs: readonly [Limb, Limb], map: JointMap, rest: Rest
|
|
|
164
191
|
const lo = idx[0]!;
|
|
165
192
|
const hi = idx[idx.length - 1]!;
|
|
166
193
|
const between = names.slice(lo, hi + 1).filter((n) => !slotsA.has(n) && !slotsB.has(n));
|
|
167
|
-
if (between.length > 0) return none(
|
|
194
|
+
if (between.length > 0) return { ...none(betweenWhy(between, between.length)), pair: { ...pair, why: betweenWhy(between, between.length), between } };
|
|
168
195
|
return { pair: { ...pair, keyed: true }, lo, hi, aBehind: arrangement(names, lo, a, b), bBehind: arrangement(names, lo, b, a) };
|
|
169
196
|
}
|
|
170
197
|
|
|
198
|
+
/** The not-keyed reason for slots drawn between the pair: the count first, then the first `shown` of the list (#324). */
|
|
199
|
+
function betweenWhy(between: readonly string[], shown: number): string {
|
|
200
|
+
const tail = 'the setup order is not changed here';
|
|
201
|
+
if (between.length === 1) return shown > 0 ? `1 slot: "${between[0]}" is drawn between them, and a swap would re-layer it (${tail})` : `1 slot is drawn between them (… +1 more), and a swap would re-layer it (${tail})`;
|
|
202
|
+
const more = between.length - shown;
|
|
203
|
+
const list = `${between.slice(0, shown).map((n) => `"${n}"`).join(', ')}${more > 0 ? `${shown > 0 ? ', ' : ''}… +${more} more` : ''}`;
|
|
204
|
+
return `${between.length} slots are drawn between them (${list}), and a swap would re-layer them (${tail})`;
|
|
205
|
+
}
|
|
206
|
+
|
|
171
207
|
type Joints = Partial<Record<Body18, Joint>>;
|
|
172
208
|
|
|
173
209
|
/** A segment's rest length as the depth states it (drawn ÷ ratio), the median over the frames that name it; null where none does. */
|
|
@@ -187,13 +223,40 @@ function restOf(track: readonly Joints[], depth: ReadonlyArray<FrameDepth | unde
|
|
|
187
223
|
return vals.length % 2 === 1 ? vals[m]! : (vals[m - 1]! + vals[m]!) / 2;
|
|
188
224
|
}
|
|
189
225
|
|
|
190
|
-
/**
|
|
191
|
-
|
|
192
|
-
|
|
226
|
+
/**
|
|
227
|
+
* #281's tie for two limbs, px: √(1 − 0.95²) × (upper + lower ÷ 2), the smaller limb's; null where a rest length is unread.
|
|
228
|
+
* `leg` is that limb's upper + lower rest length (#324's unit for the gap).
|
|
229
|
+
*/
|
|
230
|
+
function tieOf(track: readonly Joints[], depth: ReadonlyArray<FrameDepth | undefined>, limbs: readonly [Limb, Limb]): { tie: number | null; leg: number | null } {
|
|
231
|
+
const rests = limbs.map((l) => {
|
|
193
232
|
const [u, w] = LIMBS[l].map((s) => restOf(track, depth, s));
|
|
194
|
-
return u === null || u === undefined || w === null || w === undefined ? null : u + w / 2;
|
|
233
|
+
return u === null || u === undefined || w === null || w === undefined ? null : { hidden: u + w / 2, leg: u + w };
|
|
195
234
|
});
|
|
196
|
-
|
|
235
|
+
if (rests[0] == null || rests[1] == null) return { tie: null, leg: null };
|
|
236
|
+
const r = rests[1].hidden < rests[0].hidden ? rests[1] : rests[0];
|
|
237
|
+
return { tie: Math.round(TIE_PER_LENGTH * r.hidden * 100) / 100, leg: r.leg };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const round = (x: number, k: number): number => Math.round(x * 10 ** k) / 10 ** k;
|
|
241
|
+
|
|
242
|
+
/** One pair's leg depth gap against its tie over the frames (the rule's own test: unequal and at least the tie). */
|
|
243
|
+
function gapOf(pair: DrawOrderPair, layers: readonly FrameLayers[], leg: number | null): DrawOrderGap {
|
|
244
|
+
const tie = pair.tie ?? 0;
|
|
245
|
+
const gaps: number[] = [];
|
|
246
|
+
let passed = 0;
|
|
247
|
+
for (const l of layers) {
|
|
248
|
+
const za = l.depth[pair.limbs[0]];
|
|
249
|
+
const zb = l.depth[pair.limbs[1]];
|
|
250
|
+
if (za === undefined || zb === undefined) continue;
|
|
251
|
+
gaps.push(Math.abs(za - zb));
|
|
252
|
+
if (za !== zb && Math.abs(za - zb) >= tie) passed++;
|
|
253
|
+
}
|
|
254
|
+
gaps.sort((x, y) => x - y);
|
|
255
|
+
const m = gaps.length >> 1;
|
|
256
|
+
const median = gaps.length === 0 ? null : gaps.length % 2 === 1 ? gaps[m]! : (gaps[m - 1]! + gaps[m]!) / 2;
|
|
257
|
+
const max = gaps.length === 0 ? null : gaps[gaps.length - 1]!;
|
|
258
|
+
const legs = (x: number | null): number | null => (x === null || leg === null || !(leg > 0) ? null : round(x / leg, 3));
|
|
259
|
+
return { a: pair.a, b: pair.b, median: median === null ? null : round(median, 2), max: max === null ? null : round(max, 2), medianLegs: legs(median), maxLegs: legs(max), tie: pair.tie, tieLegs: legs(pair.tie), passed, frames: gaps.length };
|
|
197
260
|
}
|
|
198
261
|
|
|
199
262
|
/**
|
|
@@ -203,7 +266,11 @@ function tieOf(track: readonly Joints[], depth: ReadonlyArray<FrameDepth | undef
|
|
|
203
266
|
*/
|
|
204
267
|
export function drawOrderFromDepth(layers: readonly FrameLayers[], track: readonly Joints[], depth: ReadonlyArray<FrameDepth | undefined>, times: readonly number[], map: JointMap, rest: Rest, order: ReadonlyArray<{ name: string; bone: string }>, sets?: RigSets, loop = false): DrawOrderReading {
|
|
205
268
|
const resolved = DRAW_ORDER_PAIRS.map((p) => resolvePair(p, map, rest, order, sets));
|
|
206
|
-
|
|
269
|
+
const legs = resolved.map((r) => {
|
|
270
|
+
const t = tieOf(track, depth, r.pair.limbs);
|
|
271
|
+
r.pair.tie = t.tie;
|
|
272
|
+
return t.leg;
|
|
273
|
+
});
|
|
207
274
|
const names = order.map((s) => s.name);
|
|
208
275
|
// Per pair, per frame: who is behind ('a' | 'b'), or null for the setup order before any decision.
|
|
209
276
|
const states = resolved.map((r) => {
|
|
@@ -247,7 +314,7 @@ export function drawOrderFromDepth(layers: readonly FrameLayers[], track: readon
|
|
|
247
314
|
else spans.push({ behind: s === 'a' ? r.pair.a : r.pair.b, front: s === 'a' ? r.pair.b : r.pair.a, frames: [f, f], keyed: (s === 'a' ? r.aBehind : r.bBehind).length > 0 });
|
|
248
315
|
});
|
|
249
316
|
});
|
|
250
|
-
return { pairs: resolved.map((r) => r.pair), keys, spans };
|
|
317
|
+
return { pairs: resolved.map((r) => r.pair), keys, spans, gaps: resolved.map((r, k) => gapOf(r.pair, layers, legs[k]!)) };
|
|
251
318
|
}
|
|
252
319
|
|
|
253
320
|
/** The animation's `drawOrder` value: the keys without their frames. */
|
|
@@ -285,6 +352,26 @@ export function drawOrderLines(d: DrawOrderReading, prefix = 'DRAWORDER '): stri
|
|
|
285
352
|
while (n > 1 && text(n).length > DRAW_ORDER_LINE_MAX) n--;
|
|
286
353
|
const why = d.pairs.every((p) => !p.keyed) ? 'no pair keyed' : 'the setup order holds on every frame';
|
|
287
354
|
const lines = [keyed.length === 0 ? `${head} (${why})` : text(n)];
|
|
288
|
-
for (const p of d.pairs)
|
|
355
|
+
for (const p of d.pairs) {
|
|
356
|
+
if (p.keyed) continue;
|
|
357
|
+
const line = (why: string | undefined): string => `${prefix}${p.a} / ${p.b} not keyed: ${why}`;
|
|
358
|
+
// Slots between the pair: the count first, the list cut until the line fits (#324).
|
|
359
|
+
let shown = p.between?.length ?? 0;
|
|
360
|
+
while (p.between !== undefined && shown > 0 && line(betweenWhy(p.between, shown)).length > DRAW_ORDER_LINE_MAX) shown--;
|
|
361
|
+
lines.push(line(p.between === undefined ? p.why : betweenWhy(p.between, shown)).slice(0, DRAW_ORDER_LINE_MAX));
|
|
362
|
+
}
|
|
289
363
|
return lines;
|
|
290
364
|
}
|
|
365
|
+
|
|
366
|
+
/** px with its fraction of the leg: "12.3 px (0.084 leg)". */
|
|
367
|
+
const pxLeg = (px: number | null, legs: number | null): string => (px === null ? 'unread' : `${px} px${legs === null ? '' : ` (${legs} leg)`}`);
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* The DRAWGAP lines (rig-c/pose#324): one per pair, keyed or not — the leg depth gap's median and
|
|
371
|
+
* max against the tie, and how many frames passed it. Cut to `DRAW_ORDER_LINE_MAX` characters.
|
|
372
|
+
*/
|
|
373
|
+
export function drawGapLines(d: DrawOrderReading, prefix = 'DRAWGAP '): string[] {
|
|
374
|
+
return (d.gaps ?? []).map((g) =>
|
|
375
|
+
`${prefix}${g.a} / ${g.b}: leg depth gap median ${pxLeg(g.median, g.medianLegs)}, max ${pxLeg(g.max, g.maxLegs)}, against the tie of ${g.tie === null ? 'none (a rest length is unread; equal depths only)' : pxLeg(g.tie, g.tieLegs)}; ${g.passed} of ${g.frames} frame(s) passed it`.slice(0, DRAW_ORDER_LINE_MAX),
|
|
376
|
+
);
|
|
377
|
+
}
|
package/src/outdir.ts
CHANGED
|
@@ -14,7 +14,7 @@ export type OutDirs = Map<string, string[]>;
|
|
|
14
14
|
|
|
15
15
|
const writable = (dir: string): boolean => {
|
|
16
16
|
try {
|
|
17
|
-
accessSync(dir, constants.W_OK);
|
|
17
|
+
accessSync(dir, constants.W_OK | constants.X_OK);
|
|
18
18
|
return true;
|
|
19
19
|
} catch {
|
|
20
20
|
return false;
|
|
@@ -22,9 +22,9 @@ const writable = (dir: string): boolean => {
|
|
|
22
22
|
};
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
* Check where each named output lands: its directory is there
|
|
26
|
-
* is a directory this process can write into.
|
|
27
|
-
* is added to `dirs`. A directory a flag names itself (--build, --render) is checked by its parent.
|
|
25
|
+
* Check where each named output lands: its directory is there and this process can write into it
|
|
26
|
+
* (rig-c/pose#323), or the nearest existing ancestor is a directory this process can write into.
|
|
27
|
+
* A miss is added to `p`; a directory still to make is added to `dirs`. A directory a flag names itself (--build, --render) is checked by its parent.
|
|
28
28
|
*/
|
|
29
29
|
export function planOutDirs(outputs: ReadonlyArray<readonly [string, string | undefined]>, p: Problems, dirs: OutDirs = new Map()): OutDirs {
|
|
30
30
|
for (const [flag, path] of outputs) {
|
|
@@ -33,7 +33,7 @@ export function planOutDirs(outputs: ReadonlyArray<readonly [string, string | un
|
|
|
33
33
|
let up = dir;
|
|
34
34
|
while (!existsSync(up) && dirname(up) !== up) up = dirname(up);
|
|
35
35
|
if (!statSync(up).isDirectory()) p.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`);
|
|
36
|
-
else if (
|
|
36
|
+
else if (!writable(up)) p.add('ARG_PATH', up === dir ? `${flag} ${path} — ${dir} is not writable` : `${flag} ${path} — ${up} is not writable, so directory ${dir} cannot be made under it`);
|
|
37
37
|
else if (up !== dir) dirs.set(dir, [...(dirs.get(dir) ?? []), `${flag} ${path}`]);
|
|
38
38
|
}
|
|
39
39
|
return dirs;
|
package/src/pelvis.ts
CHANGED
|
@@ -31,6 +31,11 @@
|
|
|
31
31
|
* scan runs down to where the colour was found. The first patch that holds
|
|
32
32
|
* and is not the background is the garment; with none, the midpoint's median
|
|
33
33
|
* is read as it is.
|
|
34
|
+
* The line is fitted through the band's top edge (rig-c/pose#318): where a
|
|
35
|
+
* strip of the garment is too thin for the colour test (a waistband drawn as
|
|
36
|
+
* two ink lines), the tops step between the strip's lines, and each column
|
|
37
|
+
* is followed up to its topmost garment pixel; an edge that cuts across the
|
|
38
|
+
* band under it (a fold) is declined.
|
|
34
39
|
*
|
|
35
40
|
* The forearms are not read: pixels near the segments elbow → wrist (and the
|
|
36
41
|
* hands) are masked, since a hanging arm passes the hips in a walk. A frame
|
|
@@ -76,13 +81,17 @@ export const MAX_RMS = 2.5;
|
|
|
76
81
|
export const ARM_RADIUS = 0.15;
|
|
77
82
|
/** Hips closer than this in the picture, px, have no span to read across (a figure seen side-on). */
|
|
78
83
|
export const MIN_SPAN = 6;
|
|
84
|
+
/** The band's top is taken above the scan's tops only when their climbs to it differ by more than this many rows (rig-c/pose#318): each end of a clean edge's climb is found to a row. */
|
|
85
|
+
export const CLIMB_SPREAD = 2;
|
|
86
|
+
/** The band's top is climbed against at most this many garment colours (rig-c/pose#318): a paletted garment's own, or a painted one's fullest cells. */
|
|
87
|
+
export const MAX_CLOTH = 16;
|
|
79
88
|
|
|
80
89
|
/**
|
|
81
90
|
* One frame's reading: the tilt (Spine degrees, r_hip → l_hip), or why it was declined. A waistband
|
|
82
91
|
* read through the window mean says so (`mean`), and one whose colour was found below the hips says
|
|
83
|
-
* how far (`below`, px).
|
|
92
|
+
* how far (`below`, px); one whose band top lay above the colour's edge in some columns says how many (`raised`, #318).
|
|
84
93
|
*/
|
|
85
|
-
export type Reading = { tilt: number; cover: number; rms: number; mean?: true; below?: number } | { kind: string; why: string };
|
|
94
|
+
export type Reading = { tilt: number; cover: number; rms: number; mean?: true; below?: number; raised?: number } | { kind: string; why: string };
|
|
86
95
|
|
|
87
96
|
/** The frame as RGB over white, its background colour, and which pixels are masked (forearms, hands). */
|
|
88
97
|
interface Frame {
|
|
@@ -321,6 +330,7 @@ function waistband(f: Frame, c: Pt, span: number, x0: number, x1: number, cols:
|
|
|
321
330
|
}
|
|
322
331
|
}
|
|
323
332
|
}
|
|
333
|
+
const raised = toBandTop(f, tops, g, top, colourAt);
|
|
324
334
|
const share = tops.length / cols;
|
|
325
335
|
if (share < MIN_COVER) {
|
|
326
336
|
return above > cols / 2 ? { kind: 'garment above the band', why: `the garment runs above the band read (${above} of ${cols} columns carry its colour ${(1.5 * span).toFixed(0)} px above the hips); no waistband edge` } : { kind: 'waistband edge too short', why: `the garment's top edge is found in ${tops.length} of the ${cols} columns between the hips (${(100 * share).toFixed(0)}% < ${100 * MIN_COVER}%)` };
|
|
@@ -329,7 +339,197 @@ function waistband(f: Frame, c: Pt, span: number, x0: number, x1: number, cols:
|
|
|
329
339
|
if (fit === null) return { kind: 'no line', why: 'the garment\'s top edge does not fit a line (fewer than three columns)' };
|
|
330
340
|
if (fit.rms > MAX_RMS) return { kind: 'not a straight waistband', why: `the garment's top edge lies ${fit.rms.toFixed(2)} px from its line (root mean square > ${MAX_RMS}); not a straight waistband` };
|
|
331
341
|
if (Math.abs(Math.atan(fit.m)) * (180 / Math.PI) > 45) return { kind: 'waistband too steep', why: `the garment's top edge runs ${(Math.atan(fit.m) * (180 / Math.PI)).toFixed(1)}° from level; a waistband is within 45°` };
|
|
332
|
-
|
|
342
|
+
const off = notTheBand(f, fit, tops, top, colourAt);
|
|
343
|
+
if (off !== null) return off;
|
|
344
|
+
return { tilt: tiltOf(fit.m, rHip, lHip), cover: share, rms: fit.rms, ...(isMean ? { mean: true as const } : {}), ...(below > 0 ? { below } : {}), ...(raised > 0 ? { raised } : {}) };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* rig-c/pose#318: the band's top edge, the topmost garment boundary in each column. The three-row test misses garment
|
|
349
|
+
* above the row it finds when a strip of it is too thin to carry the colour three rows deep (through the window mean,
|
|
350
|
+
* a strip between two ink lines never does), so the tops step between a strip's top line and the cloth under its lower
|
|
351
|
+
* line. The garment's colours are the picture's own: the pixels of three rows in each column `DITHER_RADIUS + 1` below
|
|
352
|
+
* the row the scan found, clear of the edge's blend (an outline there would lend the colour above to the garment). The
|
|
353
|
+
* thing above has one colour, `a`: the median, over the columns read, of the rows from the scan's top down to
|
|
354
|
+
* `3 · (DITHER_RADIUS + 1)` above the topmost top — clear of the edge's blend, an outline and a window, and by area, so
|
|
355
|
+
* a line crossing it or a tilted band's outline in a few of its rows does not move it. From each top
|
|
356
|
+
* the column is followed up, pixel by pixel, while a pixel is no nearer to `a` than to the nearest garment pixel (cloth
|
|
357
|
+
* of any shade, and the ink lines on it, are); a climb that reaches the scan's top or a forearm does not see the top.
|
|
358
|
+
* The band's top is the topmost cloth pixel on the path (within `GARMENT_DIFF` of a garment pixel), from where the climb
|
|
359
|
+
* stopped down to the scan's three rows: an outline above it, or a line leaving the band upward, is ink, not garment;
|
|
360
|
+
* a thin strip's cloth between its two lines is garment. A clean edge climbs
|
|
361
|
+
* about the same rows in every column — the distance between two boundaries each found to a row — so only when the
|
|
362
|
+
* climbs differ by more than `CLIMB_SPREAD` rows (90th less 10th percentile) do the columns take their topmost garment
|
|
363
|
+
* row — and a column whose top was not seen is then not read, since its scan top is the lower level; otherwise the
|
|
364
|
+
* tops stay exactly as read. Nothing is done when the colour above is the garment's (its median
|
|
365
|
+
* within `GARMENT_DIFF`). Rewrites `tops` in place; returns how many moved.
|
|
366
|
+
*/
|
|
367
|
+
function toBandTop(f: Frame, tops: Pt[], g: RGB, top: number, colourAt: (x: number, y: number) => RGB | null): number {
|
|
368
|
+
if (tops.length === 0) return 0;
|
|
369
|
+
const lift = DITHER_RADIUS + 1;
|
|
370
|
+
const ya = Math.min(...tops.map((p) => p[1])) - 3 * lift;
|
|
371
|
+
if (ya < top) return 0;
|
|
372
|
+
// The garment's colours, at most `MAX_CLOTH`: a paletted garment's few, as they are; a painted one's many, as the mean
|
|
373
|
+
// colours of the `MAX_CLOTH` fullest cells of a `GARMENT_DIFF / 2` grid (first filled first among equals). The climb
|
|
374
|
+
// compares each pixel against them all, so their number is bounded whatever the span.
|
|
375
|
+
const exact = new Map<string, RGB>();
|
|
376
|
+
const cells = new Map<string, { s: RGB; n: number }>();
|
|
377
|
+
const step = GARMENT_DIFF / 2;
|
|
378
|
+
for (const [x, y0] of tops) {
|
|
379
|
+
for (let k = 0; k < 3; k++) {
|
|
380
|
+
const y = y0 + lift + k;
|
|
381
|
+
if (y < 0 || y >= f.h || f.masked(x, y)) continue;
|
|
382
|
+
const p = f.rgb(x, y);
|
|
383
|
+
if (exact.size <= MAX_CLOTH) exact.set(p.join(','), p);
|
|
384
|
+
const key = p.map((v) => Math.floor(v / step)).join(',');
|
|
385
|
+
const c = cells.get(key);
|
|
386
|
+
if (c === undefined) cells.set(key, { s: [p[0], p[1], p[2]], n: 1 });
|
|
387
|
+
else {
|
|
388
|
+
c.s[0] += p[0];
|
|
389
|
+
c.s[1] += p[1];
|
|
390
|
+
c.s[2] += p[2];
|
|
391
|
+
c.n++;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
const cloth: RGB[] = exact.size <= MAX_CLOTH ? [...exact.values()] : [...cells.values()].sort((u, v) => v.n - u.n).slice(0, MAX_CLOTH).map((c) => [c.s[0] / c.n, c.s[1] / c.n, c.s[2] / c.n]);
|
|
396
|
+
if (cloth.length === 0) return 0;
|
|
397
|
+
// The colour above: the median of the region, channel by channel, read twice over it rather than held.
|
|
398
|
+
const a = regionMedian((visit) => {
|
|
399
|
+
for (const [x] of tops) {
|
|
400
|
+
for (let y = top; y <= ya; y++) {
|
|
401
|
+
const p = f.masked(x, y) ? null : colourAt(x, y);
|
|
402
|
+
if (p !== null) visit(p);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
if (a === null) return 0;
|
|
407
|
+
if (diff(a, g) <= GARMENT_DIFF) return 0;
|
|
408
|
+
const nearest = (p: RGB, cs: RGB[]) => Math.min(...cs.map((q) => diff(p, q)));
|
|
409
|
+
const climbs = tops.map(([x, y0]) => {
|
|
410
|
+
let y = y0;
|
|
411
|
+
while (true) {
|
|
412
|
+
if (y - 1 < top || f.masked(x, y - 1)) return null;
|
|
413
|
+
const p = f.rgb(x, y - 1);
|
|
414
|
+
if (diff(p, a) < nearest(p, cloth)) break;
|
|
415
|
+
y--;
|
|
416
|
+
}
|
|
417
|
+
// The band's top: the topmost cloth pixel on the path, from where the climb stopped down to the scan's three rows.
|
|
418
|
+
for (let v = y; v <= y0 + 2; v++) if (v < f.h && nearest(f.rgb(x, v), cloth) <= GARMENT_DIFF) return y0 - v;
|
|
419
|
+
return null;
|
|
420
|
+
});
|
|
421
|
+
const ks = climbs.filter((k): k is number => k !== null).sort((p, q) => p - q);
|
|
422
|
+
if (ks.length === 0) return 0;
|
|
423
|
+
const rank = (q: number) => ks[Math.min(ks.length - 1, Math.max(0, Math.ceil(q * ks.length) - 1))]!;
|
|
424
|
+
if (rank(0.9) - rank(0.1) <= CLIMB_SPREAD) return 0;
|
|
425
|
+
let moved = 0;
|
|
426
|
+
const read = tops.flatMap((t, i): Pt[] => {
|
|
427
|
+
const k = climbs[i];
|
|
428
|
+
if (k === null || k === undefined) return [];
|
|
429
|
+
if (k !== 0) moved++;
|
|
430
|
+
return [[t[0], t[1] - k]];
|
|
431
|
+
});
|
|
432
|
+
tops.splice(0, tops.length, ...read);
|
|
433
|
+
return moved;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* The median, channel by channel, of the colours a region yields (`median`'s value: the middle one, or the mean of the
|
|
438
|
+
* two middle ones), without holding them: a first pass counts each channel's values by whole step (0–255), which names
|
|
439
|
+
* the step the middle rank falls in; a second keeps only the values in that step, as distinct values and counts. Memory
|
|
440
|
+
* is the 256 counts and the distinct values of one step. Null for an empty region.
|
|
441
|
+
*/
|
|
442
|
+
function regionMedian(each: (visit: (p: RGB) => void) => void): RGB | null {
|
|
443
|
+
const bin = (v: number) => Math.max(0, Math.min(255, Math.floor(v)));
|
|
444
|
+
const hist = [new Float64Array(256), new Float64Array(256), new Float64Array(256)];
|
|
445
|
+
let n = 0;
|
|
446
|
+
each((p) => {
|
|
447
|
+
n++;
|
|
448
|
+
for (let k = 0; k < 3; k++) hist[k]![bin(p[k]!)]!++;
|
|
449
|
+
});
|
|
450
|
+
if (n === 0) return null;
|
|
451
|
+
const m = n >> 1;
|
|
452
|
+
const ranks = n % 2 === 1 ? [m] : [m - 1, m];
|
|
453
|
+
// For each channel and rank: the step it falls in and its rank within the step.
|
|
454
|
+
const want = [0, 1, 2].map((k) => ranks.map((r) => {
|
|
455
|
+
let below = 0;
|
|
456
|
+
for (let b = 0; b < 256; b++) {
|
|
457
|
+
const c = hist[k]![b]!;
|
|
458
|
+
if (r < below + c) return { b, r: r - below };
|
|
459
|
+
below += c;
|
|
460
|
+
}
|
|
461
|
+
throw new Error('unreachable: a rank beyond the count');
|
|
462
|
+
}));
|
|
463
|
+
const kept = [0, 1, 2].map(() => new Map<number, Map<number, number>>());
|
|
464
|
+
for (let k = 0; k < 3; k++) for (const w of want[k]!) kept[k]!.set(w.b, new Map());
|
|
465
|
+
each((p) => {
|
|
466
|
+
for (let k = 0; k < 3; k++) {
|
|
467
|
+
const vs = kept[k]!.get(bin(p[k]!));
|
|
468
|
+
if (vs !== undefined) vs.set(p[k]!, (vs.get(p[k]!) ?? 0) + 1);
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
const at = (k: number, w: { b: number; r: number }) => {
|
|
472
|
+
let below = 0;
|
|
473
|
+
for (const [v, c] of [...kept[k]!.get(w.b)!.entries()].sort((u, z) => u[0] - z[0])) {
|
|
474
|
+
if (w.r < below + c) return v;
|
|
475
|
+
below += c;
|
|
476
|
+
}
|
|
477
|
+
throw new Error('unreachable: a rank beyond the step');
|
|
478
|
+
};
|
|
479
|
+
return [0, 1, 2].map((k) => {
|
|
480
|
+
const vs = want[k]!.map((w) => at(k, w));
|
|
481
|
+
return vs.length === 1 ? vs[0]! : (vs[0]! + vs[1]!) / 2;
|
|
482
|
+
}) as RGB;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* rig-c/pose#318: the edge the waistband reader fitted is declined when it cuts across the band instead of running
|
|
487
|
+
* along it — a fold's edge under a band the colour test passed over. Above the edge, in each column read, the colour
|
|
488
|
+
* `DITHER_RADIUS + 1` rows up (past the outline and the window's blend) is followed up while it holds within
|
|
489
|
+
* `GARMENT_DIFF`; where it stops is the band's top. When at least `MIN_COVER` of the columns read show such a band (not
|
|
490
|
+
* the ground, not open up to the scan's top), its top is fitted as a line. The two lines bound a strip of thickness
|
|
491
|
+
* `ta` and `tb` at the read columns' ends. A line inside a band of thickness t across a width W departs from the band's
|
|
492
|
+
* direction by at most atan(t / W); an edge whose strip more than doubles across the hips (|ta − tb| > min(ta, tb)), or
|
|
493
|
+
* crosses the band's top, cuts across the garment instead of running along the band.
|
|
494
|
+
*
|
|
495
|
+
* Null when the edge is the band's (or nothing above it is a band to hold it to).
|
|
496
|
+
*/
|
|
497
|
+
function notTheBand(f: Frame, fit: { m: number; q: number }, tops: Pt[], top: number, colourAt: (x: number, y: number) => RGB | null): Reading | null {
|
|
498
|
+
const edgeAt = (x: number) => fit.m * x + fit.q;
|
|
499
|
+
const bandTops: Pt[] = [];
|
|
500
|
+
for (const [x, y] of tops) {
|
|
501
|
+
const s = y - (DITHER_RADIUS + 1);
|
|
502
|
+
if (s < top || f.masked(x, s)) continue;
|
|
503
|
+
const c0 = colourAt(x, s);
|
|
504
|
+
if (c0 === null || diff(c0, f.bg) <= FIGURE_DIFF) continue;
|
|
505
|
+
let v = s;
|
|
506
|
+
let closed = false;
|
|
507
|
+
while (v - 1 >= top) {
|
|
508
|
+
if (f.masked(x, v - 1)) break;
|
|
509
|
+
const p = colourAt(x, v - 1);
|
|
510
|
+
if (p === null) break;
|
|
511
|
+
if (diff(p, c0) > GARMENT_DIFF) {
|
|
512
|
+
closed = true;
|
|
513
|
+
break;
|
|
514
|
+
}
|
|
515
|
+
v--;
|
|
516
|
+
}
|
|
517
|
+
if (closed) bandTops.push([x, v]);
|
|
518
|
+
}
|
|
519
|
+
if (bandTops.length < MIN_COVER * tops.length) return null;
|
|
520
|
+
const band = fitLine(bandTops);
|
|
521
|
+
if (band === null || band.rms > MAX_RMS) return null;
|
|
522
|
+
const xa = Math.min(...tops.map((p) => p[0]));
|
|
523
|
+
const xb = Math.max(...tops.map((p) => p[0]));
|
|
524
|
+
const ta = edgeAt(xa) - (band.m * xa + band.q);
|
|
525
|
+
const tb = edgeAt(xb) - (band.m * xb + band.q);
|
|
526
|
+
if (Math.abs(ta - tb) <= Math.min(ta, tb)) return null;
|
|
527
|
+
const deg = (m: number) => Math.atan(m) * (180 / Math.PI);
|
|
528
|
+
const allow = Math.max(0, Math.min(ta, tb));
|
|
529
|
+
return {
|
|
530
|
+
kind: 'edge leaves the band',
|
|
531
|
+
why: `the garment's edge fitted across the hips runs ${Math.abs(deg(fit.m) - deg(band.m)).toFixed(1)}° from the top of the band above it (${bandTops.length} of ${tops.length} columns; the band ${ta.toFixed(1)} px thick at x ${xa} and ${tb.toFixed(1)} px at x ${xb}); an edge inside a band ${allow.toFixed(1)} px thick at its thinnest departs from it by at most ${deg(allow / (xb - xa)).toFixed(1)}° across the ${xb - xa} px read, so this edge cuts across the garment (a fold), not along the band`,
|
|
532
|
+
};
|
|
333
533
|
}
|
|
334
534
|
|
|
335
535
|
/**
|
|
@@ -420,6 +620,8 @@ function viaNote(readings: Reading[]): string {
|
|
|
420
620
|
const parts: string[] = [];
|
|
421
621
|
if (mean > 0) parts.push(`${mean} through the ${2 * DITHER_RADIUS + 1}×${2 * DITHER_RADIUS + 1} window mean (the garment's colour does not hold pixel by pixel: a dithered or paletted picture)`);
|
|
422
622
|
if (below > 0) parts.push(`${below} with the colour found below the hips (up to ${Math.max(...ok.map((r) => r.below ?? 0)).toFixed(1)} px; the hips on the band's edge)`);
|
|
623
|
+
const raised = ok.filter((r) => r.raised !== undefined);
|
|
624
|
+
if (raised.length > 0) parts.push(`${raised.length} with the band's top read above the colour's edge in some columns (up to ${Math.max(...raised.map((r) => r.raised ?? 0))} columns: a strip too thin to carry the colour, read as its top line)`);
|
|
423
625
|
return parts.length === 0 ? '' : `; of the frames read, ${parts.join(', ')}`;
|
|
424
626
|
}
|
|
425
627
|
|
package/src/reduce.ts
CHANGED
|
@@ -80,6 +80,12 @@ export interface Reduced {
|
|
|
80
80
|
rows: ReduceRow[];
|
|
81
81
|
/** Each bone's world-aim error on every frame (reduced less dense, degrees), as the poser evaluates the reduced keys. */
|
|
82
82
|
errors: Map<string, number[]>;
|
|
83
|
+
/**
|
|
84
|
+
* rig-c/pose#320: each trunk root whose smoothing (`smoothTrunk`, #116) was not kept because it left a bone it
|
|
85
|
+
* manages past that bone's own tolerance — the bone, its smoothed worst error and its tolerance (degrees); the
|
|
86
|
+
* fitted keys stand. Absent when every smoothing was kept.
|
|
87
|
+
*/
|
|
88
|
+
unsmoothed?: Array<{ root: string; bone: string; worst: number; tolerance: number }>;
|
|
83
89
|
}
|
|
84
90
|
|
|
85
91
|
// ---------------------------------------------------------------------------
|
|
@@ -607,7 +613,9 @@ function solveLimbPreservingJitter(
|
|
|
607
613
|
* above the sequence's own. Optimizing knot values and handle y-coordinates
|
|
608
614
|
* jointly within the ±tolerance world-aim tube removes those C0 breaks while
|
|
609
615
|
* keeping any limb chains hanging from the trunk within 0.04 of their landing
|
|
610
|
-
* and jitter.
|
|
616
|
+
* and jitter. The tube's bounds are a penalty, not a constraint: a root whose
|
|
617
|
+
* smoothing leaves any bone it manages past that bone's own tolerance keeps its
|
|
618
|
+
* fitted keys instead, recorded in `out.unsmoothed` (rig-c/pose#320).
|
|
611
619
|
*/
|
|
612
620
|
export function smoothTrunk(
|
|
613
621
|
times: readonly number[],
|
|
@@ -737,6 +745,16 @@ export function smoothTrunk(
|
|
|
737
745
|
}
|
|
738
746
|
};
|
|
739
747
|
|
|
748
|
+
// rig-c/pose#320: the smoothing is kept only within every managed bone's own tolerance; past it, the fitted keys
|
|
749
|
+
// (within it by construction) stand, and the run says so.
|
|
750
|
+
const past = allManaged.map((b) => {
|
|
751
|
+
const e = b === root ? eRoot : eSub.get(b)!;
|
|
752
|
+
return { b, worst: Math.max(...e.map(Math.abs)) };
|
|
753
|
+
}).find((x) => x.worst > tolOf(x.b));
|
|
754
|
+
if (past !== undefined) {
|
|
755
|
+
(out.unsmoothed ??= []).push({ root, bone: past.b, worst: past.worst, tolerance: tolOf(past.b) });
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
740
758
|
updateBone(root, smRoot.keys, eRoot);
|
|
741
759
|
for (const b of [...headBones, ...arm0Bones, ...arm1Bones]) {
|
|
742
760
|
updateBone(b, smSub.get(b)!.keys, eSub.get(b)!);
|
package/src/run.ts
CHANGED
|
@@ -11,7 +11,7 @@ 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
13
|
import { depthLine, layersOf, type FrameLayers } from './depth.ts';
|
|
14
|
-
import { drawOrderFromDepth, drawOrderKeys, drawOrderLines, type DrawOrderReading } from './draworder.ts';
|
|
14
|
+
import { drawGapLines, drawOrderFromDepth, drawOrderKeys, drawOrderLines, type DrawOrderReading } from './draworder.ts';
|
|
15
15
|
import { convertCoco17, personTrack, readPoseSequence, TRAVEL_IN_PLACE, type CameraFrame } from './pose.ts';
|
|
16
16
|
import { isObject, lineOf, parseJson, Problems, Refusal, type Problem } from './problems.ts';
|
|
17
17
|
import { boneIdentity, droppedLine, keptOutLine, mergeProblems, readFragment, readIdle, targetOf, trackLabel, type IdleReading, type RigcTrack, type Target } from './idle.ts';
|
|
@@ -134,7 +134,7 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
134
134
|
const mapSnapshot = readReportInput(a.map), poseSnapshot = readReportInput(a.pose);
|
|
135
135
|
const map = readMap(mapSnapshot.bytes.toString('utf8'), a.map);
|
|
136
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
|
-
|
|
137
|
+
let reduceOf = a.reduce === undefined ? null : reduceTolerances(map, a.reduce, a.reduceChains, a.map);
|
|
138
138
|
const poseText = poseSnapshot.bytes.toString('utf8');
|
|
139
139
|
const seq = a.coco ? convertCoco17(poseText, a.pose) : readPoseSequence(poseText, a.pose);
|
|
140
140
|
if (seq.cleaned === undefined) refuse([{ code: 'POSE_NOT_CLEANED', text: notCleaned(a.pose, a.coco) }]);
|
|
@@ -229,7 +229,7 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
229
229
|
if (layers !== null) say(` ${depthLine(layers, seq.frames, seq.cleaned!.params.depth?.gait ?? null)}`);
|
|
230
230
|
// rig-c/pose#304: with --draw-order depth, the legs' draw order keyed from those layers.
|
|
231
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}`);
|
|
232
|
+
if (drawOrder !== null) for (const l of [...drawOrderLines(drawOrder), ...drawGapLines(drawOrder)]) say(` ${l}`);
|
|
233
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`);
|
|
234
234
|
if (r.feet !== null) {
|
|
235
235
|
const f = r.feet;
|
|
@@ -256,14 +256,23 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
256
256
|
if (r.face !== null) say(` FACE ${faceLine(r.face)}`);
|
|
257
257
|
if (r.body !== null) say(` BODY ${bodyLine(r.body)}`);
|
|
258
258
|
|
|
259
|
+
// #320: a mapped bone above a planted or aimed chain --reduce names is held to that chain's tolerance when its own is looser.
|
|
260
|
+
const held = reduceOf === null || a.reduceChains === undefined ? [] : holdParents(map, rest, new Set(r.targets.keys()), a.reduceChains, reduceOf, r.ankles === null ? [] : [r.ankles.bones.r, r.ankles.bones.l]);
|
|
261
|
+
if (held.length > 0) {
|
|
262
|
+
const first = reduceOf!;
|
|
263
|
+
reduceOf = (bone) => held.find((h) => h.bone === bone)?.to ?? first(bone);
|
|
264
|
+
for (const h of held) say(` REDUCE ${heldLine(h)}`);
|
|
265
|
+
}
|
|
259
266
|
// --reduce (#32): the dense keys thinned top-down, before anything reads the tracks.
|
|
260
267
|
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!);
|
|
261
268
|
if (reduced !== null) {
|
|
262
269
|
for (const row of reduced.rows) {
|
|
263
270
|
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'];
|
|
264
|
-
const
|
|
271
|
+
const hold = held.find((h) => h.bone === row.bone && row.kind === 'angle');
|
|
272
|
+
const own = row.kind === 'angle' && reduceOf!(row.bone) !== a.reduce ? (hold === undefined ? ` — within its chain's ${reduceOf!(row.bone)}°` : ` — held to ${hold.to}° for ${hold.for.map((c) => `chain "${c.chain}"`).join(', ')} below it`) : '';
|
|
265
273
|
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}`);
|
|
266
274
|
}
|
|
275
|
+
for (const u of reduced.unsmoothed ?? []) say(` REDUCE ${unsmoothedLine(u)}`);
|
|
267
276
|
const sum = (kind: ReduceRow['kind'], k: 'before' | 'after') => reduced.rows.filter((x) => x.kind === kind).reduce((t, x) => t + x[k], 0);
|
|
268
277
|
const scales = reduced.rows.some((x) => x.kind === 'scale') ? `; scalex keys ${sum('scale', 'before')} → ${sum('scale', 'after')} within ${SCALE_REDUCE} of the setup length` : '';
|
|
269
278
|
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)' : ''}`);
|
|
@@ -477,14 +486,14 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
477
486
|
// Only when the sequence carries depth (#281): a sequence without it writes the report it always has.
|
|
478
487
|
...(layers !== null ? { layers: layers.map((l, frame) => ({ frame, order: l.order, depth: l.depth })) } : {}),
|
|
479
488
|
// Only with --draw-order depth (#304).
|
|
480
|
-
...(drawOrder !== null ? { drawOrder: { source: 'depth', pairs: drawOrder.pairs, keys: drawOrder.keys, spans: drawOrder.spans } } : {}),
|
|
489
|
+
...(drawOrder !== null ? { drawOrder: { source: 'depth', pairs: drawOrder.pairs, keys: drawOrder.keys, spans: drawOrder.spans, gaps: drawOrder.gaps ?? [] } } : {}),
|
|
481
490
|
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 },
|
|
482
491
|
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 },
|
|
483
492
|
shear,
|
|
484
493
|
idleDropped: idle === null ? null : idle.dropped,
|
|
485
494
|
idleKeptOut: idle === null ? null : idle.keptOut,
|
|
486
495
|
merged: fragment.map((tr) => ({ track: trackLabel(tr, groups), keys: tr.keys.length })),
|
|
487
|
-
reduce: reduced === null ? null : { tolerance: a.reduce, ...(a.reduceChains === undefined ? {} : { chains: a.reduceChains }), hipTolerance: a.reduceHip ?? null, rows: reduced.rows, ankleDrift: drift },
|
|
496
|
+
reduce: reduced === null ? null : { tolerance: a.reduce, ...(a.reduceChains === undefined ? {} : { chains: a.reduceChains }), ...(held.length === 0 ? {} : { held }), ...(reduced.unsmoothed === undefined ? {} : { unsmoothed: reduced.unsmoothed }), hipTolerance: a.reduceHip ?? null, rows: reduced.rows, ankleDrift: drift },
|
|
488
497
|
}, 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});
|
|
489
498
|
if (a.build !== undefined) {
|
|
490
499
|
// Built again where it will live, not copied: rigc's page paths are relative to the build (#18).
|
|
@@ -1375,6 +1384,80 @@ export function reduceTolerances(map: JointMap, tolerance: number, chains: Recor
|
|
|
1375
1384
|
return (bone) => by.get(bone) ?? tolerance;
|
|
1376
1385
|
}
|
|
1377
1386
|
|
|
1387
|
+
/** rig-c/pose#320: a mapped bone held to the tolerance of a planted or aimed chain below it. */
|
|
1388
|
+
export interface ReduceHeld {
|
|
1389
|
+
/** The held bone, and the map chain it is a bone of (null for a mapped bone no chain lists). */
|
|
1390
|
+
bone: string;
|
|
1391
|
+
chain: string | null;
|
|
1392
|
+
/** Its tolerance as --reduce gave it (the run's, or its own chain's), degrees. */
|
|
1393
|
+
from: number;
|
|
1394
|
+
/** The tolerance it is held to: the tightest planted or aimed chain --reduce names below it, degrees. */
|
|
1395
|
+
to: number;
|
|
1396
|
+
/**
|
|
1397
|
+
* Each such chain below it reduced within `to`, in the map's order: its name, what holds it (`ankle`: it carries
|
|
1398
|
+
* a planted ankle's landing; `aim`: the map aims it), its first bone, and the lever — that bone's origin's setup
|
|
1399
|
+
* distance from the held bone's origin, rig units.
|
|
1400
|
+
*/
|
|
1401
|
+
for: Array<{ chain: string; by: 'ankle' | 'aim'; bone: string; lever: number }>;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
/**
|
|
1405
|
+
* rig-c/pose#320: every mapped bone above a planted or aimed chain `chains`
|
|
1406
|
+
* names, whose own tolerance (`tolOf`) is looser than that chain's, held to it
|
|
1407
|
+
* — to the tightest such chain when several hang below it. A chain is planted
|
|
1408
|
+
* when it carries an ankle the landing check holds (`ankles`), aimed when the
|
|
1409
|
+
* map gives it an `aim` other than "x".
|
|
1410
|
+
*
|
|
1411
|
+
* The lever. A turn error θ on a bone turns everything under it rigidly: a
|
|
1412
|
+
* chain below has its first bone moved by θ·L (L = `lever`) and its aim turned
|
|
1413
|
+
* by θ. The reduction refits the chain against the turn (`target = dense −
|
|
1414
|
+
* e_above`), so the aim is the chain's own error, but not the move: the ankle
|
|
1415
|
+
* allowance (`footReduce`) sums every segment from the legs' top down — the
|
|
1416
|
+
* held bone's lever among them — times the leg's own degrees. θ·L ≤ θ_leg·L is
|
|
1417
|
+
* θ ≤ θ_leg whatever L is. An aimed chain is refitted, and on the trunk
|
|
1418
|
+
* smoothed (#116), against its parent's error as it stands: held to the
|
|
1419
|
+
* chain's degrees, that error never asks the chain to absorb more than its own
|
|
1420
|
+
* tolerance. A bent chain (an arm, a leg with no ankle check) has only its aim
|
|
1421
|
+
* checked, which the refit holds, so it holds nothing above it. Only a
|
|
1422
|
+
* per-chain entry can be tighter than a bone above it: with none, nothing is held.
|
|
1423
|
+
*/
|
|
1424
|
+
export function holdParents(map: JointMap, rest: Rest, mapped: ReadonlySet<string>, chains: Record<string, number>, tolOf: (bone: string) => number, ankles: readonly string[]): ReduceHeld[] {
|
|
1425
|
+
const held: ReduceHeld[] = [];
|
|
1426
|
+
for (const c of map.chains) {
|
|
1427
|
+
const t = Object.hasOwn(chains, c.name) ? chains[c.name] : undefined;
|
|
1428
|
+
const top = c.bones[0];
|
|
1429
|
+
const by = c.bones.some((b) => ankles.includes(b)) ? ('ankle' as const) : c.aim.some((x) => x !== 'x') ? ('aim' as const) : null;
|
|
1430
|
+
if (t === undefined || top === undefined || by === null || !rest.has(top)) continue;
|
|
1431
|
+
const at = rest.get(top)!;
|
|
1432
|
+
for (let p = at.parent; p !== null; p = rest.get(p)?.parent ?? null) {
|
|
1433
|
+
if (!mapped.has(p) || c.bones.includes(p) || !(tolOf(p) > t)) continue;
|
|
1434
|
+
const was = held.findIndex((h) => h.bone === p);
|
|
1435
|
+
if (was >= 0 && held[was]!.to < t) continue;
|
|
1436
|
+
const x = rest.get(p)!;
|
|
1437
|
+
const child = { chain: c.name, by, bone: top, lever: Number(Math.hypot(at.worldX - x.worldX, at.worldY - x.worldY).toPrecision(6)) };
|
|
1438
|
+
if (was >= 0 && held[was]!.to === t) held[was]!.for.push(child);
|
|
1439
|
+
else if (was >= 0) held[was] = { ...held[was]!, to: t, for: [child] };
|
|
1440
|
+
else held.push({ bone: p, chain: map.chains.find((m) => m.bones.includes(p))?.name ?? null, from: tolOf(p), to: t, for: [child] });
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
return held;
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
/** rig-c/pose#320: the REDUCE line naming a held bone, the chains it was held for, and why. */
|
|
1447
|
+
export function heldLine(h: ReduceHeld): string {
|
|
1448
|
+
const below = h.for.map((c) => `chain "${c.chain}" (${c.by === 'ankle' ? 'a planted ankle' : 'aimed'}; L = ${c.lever} units to bone "${c.bone}")`).join(', ');
|
|
1449
|
+
const why = [
|
|
1450
|
+
...(h.for.some((c) => c.by === 'ankle') ? ['the ankle allowance counts L at the leg\'s own degrees'] : []),
|
|
1451
|
+
...(h.for.some((c) => c.by === 'aim') ? ['an aimed chain refits against its parent\'s error within its own'] : []),
|
|
1452
|
+
].join(' and ');
|
|
1453
|
+
return `held: bone "${h.bone}"${h.chain === null ? '' : ` (chain "${h.chain}")`} reduced within ${h.to}°, not ${h.from}° — for ${below} below it, reduced within ${h.to}°: a turn error θ on bone "${h.bone}" moves a chain below it by θ·L and turns it by θ, and ${why}, so θ ≤ ${h.to}°`;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
/** rig-c/pose#320: the REDUCE line for a trunk smoothing (#116) not kept. */
|
|
1457
|
+
export function unsmoothedLine(u: { root: string; bone: string; worst: number; tolerance: number }): string {
|
|
1458
|
+
return `smoothing (rig-c/pose#116) from bone "${u.root}" down not kept: it left bone "${u.bone}" at ${u.worst.toPrecision(3)}°, past its ${u.tolerance}°; the fitted keys stand, each within its tolerance`;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1378
1461
|
/** rig-c/pose#283: the per-chain tolerances as the run and REDUCE lines print them; empty without. */
|
|
1379
1462
|
export function chainsText(chains: Record<string, number> | undefined): string {
|
|
1380
1463
|
if (chains === undefined) return '';
|