@rig-c/pose 0.3.0 → 0.5.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 +59 -2
- package/cli.ts +13 -2
- package/package.json +1 -1
- package/src/map.ts +207 -6
- package/src/retarget.ts +202 -21
- package/src/rigc.ts +36 -1
- package/src/run.ts +30 -9
- package/src/switch.ts +455 -0
package/src/switch.ts
ADDED
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attachment switches: when the pose is in a pose the rig has an alternate set
|
|
3
|
+
* of drawings for, show that set. Pure — no file, no clock, no process.
|
|
4
|
+
*
|
|
5
|
+
* The metric, per frame and per set: the largest |wrap(pose − key)| over every
|
|
6
|
+
* segment of the set's chains, where "pose" is the segment's angle exactly as
|
|
7
|
+
* the retarget asked the bone for it (after holds, smoothing, the short hold,
|
|
8
|
+
* the flip guard and the hinge check; before any range clamp) and "key" is the
|
|
9
|
+
* same segment's angle in the set's key skeleton, both by `pictureAngle`. Per
|
|
10
|
+
* frame the candidate is the set with the smallest distance within the
|
|
11
|
+
* tolerance (ties: map order); a run of one candidate shorter than the minimum
|
|
12
|
+
* span is dropped, and the base art shows there; a gap shorter than the minimum
|
|
13
|
+
* span between two runs of one set is bridged by that set. No other hysteresis.
|
|
14
|
+
*
|
|
15
|
+
* Sets compete only with the sets they share a slot with (shown or hidden,
|
|
16
|
+
* transitively), or a bone they offset: two that touch nothing in common — a
|
|
17
|
+
* drawing of each arm — can show at once, and two that do never overlap, so
|
|
18
|
+
* each slot has one value per frame and one `attachment` track, and each
|
|
19
|
+
* offset bone one `translate` track.
|
|
20
|
+
*
|
|
21
|
+
* While a set shows, every slot in its `slots` shows that attachment and every
|
|
22
|
+
* slot in its `hides` is empty; everywhere else a slot shows its setup
|
|
23
|
+
* attachment, as the compiled model states it. A direct switch from one set to
|
|
24
|
+
* another therefore puts the first set's slots back to setup (an extra slot
|
|
25
|
+
* whose setup is empty goes empty) on the frame the second set shows.
|
|
26
|
+
*
|
|
27
|
+
* Keys are stepped (rig-c's attachment timeline has no curve; the offsets'
|
|
28
|
+
* `translate` keys are written `stepped`). A switch onto frame i is keyed half
|
|
29
|
+
* a frame before it, at the midpoint of frames i−1 and i: a stepped key that
|
|
30
|
+
* sits ON a frame's time fires a frame late wherever the sampler's accumulated
|
|
31
|
+
* time lands below it (rig-c docs/AUTHORING.md §4.5), and the pose's times and
|
|
32
|
+
* a renderer's `i / fps` are two grids that disagree by more than that.
|
|
33
|
+
* Halfway, both see it on frame i.
|
|
34
|
+
*/
|
|
35
|
+
import type { AlternateSet, JointMap, Rest, RigSlots } from './map.ts';
|
|
36
|
+
import { descends } from './map.ts';
|
|
37
|
+
import type { MotionKey, MotionTrack } from './retarget.ts';
|
|
38
|
+
import { pictureAngle, wrap } from './retarget.ts';
|
|
39
|
+
import type { MeshFolds } from './rigc.ts';
|
|
40
|
+
|
|
41
|
+
export interface SwitchOptions {
|
|
42
|
+
/** Degrees: the largest segment difference at which a set may show. */
|
|
43
|
+
switchTolerance: number;
|
|
44
|
+
/** Seconds: a run of one set shorter than this is dropped. */
|
|
45
|
+
switchMin: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface Switch {
|
|
49
|
+
set: string;
|
|
50
|
+
chains: string[];
|
|
51
|
+
/** Slot → the attachment it shows over the span. */
|
|
52
|
+
slots: Record<string, string>;
|
|
53
|
+
/** Slots empty over the span. */
|
|
54
|
+
hides: string[];
|
|
55
|
+
/** Bone → the translate it holds over the span. */
|
|
56
|
+
offsets: Record<string, [number, number]>;
|
|
57
|
+
/** The frame time it shows from, and the time the base art comes back (the animation's end when it runs to the end). */
|
|
58
|
+
from: number;
|
|
59
|
+
to: number;
|
|
60
|
+
/** First and last frame index shown. */
|
|
61
|
+
frames: [number, number];
|
|
62
|
+
/** Over the frames shown, degrees. */
|
|
63
|
+
distance: { max: number; median: number };
|
|
64
|
+
/** Per bone the set has a `drawn` angle for: |wrap(target − drawn)| over the frames shown, degrees. */
|
|
65
|
+
artTurn: Record<string, { max: number; median: number }>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface AttachmentKey {
|
|
69
|
+
t: number;
|
|
70
|
+
/** The attachment shown from `t`, or null for nothing. */
|
|
71
|
+
v: string | null;
|
|
72
|
+
}
|
|
73
|
+
export interface SlotTrack {
|
|
74
|
+
slot: string;
|
|
75
|
+
property: 'attachment';
|
|
76
|
+
keys: AttachmentKey[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Inside a set's span, a bone turned from its drawn angle past its range: the demand for another drawing of that set. */
|
|
80
|
+
export interface ArtRange {
|
|
81
|
+
set: string;
|
|
82
|
+
chain: string;
|
|
83
|
+
bone: string;
|
|
84
|
+
/** First and last frame of the run (spans merged across a gap of one frame). */
|
|
85
|
+
frames: [number, number];
|
|
86
|
+
seconds: number;
|
|
87
|
+
/** Degrees past the range, the most on any frame of the run. */
|
|
88
|
+
maxOverflow: number;
|
|
89
|
+
/** The signed art turn, wrap(target − drawn), on that frame. */
|
|
90
|
+
turn: number;
|
|
91
|
+
/** The bone's range, degrees. */
|
|
92
|
+
limits: [number, number];
|
|
93
|
+
/** Whether the bone was held at the limit (the set's `turn`) or followed the pose (its chain's `range` only). */
|
|
94
|
+
clamped: boolean;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface Uncovered {
|
|
98
|
+
slot: string;
|
|
99
|
+
attachment: string;
|
|
100
|
+
/** The map's chains that move the mesh (the deepest of them). */
|
|
101
|
+
chains: string[];
|
|
102
|
+
/** The fold times, seconds, on which the slot still shows this mesh. */
|
|
103
|
+
times: number[];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface Switches {
|
|
107
|
+
switches: Switch[];
|
|
108
|
+
/** The `attachment` tracks, one per slot whose picture changes. */
|
|
109
|
+
slotTracks: SlotTrack[];
|
|
110
|
+
/** The offsets' `translate` tracks, one per bone a shown set offsets. */
|
|
111
|
+
offsetTracks: MotionTrack[];
|
|
112
|
+
/** Per switched or hidden slot, what it shows on each frame (null: nothing). */
|
|
113
|
+
shown: Map<string, Array<string | null>>;
|
|
114
|
+
/** Per set, its distance on each frame, degrees. */
|
|
115
|
+
distances: Map<string, number[]>;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Per set, its distance to the pose on every frame: the largest segment difference over its chains. */
|
|
119
|
+
export function setDistances(map: JointMap, targets: ReadonlyMap<string, readonly number[]>, n: number): Map<string, number[]> {
|
|
120
|
+
const out = new Map<string, number[]>();
|
|
121
|
+
for (const a of map.alternates) {
|
|
122
|
+
const segs: Array<{ asked: readonly number[]; key: number }> = [];
|
|
123
|
+
for (const cn of a.chains) {
|
|
124
|
+
const c = map.chains.find((x) => x.name === cn)!;
|
|
125
|
+
c.bones.forEach((b, k) => segs.push({ asked: targets.get(b)!, key: pictureAngle(a.pose[c.joints[k]!]!, a.pose[c.joints[k + 1]!]!) }));
|
|
126
|
+
}
|
|
127
|
+
const d: number[] = [];
|
|
128
|
+
for (let i = 0; i < n; i++) d.push(Math.max(...segs.map((s) => Math.abs(wrap(s.asked[i]! - s.key)))));
|
|
129
|
+
out.set(a.name, d);
|
|
130
|
+
}
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function median(xs: readonly number[]): number {
|
|
135
|
+
const s = [...xs].sort((a, b) => a - b);
|
|
136
|
+
const m = s.length >> 1;
|
|
137
|
+
return s.length % 2 === 1 ? s[m]! : (s[m - 1]! + s[m]!) / 2;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const r6 = (v: number) => Math.round(v * 1e6) / 1e6;
|
|
141
|
+
const r3 = (v: number) => {
|
|
142
|
+
const r = Math.round(v * 1000) / 1000;
|
|
143
|
+
return Object.is(r, -0) ? 0 : r;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/** What a set switches: its slots shown or hidden, and the bones it offsets — each a value that has one track. */
|
|
147
|
+
function touches(a: AlternateSet): string[] {
|
|
148
|
+
return [...Object.keys(a.slots), ...a.hides].map((s) => `slot:${s}`).concat(Object.keys(a.offsets).map((b) => `bone:${b}`));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Groups of sets that switch a common slot or offset a common bone, transitively; each in map order. */
|
|
152
|
+
function competing(sets: readonly AlternateSet[]): AlternateSet[][] {
|
|
153
|
+
const parent = sets.map((_, i) => i);
|
|
154
|
+
const find = (i: number): number => (parent[i] === i ? i : (parent[i] = find(parent[i]!)));
|
|
155
|
+
const owner = new Map<string, number>();
|
|
156
|
+
sets.forEach((a, i) => {
|
|
157
|
+
for (const s of touches(a)) {
|
|
158
|
+
const j = owner.get(s);
|
|
159
|
+
if (j === undefined) owner.set(s, i);
|
|
160
|
+
else parent[find(i)] = find(j);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
const groups = new Map<number, AlternateSet[]>();
|
|
164
|
+
sets.forEach((a, i) => {
|
|
165
|
+
const g = find(i);
|
|
166
|
+
if (!groups.has(g)) groups.set(g, []);
|
|
167
|
+
groups.get(g)!.push(a);
|
|
168
|
+
});
|
|
169
|
+
return [...groups.values()];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** The key time for a change that shows on frame i: half a frame before it, 0 on the first frame. */
|
|
173
|
+
const changeAt = (times: readonly number[], i: number): number => (i === 0 ? 0 : r6((times[i - 1]! + times[i]!) / 2));
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Select the switches, and key them: per frame and per group of competing
|
|
177
|
+
* sets, the nearest within the tolerance; runs shorter than the minimum span
|
|
178
|
+
* dropped; one `attachment` track per slot whose picture changes, back to the
|
|
179
|
+
* slot's setup attachment at each end; one stepped `translate` track per bone
|
|
180
|
+
* a shown set offsets, 0 outside its spans.
|
|
181
|
+
*/
|
|
182
|
+
export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, readonly number[]>, times: readonly number[], duration: number, slots: RigSlots, opts: SwitchOptions): Switches {
|
|
183
|
+
const n = times.length;
|
|
184
|
+
const distances = setDistances(map, targets, n);
|
|
185
|
+
const switches: Switch[] = [];
|
|
186
|
+
for (const group of competing(map.alternates)) {
|
|
187
|
+
const pick: Array<AlternateSet | null> = [];
|
|
188
|
+
for (let i = 0; i < n; i++) {
|
|
189
|
+
let best: AlternateSet | null = null;
|
|
190
|
+
let bestD = Infinity;
|
|
191
|
+
for (const a of group) {
|
|
192
|
+
const d = distances.get(a.name)![i]!;
|
|
193
|
+
if (d <= opts.switchTolerance && d < bestD) {
|
|
194
|
+
best = a;
|
|
195
|
+
bestD = d;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
pick.push(best);
|
|
199
|
+
}
|
|
200
|
+
// A gap shorter than the minimum span between two runs of one set is that set
|
|
201
|
+
// too: the base art flashing for a frame between them is the flicker the
|
|
202
|
+
// minimum span exists to prevent (measured on the dancer: one-frame breaks at
|
|
203
|
+
// a blurred frame inside a span of one set).
|
|
204
|
+
for (let i = 0; i < n;) {
|
|
205
|
+
const a = pick[i];
|
|
206
|
+
let j = i;
|
|
207
|
+
while (j + 1 < n && pick[j + 1] === a) j++;
|
|
208
|
+
const before = i > 0 ? pick[i - 1] : null;
|
|
209
|
+
const after = j + 1 < n ? pick[j + 1] : null;
|
|
210
|
+
if (before !== null && before !== undefined && before === after && a !== before) {
|
|
211
|
+
const gap = times[j + 1]! - times[i]!;
|
|
212
|
+
if (gap < opts.switchMin - 1e-9) for (let k = i; k <= j; k++) pick[k] = before;
|
|
213
|
+
}
|
|
214
|
+
i = j + 1;
|
|
215
|
+
}
|
|
216
|
+
for (let i = 0; i < n;) {
|
|
217
|
+
const a = pick[i];
|
|
218
|
+
let j = i;
|
|
219
|
+
while (j + 1 < n && pick[j + 1] === a) j++;
|
|
220
|
+
if (a !== null && a !== undefined) {
|
|
221
|
+
const from = times[i]!;
|
|
222
|
+
const to = j + 1 < n ? times[j + 1]! : duration;
|
|
223
|
+
if (to - from >= opts.switchMin - 1e-9) {
|
|
224
|
+
const ds = distances.get(a.name)!.slice(i, j + 1);
|
|
225
|
+
switches.push({ set: a.name, chains: a.chains, slots: a.slots, hides: a.hides, offsets: a.offsets, from, to, frames: [i, j], distance: { max: r3(Math.max(...ds)), median: r3(median(ds)) }, artTurn: {} });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
i = j + 1;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const order = (name: string) => map.alternates.findIndex((a) => a.name === name);
|
|
232
|
+
switches.sort((x, y) => x.frames[0] - y.frames[0] || order(x.set) - order(y.set));
|
|
233
|
+
|
|
234
|
+
// What each touched slot shows per frame, then a key wherever that changes.
|
|
235
|
+
const slotOrder: string[] = [];
|
|
236
|
+
const boneOrder: string[] = [];
|
|
237
|
+
for (const a of map.alternates) {
|
|
238
|
+
for (const s of [...Object.keys(a.slots), ...a.hides]) if (!slotOrder.includes(s)) slotOrder.push(s);
|
|
239
|
+
for (const b of Object.keys(a.offsets)) if (!boneOrder.includes(b)) boneOrder.push(b);
|
|
240
|
+
}
|
|
241
|
+
const shown = new Map<string, Array<string | null>>();
|
|
242
|
+
const moved = new Map<string, Array<[number, number]>>();
|
|
243
|
+
for (const sw of switches) {
|
|
244
|
+
const values: Array<[string, string | null]> = [...Object.entries(sw.slots), ...sw.hides.map((h): [string, null] => [h, null])];
|
|
245
|
+
for (const [s, v] of values) {
|
|
246
|
+
if (!shown.has(s)) shown.set(s, Array.from({ length: n }, () => slots.get(s)!.setup));
|
|
247
|
+
const row = shown.get(s)!;
|
|
248
|
+
for (let i = sw.frames[0]; i <= sw.frames[1]; i++) row[i] = v;
|
|
249
|
+
}
|
|
250
|
+
for (const [b, o] of Object.entries(sw.offsets)) {
|
|
251
|
+
if (!moved.has(b)) moved.set(b, Array.from({ length: n }, (): [number, number] => [0, 0]));
|
|
252
|
+
const row = moved.get(b)!;
|
|
253
|
+
for (let i = sw.frames[0]; i <= sw.frames[1]; i++) row[i] = o;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const slotTracks: SlotTrack[] = [];
|
|
257
|
+
for (const s of slotOrder) {
|
|
258
|
+
const row = shown.get(s);
|
|
259
|
+
if (row === undefined) continue;
|
|
260
|
+
let prev = slots.get(s)!.setup;
|
|
261
|
+
const keys: AttachmentKey[] = [];
|
|
262
|
+
row.forEach((v, i) => {
|
|
263
|
+
if (v === prev) return;
|
|
264
|
+
keys.push({ t: changeAt(times, i), v });
|
|
265
|
+
prev = v;
|
|
266
|
+
});
|
|
267
|
+
if (keys.length > 0) slotTracks.push({ slot: s, property: 'attachment', keys });
|
|
268
|
+
}
|
|
269
|
+
const offsetTracks: MotionTrack[] = [];
|
|
270
|
+
for (const b of boneOrder) {
|
|
271
|
+
const row = moved.get(b);
|
|
272
|
+
if (row === undefined) continue;
|
|
273
|
+
const keys: MotionKey[] = [{ t: 0, v: [r3(row[0]![0]), r3(row[0]![1])] }];
|
|
274
|
+
row.forEach((v, i) => {
|
|
275
|
+
const prev = row[i - 1];
|
|
276
|
+
if (i === 0 || (prev![0] === v[0] && prev![1] === v[1])) return;
|
|
277
|
+
keys.push({ t: changeAt(times, i), v: [r3(v[0]), r3(v[1])] });
|
|
278
|
+
});
|
|
279
|
+
// Every key but the last holds until the next (a stepped curve on the last has nothing to ease to).
|
|
280
|
+
for (let k = 0; k + 1 < keys.length; k++) keys[k]!.ease = 'stepped';
|
|
281
|
+
offsetTracks.push({ bone: b, property: 'translate', keys });
|
|
282
|
+
}
|
|
283
|
+
return { switches, slotTracks, offsetTracks, shown, distances };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Per chain, the frames on which a switch of a set depicting it shows: its art was painted for that pose. */
|
|
287
|
+
export function coveredChains(switches: readonly Switch[], n: number): Map<string, boolean[]> {
|
|
288
|
+
const out = new Map<string, boolean[]>();
|
|
289
|
+
for (const sw of switches) {
|
|
290
|
+
for (const c of sw.chains) {
|
|
291
|
+
if (!out.has(c)) out.set(c, Array.from({ length: n }, () => false));
|
|
292
|
+
const row = out.get(c)!;
|
|
293
|
+
for (let i = sw.frames[0]; i <= sw.frames[1]; i++) row[i] = true;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return out;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Runs of flagged frames, merged across a gap of `gap` frames or fewer. */
|
|
300
|
+
export function runsOf(flag: readonly boolean[], gap: number): Array<[number, number]> {
|
|
301
|
+
const out: Array<[number, number]> = [];
|
|
302
|
+
for (let i = 0; i < flag.length; i++) {
|
|
303
|
+
if (!flag[i]) continue;
|
|
304
|
+
let j = i;
|
|
305
|
+
while (j + 1 < flag.length && flag[j + 1]) j++;
|
|
306
|
+
const last = out[out.length - 1];
|
|
307
|
+
if (last !== undefined && i - last[1] - 1 <= gap) last[1] = j;
|
|
308
|
+
else out.push([i, j]);
|
|
309
|
+
i = j;
|
|
310
|
+
}
|
|
311
|
+
return out;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Degrees past a range that do not count: the writer rounds a rotate key to
|
|
316
|
+
* 0.001°, and the rest angles come from rig-c's float32 model (≈ 1e-5° here),
|
|
317
|
+
* so a key the pose puts exactly on a limit can read a hair past it.
|
|
318
|
+
*/
|
|
319
|
+
export const RANGE_SLACK = 0.001;
|
|
320
|
+
|
|
321
|
+
/** How far a value is past [min, max], degrees; 0 inside. */
|
|
322
|
+
export const overflow = (v: number, [min, max]: readonly [number, number]): number => (v > max ? v - max : v < min ? min - v : 0);
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* The art turn of each switch, in place (`artTurn`), and the art-range demand:
|
|
326
|
+
* inside a span, per bone with a `drawn` angle in the set and limits — the
|
|
327
|
+
* set's `turn` for it, else its chain's `range` — every frame whose signed art
|
|
328
|
+
* turn wrap(target − drawn) falls outside them, in runs merged across a gap of
|
|
329
|
+
* `gap` frames. Measured on the targets as the pose asks them, before
|
|
330
|
+
* `clampArt` holds the set's `turn` limits.
|
|
331
|
+
*/
|
|
332
|
+
export function measureArt(switches: Switch[], map: JointMap, targets: ReadonlyMap<string, readonly number[]>, endOf: (i: number) => number, times: readonly number[], gap: number): ArtRange[] {
|
|
333
|
+
const out: ArtRange[] = [];
|
|
334
|
+
for (const sw of switches) {
|
|
335
|
+
const set = map.alternates.find((a) => a.name === sw.set)!;
|
|
336
|
+
for (const [bone, drawn] of Object.entries(set.drawn)) {
|
|
337
|
+
const target = targets.get(bone)!;
|
|
338
|
+
const turns: number[] = [];
|
|
339
|
+
for (let i = sw.frames[0]; i <= sw.frames[1]; i++) turns.push(wrap(target[i]! - drawn));
|
|
340
|
+
const abs = turns.map(Math.abs);
|
|
341
|
+
sw.artTurn[bone] = { max: r3(Math.max(...abs)), median: r3(median(abs)) };
|
|
342
|
+
const chain = map.chains.find((c) => c.bones.includes(bone))!;
|
|
343
|
+
const limits = set.turn[bone] ?? chain.range[bone];
|
|
344
|
+
if (limits === undefined) continue;
|
|
345
|
+
const flag = turns.map((t) => overflow(t, limits) > RANGE_SLACK);
|
|
346
|
+
for (const [a, b] of runsOf(flag, gap)) {
|
|
347
|
+
let worst = a;
|
|
348
|
+
for (let k = a; k <= b; k++) if (overflow(turns[k]!, limits) > overflow(turns[worst]!, limits)) worst = k;
|
|
349
|
+
const first = sw.frames[0] + a;
|
|
350
|
+
const last = sw.frames[0] + b;
|
|
351
|
+
out.push({ set: sw.set, chain: chain.name, bone, frames: [first, last], seconds: r6(endOf(last) - times[first]!), maxOverflow: r3(overflow(turns[worst]!, limits)), turn: r3(turns[worst]!), limits, clamped: bone in set.turn });
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return out;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** The frame nearest a time: the one whose half-frame-either-side window holds it, as the switch keys are placed. */
|
|
359
|
+
function nearestFrame(times: readonly number[], t: number): number {
|
|
360
|
+
let lo = 0;
|
|
361
|
+
let hi = times.length - 1;
|
|
362
|
+
while (lo < hi) {
|
|
363
|
+
const mid = (lo + hi) >> 1;
|
|
364
|
+
if (times[mid + 1]! <= t) lo = mid + 1;
|
|
365
|
+
else if (times[mid]! >= t) hi = mid;
|
|
366
|
+
else return t - times[mid]! < times[mid + 1]! - t ? mid : mid + 1;
|
|
367
|
+
}
|
|
368
|
+
return lo;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* The folds no switch covers: every fold time on which the slot still shows
|
|
373
|
+
* the folding attachment. A fold of a slot's base mesh while a set (or
|
|
374
|
+
* nothing) shows there is covered; a fold of the set's own mesh while it
|
|
375
|
+
* shows is not.
|
|
376
|
+
*/
|
|
377
|
+
export function uncoveredFolds(meshes: readonly MeshFolds[], shown: ReadonlyMap<string, ReadonlyArray<string | null>>, times: readonly number[], slots: RigSlots, chainsOf: (slot: string, attachment: string) => string[]): Uncovered[] {
|
|
378
|
+
const out: Uncovered[] = [];
|
|
379
|
+
for (const m of meshes) {
|
|
380
|
+
const row = shown.get(m.slot);
|
|
381
|
+
const left = m.times.filter((t) => (row === undefined ? slots.get(m.slot)?.setup ?? null : row[nearestFrame(times, t)]) === m.attachment);
|
|
382
|
+
if (left.length > 0) out.push({ slot: m.slot, attachment: m.attachment, chains: chainsOf(m.slot, m.attachment), times: left });
|
|
383
|
+
}
|
|
384
|
+
return out;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* The map's chains that move a set of bones: for each bone, the chain owning
|
|
389
|
+
* it or its nearest mapped ancestor, keeping only the deepest (a chain another
|
|
390
|
+
* listed chain hangs from is dropped — the arm, not the trunk it hangs from).
|
|
391
|
+
*/
|
|
392
|
+
export function chainsMoving(bones: readonly string[], rest: Rest, map: JointMap): string[] {
|
|
393
|
+
const owner = new Map<string, string>();
|
|
394
|
+
for (const c of map.chains) for (const b of c.bones) owner.set(b, c.name);
|
|
395
|
+
const found = new Set<string>();
|
|
396
|
+
for (const bone of bones) {
|
|
397
|
+
for (let b: string | null = bone; b !== null; b = rest.get(b)?.parent ?? null) {
|
|
398
|
+
const c = owner.get(b);
|
|
399
|
+
if (c !== undefined) {
|
|
400
|
+
found.add(c);
|
|
401
|
+
break;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
const chainOf = (name: string) => map.chains.find((c) => c.name === name)!;
|
|
406
|
+
return map.chains
|
|
407
|
+
.map((c) => c.name)
|
|
408
|
+
.filter((x) => found.has(x))
|
|
409
|
+
.filter((x) => ![...found].some((y) => y !== x && chainOf(x).bones.some((b) => descends(rest, chainOf(y).bones[0]!, b))));
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const deg = (v: number) => `${v.toFixed(1)}°`;
|
|
413
|
+
const sg = (v: number) => `${v >= 0 ? '+' : '−'}${Math.abs(v).toFixed(1)}°`;
|
|
414
|
+
const spanOf = (frames: [number, number], times: readonly number[]): string =>
|
|
415
|
+
frames[0] === frames[1] ? `frame ${frames[0]} (t=${times[frames[0]]!.toFixed(3)}s)` : `frames ${frames[0]}–${frames[1]} (t=${times[frames[0]]!.toFixed(3)}–${times[frames[1]]!.toFixed(3)}s)`;
|
|
416
|
+
|
|
417
|
+
export function switchLine(s: Switch, times: readonly number[]): string {
|
|
418
|
+
const span = s.frames[0] === s.frames[1] ? `frame ${s.frames[0]}` : `frames ${s.frames[0]}–${s.frames[1]}`;
|
|
419
|
+
const shows = Object.entries(s.slots).map(([slot, att]) => `"${slot}" = "${att}"`);
|
|
420
|
+
const parts = [
|
|
421
|
+
`set "${s.set}" — ${shows.length === 0 ? 'shows no slot' : `shows ${shows.join(', ')}`}${s.hides.length > 0 ? `, hides ${s.hides.map((h) => `"${h}"`).join(', ')}` : ''}`,
|
|
422
|
+
`chains ${s.chains.join(', ')}`,
|
|
423
|
+
`${span} (t=${times[s.frames[0]]!.toFixed(3)}s until ${s.to.toFixed(3)}s) — distance max ${deg(s.distance.max)}, median ${deg(s.distance.median)}`,
|
|
424
|
+
];
|
|
425
|
+
const offs = Object.entries(s.offsets);
|
|
426
|
+
if (offs.length > 0) parts.push(`offsets ${offs.map(([b, o]) => `"${b}" (${o[0]}, ${o[1]})`).join(', ')}`);
|
|
427
|
+
const art = Object.entries(s.artTurn);
|
|
428
|
+
if (art.length > 0) parts.push(`art turn ${art.map(([b, a]) => `"${b}" max ${deg(a.max)}, median ${deg(a.median)}`).join('; ')}`);
|
|
429
|
+
return parts.join('; ');
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export function artRangeLine(e: ArtRange, times: readonly number[]): string {
|
|
433
|
+
return `set "${e.set}" chain "${e.chain}" bone "${e.bone}" turned ${sg(e.turn)} from drawn (range [${e.limits[0]}, ${e.limits[1]}]) ${spanOf(e.frames, times)}: ${e.seconds.toFixed(3)}s, up to ${deg(e.maxOverflow)} past it; ${e.clamped ? 'held at the limit' : 'the bone follows the pose'} — demand for another drawing of the set`;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Inside each span, a bone with a `turn` in the set is held within it: its
|
|
438
|
+
* target world angle is drawn + clamp(wrap(target − drawn), min, max). Each
|
|
439
|
+
* bone is clamped on its own world angle, and keys are differences of world
|
|
440
|
+
* angles, so a forearm under a clamped upper arm keeps its own target.
|
|
441
|
+
*/
|
|
442
|
+
export function clampArt(switches: readonly Switch[], map: JointMap, targets: Map<string, number[]>): void {
|
|
443
|
+
for (const sw of switches) {
|
|
444
|
+
const set = map.alternates.find((a) => a.name === sw.set)!;
|
|
445
|
+
for (const [bone, limits] of Object.entries(set.turn)) {
|
|
446
|
+
const drawn = set.drawn[bone]!;
|
|
447
|
+
const target = targets.get(bone)!;
|
|
448
|
+
for (let i = sw.frames[0]; i <= sw.frames[1]; i++) {
|
|
449
|
+
const t = wrap(target[i]! - drawn);
|
|
450
|
+
const c = Math.min(Math.max(t, limits[0]), limits[1]);
|
|
451
|
+
if (Math.abs(c - t) > RANGE_SLACK) target[i] = wrap(drawn + c);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|