@rig-c/pose 0.22.2 → 0.24.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 +8 -6
- package/package.json +1 -1
- package/src/face.ts +295 -2
- package/src/map.ts +165 -14
- package/src/retarget.ts +297 -31
- package/src/rigc.ts +29 -2
- package/src/run.ts +34 -5
- package/src/switch.ts +172 -18
- package/src/verify.ts +30 -1
package/src/map.ts
CHANGED
|
@@ -101,20 +101,38 @@ export interface AlternateSet {
|
|
|
101
101
|
/** `place: "by-hip"` (rig-c/pose#93): the chains whose wrist `pose` puts within one hip width of the mid-hip, their wrists placed by the offset from the hip while the set shows. Absent or null: the pose's angles. */
|
|
102
102
|
place?: { by: 'hip'; chains: string[] } | null;
|
|
103
103
|
}
|
|
104
|
+
export interface YawSet {
|
|
105
|
+
set: string;
|
|
106
|
+
above: number;
|
|
107
|
+
}
|
|
104
108
|
export interface FaceAxis {
|
|
105
109
|
bone: string;
|
|
106
110
|
range: [number, number];
|
|
111
|
+
sets?: YawSet[];
|
|
107
112
|
}
|
|
108
113
|
export interface FaceMap {
|
|
109
114
|
yaw?: FaceAxis;
|
|
110
115
|
pitch?: FaceAxis;
|
|
116
|
+
sets?: YawSet[];
|
|
117
|
+
}
|
|
118
|
+
export interface BodyMap {
|
|
119
|
+
yaw?: FaceAxis;
|
|
120
|
+
sets?: YawSet[];
|
|
111
121
|
}
|
|
112
122
|
export interface JointMap {
|
|
113
123
|
chains: Chain[];
|
|
114
124
|
translate: Translate | null;
|
|
115
125
|
alternates: AlternateSet[];
|
|
116
126
|
face?: FaceMap;
|
|
127
|
+
body?: BodyMap;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** An alternate attachment set from `sets.json` (`rig-parts-sets/1`, `rig-c/pose#173`). */
|
|
131
|
+
export interface RigSet {
|
|
132
|
+
slots: Record<string, string>;
|
|
133
|
+
hides: string[];
|
|
117
134
|
}
|
|
135
|
+
export type RigSets = ReadonlyMap<string, RigSet>;
|
|
118
136
|
|
|
119
137
|
/** What a slot of the built rig shows at setup, and every attachment any skin gives it. */
|
|
120
138
|
export interface RigSlot {
|
|
@@ -144,7 +162,7 @@ export function readMap(text: string, where: string): JointMap {
|
|
|
144
162
|
p.throwIfAny();
|
|
145
163
|
throw new Error('unreachable');
|
|
146
164
|
}
|
|
147
|
-
refuseUnknownKeys(p, 'MAP_KEYS', where, doc, ['spec', 'note', 'chains', 'translate', 'alternates', 'face']);
|
|
165
|
+
refuseUnknownKeys(p, 'MAP_KEYS', where, doc, ['spec', 'note', 'chains', 'translate', 'alternates', 'face', 'body']);
|
|
148
166
|
if (doc.spec !== MAP_SPEC) p.add('MAP_SPEC', `${where} "spec" is ${typeName(doc.spec)}; this reader reads "${MAP_SPEC}"`);
|
|
149
167
|
const chains: Chain[] = [];
|
|
150
168
|
if (!Array.isArray(doc.chains) || doc.chains.length === 0) {
|
|
@@ -245,22 +263,60 @@ export function readMap(text: string, where: string): JointMap {
|
|
|
245
263
|
}
|
|
246
264
|
const alternates = 'alternates' in doc ? readAlternates(p, where, doc.alternates, chains, translate) : [];
|
|
247
265
|
const face = 'face' in doc ? readFaceMap(p, where, doc.face) : undefined;
|
|
266
|
+
const body = 'body' in doc ? readBodyMap(p, where, doc.body) : undefined;
|
|
248
267
|
p.throwIfAny();
|
|
249
|
-
return { chains, translate, alternates, ...(face === undefined ? {} : { face }) };
|
|
268
|
+
return { chains, translate, alternates, ...(face === undefined ? {} : { face }), ...(body === undefined ? {} : { body }) };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function readYawSets(p: Problems, where: string, raw: unknown, shapeCode: string): YawSet[] | undefined {
|
|
272
|
+
if (!Array.isArray(raw) || raw.length === 0) {
|
|
273
|
+
p.add(shapeCode, `${where} is ${typeName(raw)}; it must be a non-empty array of { set, above }`);
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
const out: YawSet[] = [];
|
|
277
|
+
const seen = new Set<string>();
|
|
278
|
+
raw.forEach((entry: unknown, i: number) => {
|
|
279
|
+
const ew = `${where}[${i}]`;
|
|
280
|
+
if (!isObject(entry)) {
|
|
281
|
+
p.add(shapeCode, `${ew} is ${typeName(entry)}; it is { set, above }`);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
refuseUnknownKeys(p, 'MAP_KEYS', ew, entry, ['set', 'above']);
|
|
285
|
+
const set = typeof entry.set === 'string' && entry.set !== '' ? entry.set : null;
|
|
286
|
+
if (set === null) p.add('MAP_SET_NAME', `${ew} "set" is ${typeName(entry.set)}; it must be a non-empty string`);
|
|
287
|
+
else if (seen.has(set)) p.add('MAP_SET_NAME', `${ew} "set" "${set}" names two entries`);
|
|
288
|
+
if (set !== null) seen.add(set);
|
|
289
|
+
const above = typeof entry.above === 'number' && Number.isFinite(entry.above) && entry.above > 0 ? entry.above : null;
|
|
290
|
+
if (above === null) p.add('MAP_SET_ABOVE', `${ew} "above" is ${JSON.stringify(entry.above)}; it must be a positive finite number of degrees`);
|
|
291
|
+
if (set !== null && above !== null) out.push({ set, above });
|
|
292
|
+
});
|
|
293
|
+
return out.length > 0 ? out : undefined;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function faceYawSets(face: FaceMap | undefined): YawSet[] | undefined {
|
|
297
|
+
return face?.yaw?.sets ?? face?.sets;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export function bodyYawSets(body: BodyMap | undefined): YawSet[] | undefined {
|
|
301
|
+
return body?.sets ?? body?.yaw?.sets;
|
|
250
302
|
}
|
|
251
303
|
|
|
252
304
|
function readFaceMap(p: Problems, where: string, raw: unknown): FaceMap | undefined {
|
|
253
305
|
const fw = `${where} "face"`;
|
|
254
306
|
if (!isObject(raw)) {
|
|
255
|
-
p.add('MAP_FACE_SHAPE', `${fw} is ${typeName(raw)}; it is { yaw?: { bone, range: [min, max] }, pitch?: { bone, range: [min, max] } }`);
|
|
307
|
+
p.add('MAP_FACE_SHAPE', `${fw} is ${typeName(raw)}; it is { yaw?: { bone, range: [min, max], sets? }, pitch?: { bone, range: [min, max] }, sets? }`);
|
|
256
308
|
return undefined;
|
|
257
309
|
}
|
|
258
|
-
refuseUnknownKeys(p, 'MAP_KEYS', fw, raw, ['yaw', 'pitch']);
|
|
259
|
-
if (!('yaw' in raw) && !('pitch' in raw)) {
|
|
310
|
+
refuseUnknownKeys(p, 'MAP_KEYS', fw, raw, ['yaw', 'pitch', 'sets']);
|
|
311
|
+
if (!('yaw' in raw) && !('pitch' in raw) && !('sets' in raw)) {
|
|
260
312
|
p.add('MAP_FACE_SHAPE', `${fw} names neither "yaw" nor "pitch"; it must name at least one axis`);
|
|
261
313
|
return undefined;
|
|
262
314
|
}
|
|
263
315
|
const out: FaceMap = {};
|
|
316
|
+
if ('sets' in raw) {
|
|
317
|
+
const sets = readYawSets(p, `${fw} "sets"`, raw.sets, 'MAP_FACE_SHAPE');
|
|
318
|
+
if (sets !== undefined) out.sets = sets;
|
|
319
|
+
}
|
|
264
320
|
for (const axis of ['yaw', 'pitch'] as const) {
|
|
265
321
|
if (!(axis in raw)) continue;
|
|
266
322
|
const aw = `${fw} "${axis}"`;
|
|
@@ -269,19 +325,67 @@ function readFaceMap(p: Problems, where: string, raw: unknown): FaceMap | undefi
|
|
|
269
325
|
p.add('MAP_FACE_SHAPE', `${aw} is ${typeName(ax)}; it is { bone, range: [min, max] }`);
|
|
270
326
|
continue;
|
|
271
327
|
}
|
|
272
|
-
refuseUnknownKeys(p, 'MAP_KEYS', aw, ax, ['bone', 'range']);
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
p.add('
|
|
278
|
-
|
|
279
|
-
|
|
328
|
+
refuseUnknownKeys(p, 'MAP_KEYS', aw, ax, axis === 'yaw' ? ['bone', 'range', 'sets'] : ['bone', 'range']);
|
|
329
|
+
const sets = axis === 'yaw' && 'sets' in ax ? readYawSets(p, `${aw} "sets"`, ax.sets, 'MAP_FACE_SHAPE') : undefined;
|
|
330
|
+
const hasBoneOrRange = 'bone' in ax || 'range' in ax || !('sets' in ax);
|
|
331
|
+
if (hasBoneOrRange) {
|
|
332
|
+
const bone = typeof ax.bone === 'string' && ax.bone !== '' ? ax.bone : null;
|
|
333
|
+
if (bone === null) p.add('MAP_FACE_BONE', `${aw} "bone" is ${typeName(ax.bone)}; it must name a bone`);
|
|
334
|
+
const v = ax.range;
|
|
335
|
+
if (!Array.isArray(v) || v.length !== 2 || !v.every((x) => typeof x === 'number' && Number.isFinite(x)) || (v[0] as number) > 0 || (v[1] as number) < 0 || (v[0] as number) >= (v[1] as number)) {
|
|
336
|
+
p.add('MAP_FACE_RANGE', `${aw} "range" is ${JSON.stringify(v)}; it must be [min, max], two finite numbers of degrees with min ≤ 0 ≤ max and min < max`);
|
|
337
|
+
} else if (bone !== null) {
|
|
338
|
+
out[axis] = { bone, range: [v[0] as number, v[1] as number], ...(sets === undefined ? {} : { sets }) };
|
|
339
|
+
}
|
|
340
|
+
} else if (sets !== undefined) {
|
|
341
|
+
out.sets = sets;
|
|
280
342
|
}
|
|
281
343
|
}
|
|
282
344
|
return out;
|
|
283
345
|
}
|
|
284
346
|
|
|
347
|
+
function readBodyMap(p: Problems, where: string, raw: unknown): BodyMap | undefined {
|
|
348
|
+
const bw = `${where} "body"`;
|
|
349
|
+
if (!isObject(raw)) {
|
|
350
|
+
p.add('MAP_BODY_SHAPE', `${bw} is ${typeName(raw)}; it is { yaw?: { bone, range: [min, max] }, sets?: [{ set, above }] }`);
|
|
351
|
+
return undefined;
|
|
352
|
+
}
|
|
353
|
+
refuseUnknownKeys(p, 'MAP_KEYS', bw, raw, ['yaw', 'sets']);
|
|
354
|
+
if (!('yaw' in raw) && !('sets' in raw)) {
|
|
355
|
+
p.add('MAP_BODY_SHAPE', `${bw} does not name "yaw" or "sets"; it must name at least one`);
|
|
356
|
+
return undefined;
|
|
357
|
+
}
|
|
358
|
+
const out: BodyMap = {};
|
|
359
|
+
if ('sets' in raw) {
|
|
360
|
+
const sets = readYawSets(p, `${bw} "sets"`, raw.sets, 'MAP_BODY_SHAPE');
|
|
361
|
+
if (sets !== undefined) out.sets = sets;
|
|
362
|
+
}
|
|
363
|
+
if ('yaw' in raw) {
|
|
364
|
+
const aw = `${bw} "yaw"`;
|
|
365
|
+
const ax = raw.yaw;
|
|
366
|
+
if (!isObject(ax)) {
|
|
367
|
+
p.add('MAP_BODY_SHAPE', `${aw} is ${typeName(ax)}; it is { bone, range: [min, max] }`);
|
|
368
|
+
return undefined;
|
|
369
|
+
}
|
|
370
|
+
refuseUnknownKeys(p, 'MAP_KEYS', aw, ax, ['bone', 'range', 'sets']);
|
|
371
|
+
const sets = 'sets' in ax ? readYawSets(p, `${aw} "sets"`, ax.sets, 'MAP_BODY_SHAPE') : undefined;
|
|
372
|
+
const hasBoneOrRange = 'bone' in ax || 'range' in ax || !('sets' in ax);
|
|
373
|
+
if (hasBoneOrRange) {
|
|
374
|
+
const bone = typeof ax.bone === 'string' && ax.bone !== '' ? ax.bone : null;
|
|
375
|
+
if (bone === null) p.add('MAP_BODY_BONE', `${aw} "bone" is ${typeName(ax.bone)}; it must name a bone`);
|
|
376
|
+
const v = ax.range;
|
|
377
|
+
if (!Array.isArray(v) || v.length !== 2 || !v.every((x) => typeof x === 'number' && Number.isFinite(x)) || (v[0] as number) > 0 || (v[1] as number) < 0 || (v[0] as number) >= (v[1] as number)) {
|
|
378
|
+
p.add('MAP_BODY_RANGE', `${aw} "range" is ${JSON.stringify(v)}; it must be [min, max], two finite numbers of degrees with min ≤ 0 ≤ max and min < max`);
|
|
379
|
+
return undefined;
|
|
380
|
+
}
|
|
381
|
+
if (bone !== null) out.yaw = { bone, range: [v[0] as number, v[1] as number], ...(sets === undefined ? {} : { sets }) };
|
|
382
|
+
} else if (sets !== undefined) {
|
|
383
|
+
out.sets = sets;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
387
|
+
}
|
|
388
|
+
|
|
285
389
|
const SET_SHAPE = '{ name, slots: { "<slot>": "<attachment>" }, hides?: [slot], chains: [chain], pose: { "<joint>": [x, y] }, drawn?: { "<bone>": deg }, offsets?: { "<bone>": [dx, dy] }, turn?: { "<bone>": [min, max] }, frames?: [[first, last], …], place?: "by-hip" }';
|
|
286
390
|
|
|
287
391
|
/** The joints a wrist is, for `place: "by-hip"`: a chain whose last joint is one of them carries a wrist. */
|
|
@@ -535,8 +639,15 @@ export function rigPoint(rest: Rest, spec: string): [number, number] | null {
|
|
|
535
639
|
return pointAt(rest, new Map([...rest].map(([n, b]) => [n, b.length])), spec);
|
|
536
640
|
}
|
|
537
641
|
|
|
642
|
+
export function knownSetsOf(map: JointMap, rigSets: RigSets = new Map()): Map<string, RigSet> {
|
|
643
|
+
const out = new Map<string, RigSet>();
|
|
644
|
+
for (const [k, v] of rigSets) out.set(k, v);
|
|
645
|
+
for (const a of map.alternates) if (!out.has(a.name)) out.set(a.name, { slots: a.slots, hides: a.hides });
|
|
646
|
+
return out;
|
|
647
|
+
}
|
|
648
|
+
|
|
538
649
|
/** Check every name the map gives against the rig; every miss is named. */
|
|
539
|
-
export function resolveMap(map: JointMap, rest: Rest, where: string, slots: RigSlots = new Map()): void {
|
|
650
|
+
export function resolveMap(map: JointMap, rest: Rest, where: string, slots: RigSlots = new Map(), rigSets: RigSets = new Map()): void {
|
|
540
651
|
const p = new Problems();
|
|
541
652
|
const all = [...rest.keys()];
|
|
542
653
|
const listed = all.length <= 40 ? all.join(', ') : `${all.slice(0, 40).join(', ')} … (${all.length} in all)`;
|
|
@@ -619,6 +730,46 @@ export function resolveMap(map: JointMap, rest: Rest, where: string, slots: RigS
|
|
|
619
730
|
if (Math.hypot(o.a, o.c) < 1e-9) p.add('MAP_AIM_LENGTH', `${label} — bone "${ax.bone}" has a collapsed x axis at setup (scale 0)`);
|
|
620
731
|
}
|
|
621
732
|
}
|
|
733
|
+
if (map.body?.yaw !== undefined) {
|
|
734
|
+
const ax = map.body.yaw;
|
|
735
|
+
const label = '"body.yaw"';
|
|
736
|
+
if (!rest.has(ax.bone)) {
|
|
737
|
+
p.add('MAP_BONE_EXISTS', `${label} — bone "${ax.bone}" is not in the rig; its bones are ${listed}`);
|
|
738
|
+
} else {
|
|
739
|
+
const prev = owner.get(ax.bone);
|
|
740
|
+
if (prev !== undefined) p.add('MAP_BONE_ONCE', `bone "${ax.bone}" is in ${prev.startsWith('"') ? prev : `chain "${prev}"`} and in ${label}; a bone carries one rotate track`);
|
|
741
|
+
owner.set(ax.bone, label);
|
|
742
|
+
const o = rest.get(ax.bone)!;
|
|
743
|
+
if (Math.hypot(o.a, o.c) < 1e-9) p.add('MAP_AIM_LENGTH', `${label} — bone "${ax.bone}" has a collapsed x axis at setup (scale 0)`);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
const knownSets = knownSetsOf(map, rigSets);
|
|
747
|
+
const allSets = [...knownSets.keys()];
|
|
748
|
+
const setsListed = allSets.length === 0 ? 'none' : allSets.join(', ');
|
|
749
|
+
const allSlots = [...slots.keys()];
|
|
750
|
+
const slotsListed = allSlots.length === 0 ? 'none' : allSlots.length <= 40 ? allSlots.join(', ') : `${allSlots.slice(0, 40).join(', ')} … (${allSlots.length} in all)`;
|
|
751
|
+
for (const [label, declared] of [['"face.yaw.sets"', faceYawSets(map.face)], ['"body.sets"', bodyYawSets(map.body)]] as const) {
|
|
752
|
+
if (declared === undefined) continue;
|
|
753
|
+
for (const s of declared) {
|
|
754
|
+
const def = knownSets.get(s.set);
|
|
755
|
+
if (def === undefined) {
|
|
756
|
+
p.add('MAP_SET_EXISTS', `${label} — set "${s.set}" is not in the rig; its sets are ${setsListed}`);
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
if (slots.size === 0) continue;
|
|
760
|
+
for (const [slot, att] of Object.entries(def.slots)) {
|
|
761
|
+
const rs = slots.get(slot);
|
|
762
|
+
if (rs === undefined) p.add('MAP_ALT_SLOT', `set "${s.set}" — slot "${slot}" is not in the rig; its slots are ${slotsListed}`);
|
|
763
|
+
else if (!rs.attachments.has(att)) {
|
|
764
|
+
const has = [...rs.attachments];
|
|
765
|
+
p.add('MAP_ALT_ATTACHMENT', `set "${s.set}" — slot "${slot}" has no attachment "${att}" under any skin; it has ${has.length === 0 ? 'none' : has.join(', ')}`);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
for (const h of def.hides) {
|
|
769
|
+
if (!slots.has(h)) p.add('MAP_ALT_HIDES', `set "${s.set}" hides slot "${h}", which is not in the rig; its slots are ${slotsListed}`);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
622
773
|
resolveAlternates(p, map.alternates, slots);
|
|
623
774
|
if (p.any) p.list.unshift({ code: 'MAP_RESOLVES', text: `${where} does not resolve against the rig` });
|
|
624
775
|
p.throwIfAny();
|