@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/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 bone = typeof ax.bone === 'string' && ax.bone !== '' ? ax.bone : null;
274
- if (bone === null) p.add('MAP_FACE_BONE', `${aw} "bone" is ${typeName(ax.bone)}; it must name a bone`);
275
- const v = ax.range;
276
- 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)) {
277
- 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`);
278
- } else if (bone !== null) {
279
- out[axis] = { bone, range: [v[0] as number, v[1] as number] };
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();