@rig-c/pose 0.37.1 → 0.39.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 +73 -6
- package/cli.ts +73 -6
- package/package.json +1 -1
- package/src/clean.ts +72 -3
- package/src/depth.ts +190 -0
- package/src/legs.ts +189 -0
- package/src/map.ts +24 -3
- package/src/outdir.ts +54 -0
- package/src/pelvis.ts +498 -0
- package/src/pose.ts +258 -23
- package/src/problems.ts +6 -3
- package/src/retarget.ts +98 -3
- package/src/run.ts +80 -21
- package/src/runcompare.ts +6 -7
- package/src/switch.ts +109 -21
- package/src/verify.ts +90 -9
package/src/pose.ts
CHANGED
|
@@ -17,11 +17,73 @@
|
|
|
17
17
|
* is: the unit is `picture-px` (a video frame is not a painting), and `at` may
|
|
18
18
|
* lie outside the picture (a dancer's hand leaves the frame; a pose model
|
|
19
19
|
* places it anyway, and its score says how much to believe it).
|
|
20
|
+
*
|
|
21
|
+
* The minor (rig-c/pose#288): `rig-pose-sequence/1.1` adds the pelvis track —
|
|
22
|
+
* a per-frame `pelvisTilt` and the sequence's `pelvisSource` (`rig-pose
|
|
23
|
+
* pelvis`). The writer writes the minor the content needs: a sequence with the
|
|
24
|
+
* track is "rig-pose-sequence/1.1", every other one "rig-pose-sequence/1", so
|
|
25
|
+
* a sequence without it stays readable by a reader of /1. The spec string is
|
|
26
|
+
* the version the file records of what wrote it: a file of a minor newer than
|
|
27
|
+
* this reader's is read, and each key it does not know is refused naming the
|
|
28
|
+
* minor the file says it was written as.
|
|
29
|
+
*
|
|
30
|
+
* `rig-pose-sequence/1.2` (rig-c/pose#281) adds derived depth: a per-frame
|
|
31
|
+
* `depth`, one `{ ratio, sign, source }` per limb segment, written by `clean
|
|
32
|
+
* --depth walk|run` (`source: "derived"`) or by a 3D detector (`"measured"`,
|
|
33
|
+
* read and never written here). A minor includes the keys of the minors before
|
|
34
|
+
* it, so a sequence with depth is "/1.2" whether or not it also carries the
|
|
35
|
+
* pelvis track; one with the track and no depth stays "/1.1".
|
|
20
36
|
*/
|
|
21
37
|
import { BODY18, COCO17, COCO17_TO_BODY18, isBody18, type Body18 } from './joints.ts';
|
|
22
|
-
import { isObject, parseJson, Problems, refuseUnknownKeys, typeName } from './problems.ts';
|
|
38
|
+
import { isObject, parseJson, Problems, refuseUnknownKeys, refuseUnknownKeys as refuseKeys, typeName } from './problems.ts';
|
|
23
39
|
|
|
24
40
|
export const POSE_SPEC = 'rig-pose-sequence/1';
|
|
41
|
+
/** The minor with the pelvis track (rig-c/pose#288). */
|
|
42
|
+
export const POSE_SPEC_PELVIS = 'rig-pose-sequence/1.1';
|
|
43
|
+
/** The minor with derived depth (rig-c/pose#281), and the newest minor this reader reads. */
|
|
44
|
+
export const POSE_SPEC_DEPTH = 'rig-pose-sequence/1.2';
|
|
45
|
+
const POSE_MINOR = 2;
|
|
46
|
+
/**
|
|
47
|
+
* The limb segments a frame's `depth` names (rig-c/pose#281), each from its
|
|
48
|
+
* proximal joint to its distal one: the sign is + when the distal joint is
|
|
49
|
+
* nearer the camera than the proximal one.
|
|
50
|
+
*/
|
|
51
|
+
export const DEPTH_SEGMENTS = {
|
|
52
|
+
r_upper_arm: ['r_shoulder', 'r_elbow'],
|
|
53
|
+
r_forearm: ['r_elbow', 'r_wrist'],
|
|
54
|
+
r_thigh: ['r_hip', 'r_knee'],
|
|
55
|
+
r_shin: ['r_knee', 'r_ankle'],
|
|
56
|
+
l_upper_arm: ['l_shoulder', 'l_elbow'],
|
|
57
|
+
l_forearm: ['l_elbow', 'l_wrist'],
|
|
58
|
+
l_thigh: ['l_hip', 'l_knee'],
|
|
59
|
+
l_shin: ['l_knee', 'l_ankle'],
|
|
60
|
+
} as const satisfies Record<string, readonly [Body18, Body18]>;
|
|
61
|
+
export type DepthSegment = keyof typeof DEPTH_SEGMENTS;
|
|
62
|
+
export const DEPTH_SEGMENT_NAMES = Object.keys(DEPTH_SEGMENTS) as DepthSegment[];
|
|
63
|
+
/** Where a segment's depth came from: read off the picture's 2D lengths (`clean --depth`), or a 3D detector's. */
|
|
64
|
+
export const DEPTH_SOURCES = ['derived', 'measured'] as const;
|
|
65
|
+
export type DepthSource = (typeof DEPTH_SOURCES)[number];
|
|
66
|
+
/**
|
|
67
|
+
* One segment's depth on one frame (rig-c/pose#281): `ratio` its drawn length ÷
|
|
68
|
+
* its rest length; `sign` +1 when its distal joint is nearer the camera than its
|
|
69
|
+
* proximal one, −1 farther, 0 flat (or not decided); `source` as above.
|
|
70
|
+
*/
|
|
71
|
+
export interface SegmentDepth {
|
|
72
|
+
ratio: number;
|
|
73
|
+
sign: -1 | 0 | 1;
|
|
74
|
+
source: DepthSource;
|
|
75
|
+
}
|
|
76
|
+
export type FrameDepth = Partial<Record<DepthSegment, SegmentDepth>>;
|
|
77
|
+
/** The gait `clean --depth` was told the clip is: it decides the sign of a foreshortened shin. */
|
|
78
|
+
export const DEPTH_GAITS = ['walk', 'run'] as const;
|
|
79
|
+
export type DepthGait = (typeof DEPTH_GAITS)[number];
|
|
80
|
+
/**
|
|
81
|
+
* Where a sequence's `pelvisTilt` came from (rig-c/pose#288): `silhouette` — the
|
|
82
|
+
* straight edge of the drawn figure across the hips; `waistband` — the top edge
|
|
83
|
+
* of a garment covering them; `keys` — a hand file (`--pelvis-keys`).
|
|
84
|
+
*/
|
|
85
|
+
export const PELVIS_SOURCES = ['silhouette', 'waistband', 'keys'] as const;
|
|
86
|
+
export type PelvisSource = (typeof PELVIS_SOURCES)[number];
|
|
25
87
|
export const POSE_SPACE = { units: 'picture-px', origin: 'top-left', y: 'down' } as const;
|
|
26
88
|
/**
|
|
27
89
|
* `inferred` (rig-c/pose#63): a position the detector supplied but did not see
|
|
@@ -44,6 +106,21 @@ export interface Person {
|
|
|
44
106
|
export interface PoseFrame {
|
|
45
107
|
t: number;
|
|
46
108
|
people: Person[];
|
|
109
|
+
/**
|
|
110
|
+
* rig-c/pose#288, minor 1.1: the pelvis line on this frame, read off the picture —
|
|
111
|
+
* Spine degrees (counter-clockwise, y up) of the direction from the figure's
|
|
112
|
+
* right hip to its left hip, the angle `pictureAngle(r_hip, l_hip)` gives the
|
|
113
|
+
* hip keypoints (about 0 for a figure facing the camera, about ±180 facing
|
|
114
|
+
* away); null where the reading declined the frame. Present on every frame of
|
|
115
|
+
* a sequence with a `pelvisSource`, on none of one without.
|
|
116
|
+
*/
|
|
117
|
+
pelvisTilt?: number | null;
|
|
118
|
+
/**
|
|
119
|
+
* rig-c/pose#281, minor 1.2: each limb segment's depth on this frame (a segment
|
|
120
|
+
* whose joints are not both placed on the frame is not named). Present on every
|
|
121
|
+
* frame of a sequence with depth, on none of one without.
|
|
122
|
+
*/
|
|
123
|
+
depth?: FrameDepth;
|
|
47
124
|
}
|
|
48
125
|
/**
|
|
49
126
|
* The picture's camera on one frame against frame 0's (rig-c/pose#63), as
|
|
@@ -175,6 +252,14 @@ export interface CleanUnheld {
|
|
|
175
252
|
test: 'speed' | 'reach' | 'short' | 'span';
|
|
176
253
|
value: number | null;
|
|
177
254
|
}
|
|
255
|
+
/** rig-c/pose#288: clean smoothed the sequence's `pelvisTilt` as its own track, over the legs' window (`loop` as the record's). */
|
|
256
|
+
export interface PelvisParams {
|
|
257
|
+
window: number;
|
|
258
|
+
}
|
|
259
|
+
/** rig-c/pose#281: clean derived each frame's `depth` (`source: "derived"`) for this gait. */
|
|
260
|
+
export interface DepthParams {
|
|
261
|
+
gait: DepthGait;
|
|
262
|
+
}
|
|
178
263
|
export interface CleanRecord {
|
|
179
264
|
spec: typeof CLEAN_SPEC;
|
|
180
265
|
/**
|
|
@@ -182,7 +267,7 @@ export interface CleanRecord {
|
|
|
182
267
|
* `hold` (#91): the stillness hold's figures, or "off"; absent in a record
|
|
183
268
|
* written before #91.
|
|
184
269
|
*/
|
|
185
|
-
params: { loop: boolean; legs: number; rest: number; hipsInferred?: HipsInferred; hold?: HoldParams | 'off' };
|
|
270
|
+
params: { loop: boolean; legs: number; rest: number; hipsInferred?: HipsInferred; hold?: HoldParams | 'off'; pelvis?: PelvisParams; depth?: DepthParams };
|
|
186
271
|
/**
|
|
187
272
|
* `found`: every joint the spike step replaced, by frame (index into `frames`)
|
|
188
273
|
* and joint. `untested`: the frames the spike step did not test, because a
|
|
@@ -215,6 +300,8 @@ export interface PoseSequence {
|
|
|
215
300
|
* by hand. Absent: the figure's travel in the picture is its travel.
|
|
216
301
|
*/
|
|
217
302
|
travel?: Travel;
|
|
303
|
+
/** rig-c/pose#288, minor 1.1: where every frame's `pelvisTilt` came from; present exactly when the frames carry it. */
|
|
304
|
+
pelvisSource?: PelvisSource;
|
|
218
305
|
frames: PoseFrame[];
|
|
219
306
|
}
|
|
220
307
|
|
|
@@ -246,9 +333,22 @@ export function readPoseSequence(text: string, where: string): PoseSequence {
|
|
|
246
333
|
throw new Error('unreachable');
|
|
247
334
|
}
|
|
248
335
|
const frames0 = doc.spec === POSE_SPEC_FRAMES0;
|
|
249
|
-
|
|
336
|
+
// rig-c/pose#288: the minor the file says it was written as; a newer one is read, and what it adds is refused naming it.
|
|
337
|
+
const minor = frames0 ? 0 : minorOf(doc.spec);
|
|
338
|
+
const note = minor !== null && minor > POSE_MINOR ? `; the file says it was written as ${JSON.stringify(doc.spec)}, a newer minor than this reader's "${POSE_SPEC_DEPTH}" — read it with a rig-pose that reads that minor` : '';
|
|
339
|
+
const pelvisKnown = minor !== null && minor >= 1;
|
|
340
|
+
// rig-c/pose#281: depth is minor 2's.
|
|
341
|
+
const depthKnown = minor !== null && minor >= 2;
|
|
342
|
+
/** A key of a later minor in a file that says an earlier one: one line naming the minor, not an unknown key. */
|
|
343
|
+
const minorKey = (key: string, on: string) => (key === 'depth'
|
|
344
|
+
? p.add('POSE_SPEC', `${on} carries "depth", a key of "${POSE_SPEC_DEPTH}", and the file says ${JSON.stringify(doc.spec)}; a sequence carrying depth is written as "${POSE_SPEC_DEPTH}" (rig-pose clean --depth)`)
|
|
345
|
+
: p.add('POSE_SPEC', `${on} carries ${JSON.stringify(key)}, a key of "${POSE_SPEC_PELVIS}", and the file says ${JSON.stringify(doc.spec)}; a sequence carrying the pelvis track is written as "${POSE_SPEC_PELVIS}" (rig-pose pelvis)`));
|
|
346
|
+
const without = (o: Record<string, unknown>, key: string) => (pelvisKnown || !(key in o) ? o : Object.fromEntries(Object.entries(o).filter(([k]) => k !== key)));
|
|
347
|
+
const withoutDepth = (o: Record<string, unknown>) => (depthKnown || !('depth' in o) ? o : Object.fromEntries(Object.entries(o).filter(([k]) => k !== 'depth')));
|
|
348
|
+
if (!pelvisKnown && 'pelvisSource' in doc) minorKey('pelvisSource', where);
|
|
349
|
+
refuseUnknownKeys(p, 'POSE_KEYS', where, without(doc, 'pelvisSource'), frames0 ? [...TOP_KEYS, ...OPTIONAL_KEYS, 'fps'] : [...TOP_KEYS, ...OPTIONAL_KEYS, ...(pelvisKnown ? ['pelvisSource'] : [])], note);
|
|
250
350
|
for (const k of TOP_KEYS) if (!(k in doc)) p.add('POSE_KEYS', `${where} has no "${k}"; a ${POSE_SPEC} document carries ${TOP_KEYS.join(', ')}`);
|
|
251
|
-
if ('spec' in doc &&
|
|
351
|
+
if ('spec' in doc && minor === null) p.add('POSE_SPEC', `${where} "spec" is ${typeName(doc.spec)}; this reader reads "${POSE_SPEC}" and its minors ("${POSE_SPEC_PELVIS}", "${POSE_SPEC_DEPTH}"; a newer minor is read, and a key it adds is refused naming it), and the older name "${POSE_SPEC_FRAMES0}"`);
|
|
252
352
|
const space = frames0 ? FRAMES0_SPACE : POSE_SPACE;
|
|
253
353
|
if ('space' in doc && JSON.stringify(doc.space) !== JSON.stringify(space)) {
|
|
254
354
|
p.add('POSE_SPACE', `${where} "space" is ${JSON.stringify(doc.space)}; for "${frames0 ? POSE_SPEC_FRAMES0 : POSE_SPEC}" it must be exactly ${JSON.stringify(space)}`);
|
|
@@ -264,13 +364,24 @@ export function readPoseSequence(text: string, where: string): PoseSequence {
|
|
|
264
364
|
p.add('POSE_FRAMES', `${where} "frames" is ${typeName(doc.frames)}; it must be a non-empty array`);
|
|
265
365
|
} else {
|
|
266
366
|
let lastT = -Infinity;
|
|
367
|
+
const minorFrames: number[] = [];
|
|
368
|
+
const depthFrames: number[] = [];
|
|
267
369
|
doc.frames.forEach((f: unknown, i: number) => {
|
|
268
370
|
const fw = `${where} frames[${i}]`;
|
|
269
371
|
if (!isObject(f)) {
|
|
270
372
|
p.add('POSE_FRAME_SHAPE', `${fw} is ${typeName(f)}; a frame is { t, people }`);
|
|
271
373
|
return;
|
|
272
374
|
}
|
|
273
|
-
|
|
375
|
+
if (!pelvisKnown && 'pelvisTilt' in f) minorFrames.push(i);
|
|
376
|
+
if (!depthKnown && 'depth' in f) depthFrames.push(i);
|
|
377
|
+
refuseUnknownKeys(p, 'POSE_KEYS', fw, withoutDepth(without(f, 'pelvisTilt')), [...(pelvisKnown ? ['t', 'people', 'pelvisTilt'] : ['t', 'people']), ...(depthKnown ? ['depth'] : [])], note);
|
|
378
|
+
const depth = depthKnown && 'depth' in f ? readDepth(p, fw, f.depth) : undefined;
|
|
379
|
+
let pelvisTilt: number | null | undefined;
|
|
380
|
+
if (pelvisKnown && 'pelvisTilt' in f) {
|
|
381
|
+
const v = f.pelvisTilt;
|
|
382
|
+
if (v === null || (typeof v === 'number' && Number.isFinite(v) && v > -180 && v <= 180)) pelvisTilt = v;
|
|
383
|
+
else p.add('POSE_PELVIS', `${fw} "pelvisTilt" is ${typeName(v)}; it must be Spine degrees in (-180, 180] (the direction r_hip → l_hip, counter-clockwise, y up), or null where the reading declined the frame`);
|
|
384
|
+
}
|
|
274
385
|
const t = f.t;
|
|
275
386
|
if (typeof t !== 'number' || !Number.isFinite(t) || t < 0) {
|
|
276
387
|
p.add('POSE_TIME', `${fw} "t" is ${typeName(t)}; it must be a finite number of seconds ≥ 0`);
|
|
@@ -289,34 +400,133 @@ export function readPoseSequence(text: string, where: string): PoseSequence {
|
|
|
289
400
|
p.add('POSE_PERSON_SHAPE', `${pw} is ${typeName(person)}; a person is { id, joints }`);
|
|
290
401
|
return;
|
|
291
402
|
}
|
|
292
|
-
refuseUnknownKeys(p, 'POSE_KEYS', pw, person, ['id', 'joints']);
|
|
403
|
+
refuseUnknownKeys(p, 'POSE_KEYS', pw, person, ['id', 'joints'], note);
|
|
293
404
|
if (typeof person.id !== 'string' || person.id === '') {
|
|
294
405
|
p.add('POSE_PERSON_ID', `${pw} "id" is ${typeName(person.id)}; it must be a non-empty string`);
|
|
295
406
|
return;
|
|
296
407
|
}
|
|
297
408
|
if (ids.has(person.id)) p.add('POSE_PERSON_ID', `${pw} "id" "${person.id}" appears twice in frame ${i}`);
|
|
298
409
|
ids.add(person.id);
|
|
299
|
-
people.push({ id: person.id, joints: readJoints(p, pw, person.joints) });
|
|
410
|
+
people.push({ id: person.id, joints: readJoints(p, pw, person.joints, note) });
|
|
300
411
|
});
|
|
301
412
|
}
|
|
302
|
-
frames.push({ t: typeof t === 'number' ? t : NaN, people });
|
|
413
|
+
frames.push({ t: typeof t === 'number' ? t : NaN, people, ...(pelvisTilt !== undefined ? { pelvisTilt } : {}), ...(depth !== undefined ? { depth } : {}) });
|
|
303
414
|
});
|
|
415
|
+
const framesText = (fs: number[]) => `${where} ${fs.length === 1 ? `frames[${fs[0]}]` : `${fs.length} frames (${fs[0]}–${fs[fs.length - 1]})`}`;
|
|
416
|
+
if (minorFrames.length > 0) minorKey('pelvisTilt', framesText(minorFrames));
|
|
417
|
+
if (depthFrames.length > 0) minorKey('depth', framesText(depthFrames));
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
let pelvisSource: PelvisSource | undefined;
|
|
421
|
+
if (pelvisKnown && 'pelvisSource' in doc) {
|
|
422
|
+
if (typeof doc.pelvisSource === 'string' && (PELVIS_SOURCES as readonly string[]).includes(doc.pelvisSource)) pelvisSource = doc.pelvisSource as PelvisSource;
|
|
423
|
+
else p.add('POSE_PELVIS', `${where} "pelvisSource" is ${typeName(doc.pelvisSource)}; it must be one of ${PELVIS_SOURCES.map((x) => `"${x}"`).join(', ')} (where every frame's "pelvisTilt" came from)`);
|
|
424
|
+
}
|
|
425
|
+
if (pelvisKnown && Array.isArray(doc.frames)) {
|
|
426
|
+
const carry = doc.frames.map((f: unknown) => isObject(f) && 'pelvisTilt' in f);
|
|
427
|
+
const lacking = carry.flatMap((c: boolean, i: number) => (c ? [] : [i]));
|
|
428
|
+
if ('pelvisSource' in doc && lacking.length > 0) p.add('POSE_PELVIS', `${where} has "pelvisSource" and frame(s) ${spansText(lacking)} carry no "pelvisTilt"; with a pelvis track every frame carries one (null where the reading declined it)`);
|
|
429
|
+
if (!('pelvisSource' in doc) && lacking.length < carry.length) p.add('POSE_PELVIS', `${where} has "pelvisTilt" on ${carry.length - lacking.length} frame(s) and no "pelvisSource"; a pelvis track says where it came from (${PELVIS_SOURCES.join(', ')})`);
|
|
430
|
+
if ('pelvisSource' in doc) {
|
|
431
|
+
const ids = [...new Set(frames.flatMap((f) => f.people.map((q) => q.id)))].sort();
|
|
432
|
+
if (ids.length > 1) p.add('POSE_PELVIS', `${where} carries a pelvis track and ${ids.length} people (${ids.join(', ')}); the track is one figure's, so the sequence holds one person (rig-pose pelvis --person writes only that one)`);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
if (depthKnown && Array.isArray(doc.frames)) {
|
|
436
|
+
const carry = doc.frames.map((f: unknown) => isObject(f) && 'depth' in f);
|
|
437
|
+
const lacking = carry.flatMap((c: boolean, i: number) => (c ? [] : [i]));
|
|
438
|
+
if (lacking.length > 0 && lacking.length < carry.length) p.add('POSE_DEPTH', `${where} has "depth" on ${carry.length - lacking.length} frame(s) and none on frame(s) ${spansText(lacking)}; a sequence with depth carries it on every frame (an empty object where no segment is placed)`);
|
|
439
|
+
if (lacking.length < carry.length) {
|
|
440
|
+
const ids = [...new Set(frames.flatMap((f) => f.people.map((q) => q.id)))].sort();
|
|
441
|
+
if (ids.length > 1) p.add('POSE_DEPTH', `${where} carries depth and ${ids.length} people (${ids.join(', ')}); the depth is one figure's, so the sequence holds one person (rig-pose clean writes only the one it cleaned)`);
|
|
304
442
|
}
|
|
305
443
|
}
|
|
306
|
-
const camera = 'camera' in doc ? readCamera(p, where, doc.camera, Array.isArray(doc.frames) ? doc.frames.length : null) : undefined;
|
|
444
|
+
const camera = 'camera' in doc ? readCamera(p, where, doc.camera, Array.isArray(doc.frames) ? doc.frames.length : null, note) : undefined;
|
|
307
445
|
if ('cameraSource' in doc) {
|
|
308
446
|
if (typeof doc.cameraSource !== 'string' || doc.cameraSource.trim() === '') p.add('POSE_CAMERA_SOURCE', `${where} "cameraSource" is ${typeName(doc.cameraSource)}; it must be a non-empty string saying where the camera came from ("tracked", "hand-labelled, N keys")`);
|
|
309
447
|
else if (!('camera' in doc)) p.add('POSE_CAMERA_SOURCE', `${where} has "cameraSource" ${JSON.stringify(doc.cameraSource)} and no "camera"; it states where a camera came from, so it needs one`);
|
|
310
448
|
}
|
|
311
449
|
if ('travel' in doc && doc.travel !== TRAVEL_IN_PLACE) p.add('POSE_TRAVEL', `${where} "travel" is ${typeName(doc.travel)}; the one value is "${TRAVEL_IN_PLACE}" (a gait filmed without the figure travelling), and without the key the picture's travel is the figure's`);
|
|
312
|
-
const cleaned = 'cleaned' in doc ? readCleaned(p, where, doc.cleaned, Array.isArray(doc.frames) ? doc.frames.length : null) : undefined;
|
|
450
|
+
const cleaned = 'cleaned' in doc ? readCleaned(p, where, doc.cleaned, Array.isArray(doc.frames) ? doc.frames.length : null, pelvisKnown, note, depthKnown) : undefined;
|
|
451
|
+
if (depthKnown) {
|
|
452
|
+
// rig-c/pose#281: "derived" depth is clean's, and clean records the gait it read it for; "measured" is a 3D detector's.
|
|
453
|
+
const sources = new Set(frames.flatMap((f) => Object.values(f.depth ?? {}).map((d) => d.source)));
|
|
454
|
+
const recorded = isObject(doc.cleaned) && isObject(doc.cleaned.params) && 'depth' in doc.cleaned.params;
|
|
455
|
+
if (sources.size > 1) p.add('POSE_DEPTH', `${where} carries "derived" and "measured" depth together; a sequence's depth comes from one source — clean --depth derives it, a 3D detector measures it`);
|
|
456
|
+
else if (sources.has('derived') && !recorded) p.add('POSE_DEPTH', `${where} carries "derived" depth and ${'cleaned' in doc ? 'its "cleaned" record has no "params" "depth"' : 'no "cleaned" record'}; derived depth is written by rig-pose clean --depth walk|run, which records the gait it was read for`);
|
|
457
|
+
if (recorded && cleaned !== undefined && frames.some((f) => f.depth === undefined || Object.values(f.depth).some((d) => d.source !== 'derived'))) p.add('POSE_DEPTH', `${where} "cleaned" "params" has "depth" and the frames do not all carry derived depth; the record says clean derived it on every frame`);
|
|
458
|
+
}
|
|
459
|
+
if (cleaned !== undefined && pelvisKnown) {
|
|
460
|
+
const hasPelvis = isObject(doc.cleaned) && isObject(doc.cleaned.params) && 'pelvis' in doc.cleaned.params;
|
|
461
|
+
if ('pelvisSource' in doc && !hasPelvis) p.add('POSE_PELVIS', `${where} carries a pelvis track and its "cleaned" record has no "params" "pelvis"; clean smooths the track and records it, so the track was added after clean — run rig-pose pelvis before rig-pose clean`);
|
|
462
|
+
if (!('pelvisSource' in doc) && hasPelvis) p.add('POSE_PELVIS', `${where} "cleaned" "params" has "pelvis" and the sequence no "pelvisSource"; the record says clean smoothed a pelvis track the frames do not carry`);
|
|
463
|
+
}
|
|
313
464
|
p.throwIfAny();
|
|
314
465
|
const cameraSource = typeof doc.cameraSource === 'string' ? doc.cameraSource : undefined;
|
|
315
|
-
return { spec: POSE_SPEC, space: POSE_SPACE, width: doc.width as number, height: doc.height as number, source: doc.source as string, ...(cameraSource !== undefined ? { cameraSource } : {}), ...(camera !== undefined ? { camera } : {}), ...(cleaned !== undefined ? { cleaned } : {}), ...(doc.travel === TRAVEL_IN_PLACE ? { travel: TRAVEL_IN_PLACE } : {}), frames };
|
|
466
|
+
return { spec: POSE_SPEC, space: POSE_SPACE, width: doc.width as number, height: doc.height as number, source: doc.source as string, ...(cameraSource !== undefined ? { cameraSource } : {}), ...(camera !== undefined ? { camera } : {}), ...(cleaned !== undefined ? { cleaned } : {}), ...(doc.travel === TRAVEL_IN_PLACE ? { travel: TRAVEL_IN_PLACE } : {}), ...(pelvisSource !== undefined ? { pelvisSource } : {}), frames };
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/** rig-c/pose#281: a frame's `depth` — `{ "<segment>": { ratio, sign, source } }`, every part refused by name. */
|
|
470
|
+
function readDepth(p: Problems, fw: string, d: unknown): FrameDepth | undefined {
|
|
471
|
+
const dw = `${fw} "depth"`;
|
|
472
|
+
if (!isObject(d)) {
|
|
473
|
+
p.add('POSE_DEPTH', `${dw} is ${typeName(d)}; it must be an object keyed by limb segment (${DEPTH_SEGMENT_NAMES.join(', ')}), each { ratio, sign, source }`);
|
|
474
|
+
return undefined;
|
|
475
|
+
}
|
|
476
|
+
const out: FrameDepth = {};
|
|
477
|
+
for (const [name, v] of Object.entries(d)) {
|
|
478
|
+
const sw = `${dw} "${name}"`;
|
|
479
|
+
if (!(DEPTH_SEGMENT_NAMES as readonly string[]).includes(name)) {
|
|
480
|
+
p.add('POSE_DEPTH', `${dw} names ${JSON.stringify(name)}; the segments are ${DEPTH_SEGMENT_NAMES.join(', ')}`);
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
if (!isObject(v)) {
|
|
484
|
+
p.add('POSE_DEPTH', `${sw} is ${typeName(v)}; it must be { ratio, sign, source }`);
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
refuseUnknownKeys(p, 'POSE_DEPTH', sw, v, ['ratio', 'sign', 'source']);
|
|
488
|
+
let ok = true;
|
|
489
|
+
for (const k of ['ratio', 'sign', 'source']) if (!(k in v)) {
|
|
490
|
+
p.add('POSE_DEPTH', `${sw} has no "${k}"; a segment's depth is { ratio, sign, source }`);
|
|
491
|
+
ok = false;
|
|
492
|
+
}
|
|
493
|
+
if ('ratio' in v && !(typeof v.ratio === 'number' && Number.isFinite(v.ratio) && v.ratio >= 0)) {
|
|
494
|
+
p.add('POSE_DEPTH', `${sw} "ratio" is ${typeName(v.ratio)}; it must be a finite number ≥ 0 (the segment's drawn length ÷ its rest length)`);
|
|
495
|
+
ok = false;
|
|
496
|
+
}
|
|
497
|
+
if ('sign' in v && v.sign !== -1 && v.sign !== 0 && v.sign !== 1) {
|
|
498
|
+
p.add('POSE_DEPTH', `${sw} "sign" is ${typeName(v.sign)}; it must be 1 (the distal joint nearer the camera), -1 (farther) or 0 (flat, or not decided)`);
|
|
499
|
+
ok = false;
|
|
500
|
+
}
|
|
501
|
+
if ('source' in v && !(typeof v.source === 'string' && (DEPTH_SOURCES as readonly string[]).includes(v.source))) {
|
|
502
|
+
p.add('POSE_DEPTH', `${sw} "source" is ${typeName(v.source)}; it must be "derived" (clean --depth, from the picture's lengths) or "measured" (a 3D detector)`);
|
|
503
|
+
ok = false;
|
|
504
|
+
}
|
|
505
|
+
if (ok) out[name as DepthSegment] = { ratio: v.ratio as number, sign: v.sign as -1 | 0 | 1, source: v.source as DepthSource };
|
|
506
|
+
}
|
|
507
|
+
return out;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/** The minor a sequence's spec names: 0 for "rig-pose-sequence/1", N for "rig-pose-sequence/1.N"; null for anything else. */
|
|
511
|
+
function minorOf(spec: unknown): number | null {
|
|
512
|
+
if (spec === POSE_SPEC) return 0;
|
|
513
|
+
const m = typeof spec === 'string' ? /^rig-pose-sequence\/1\.([1-9]\d{0,5})$/.exec(spec) : null;
|
|
514
|
+
return m === null ? null : Number(m[1]);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/** Frame indices as runs: "3, 5–9". */
|
|
518
|
+
function spansText(fs: readonly number[]): string {
|
|
519
|
+
const out: Array<[number, number]> = [];
|
|
520
|
+
for (const f of fs) {
|
|
521
|
+
const last = out[out.length - 1];
|
|
522
|
+
if (last !== undefined && f === last[1] + 1) last[1] = f;
|
|
523
|
+
else out.push([f, f]);
|
|
524
|
+
}
|
|
525
|
+
return out.map(([a, b]) => (a === b ? `${a}` : `${a}–${b}`)).join(', ');
|
|
316
526
|
}
|
|
317
527
|
|
|
318
528
|
/** `camera`: one `{ s > 0, tx, ty, inliers ≥ 0 }` per frame, as many as `frames`. */
|
|
319
|
-
function readCamera(p: Problems, where: string, camera: unknown, frames: number | null): CameraFrame[] {
|
|
529
|
+
function readCamera(p: Problems, where: string, camera: unknown, frames: number | null, note: string): CameraFrame[] {
|
|
320
530
|
const out: CameraFrame[] = [];
|
|
321
531
|
if (!Array.isArray(camera)) {
|
|
322
532
|
p.add('POSE_CAMERA', `${where} "camera" is ${typeName(camera)}; it must be an array with one { s, tx, ty, inliers } per frame`);
|
|
@@ -329,7 +539,7 @@ function readCamera(p: Problems, where: string, camera: unknown, frames: number
|
|
|
329
539
|
p.add('POSE_CAMERA', `${cw} is ${typeName(c)}; a camera entry is { s, tx, ty, inliers }`);
|
|
330
540
|
return;
|
|
331
541
|
}
|
|
332
|
-
refuseUnknownKeys(p, 'POSE_CAMERA', cw, c, CAMERA_KEYS);
|
|
542
|
+
refuseUnknownKeys(p, 'POSE_CAMERA', cw, c, CAMERA_KEYS, note);
|
|
333
543
|
let ok = true;
|
|
334
544
|
for (const k of CAMERA_KEYS) if (!(k in c)) {
|
|
335
545
|
p.add('POSE_CAMERA', `${cw} has no "${k}"; a camera entry carries ${CAMERA_KEYS.join(', ')}`);
|
|
@@ -354,7 +564,8 @@ function readCamera(p: Problems, where: string, camera: unknown, frames: number
|
|
|
354
564
|
}
|
|
355
565
|
|
|
356
566
|
/** `cleaned`: { spec, params: { loop, legs, rest, hipsInferred?, hold? }, spikes: { found: [{ frame, joint }], untested: [frame] }, jitter: { before, after }, holds?, unheld?, seams? }. */
|
|
357
|
-
function readCleaned(p: Problems, where: string, c: unknown, frames: number | null): CleanRecord | undefined {
|
|
567
|
+
function readCleaned(p: Problems, where: string, c: unknown, frames: number | null, pelvisKnown: boolean, note: string, depthKnown: boolean): CleanRecord | undefined {
|
|
568
|
+
const refuseUnknownKeys = (q: Problems, code: string, w: string, o: Record<string, unknown>, known: readonly string[]) => refuseKeys(q, code, w, o, known, note);
|
|
358
569
|
const cw = `${where} "cleaned"`;
|
|
359
570
|
const shape = '{ spec, params: { loop, legs, rest, hipsInferred?, hold? }, spikes: { found: [{ frame, joint }], untested: [frame] }, jitter: { before, after }, holds?, unheld?, seams? }';
|
|
360
571
|
if (!isObject(c)) {
|
|
@@ -370,7 +581,25 @@ function readCleaned(p: Problems, where: string, c: unknown, frames: number | nu
|
|
|
370
581
|
const q = c.params;
|
|
371
582
|
if (!isObject(q)) p.add('POSE_CLEANED', `${cw} "params" is ${typeName(q)}; it must be { loop, legs, rest, hipsInferred?, hold? }`);
|
|
372
583
|
else {
|
|
373
|
-
refuseUnknownKeys(p, 'POSE_CLEANED', `${cw} "params"`, q, ['loop', 'legs', 'rest', 'hipsInferred', 'hold']);
|
|
584
|
+
refuseUnknownKeys(p, 'POSE_CLEANED', `${cw} "params"`, q, ['loop', 'legs', 'rest', 'hipsInferred', 'hold', ...(pelvisKnown ? ['pelvis'] : []), ...(depthKnown ? ['depth'] : [])]);
|
|
585
|
+
if (depthKnown && 'depth' in q) {
|
|
586
|
+
const v = q.depth;
|
|
587
|
+
const vw = `${cw} "params" "depth"`;
|
|
588
|
+
if (!isObject(v)) p.add('POSE_CLEANED', `${vw} is ${typeName(v)}; it must be { gait } (rig-c/pose#281: the gait clean --depth read the depth for)`);
|
|
589
|
+
else {
|
|
590
|
+
refuseUnknownKeys(p, 'POSE_CLEANED', vw, v, ['gait']);
|
|
591
|
+
if (!(typeof v.gait === 'string' && (DEPTH_GAITS as readonly string[]).includes(v.gait))) p.add('POSE_CLEANED', `${vw} "gait" is ${typeName(v.gait)}; it must be ${DEPTH_GAITS.map((g) => `"${g}"`).join(' or ')}`);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
if (pelvisKnown && 'pelvis' in q) {
|
|
595
|
+
const v = q.pelvis;
|
|
596
|
+
const vw = `${cw} "params" "pelvis"`;
|
|
597
|
+
if (!isObject(v)) p.add('POSE_CLEANED', `${vw} is ${typeName(v)}; it must be { window } (rig-c/pose#288: the window clean smoothed the pelvis track over)`);
|
|
598
|
+
else {
|
|
599
|
+
refuseUnknownKeys(p, 'POSE_CLEANED', vw, v, ['window']);
|
|
600
|
+
if (!window(v.window)) p.add('POSE_CLEANED', `${vw} "window" is ${typeName(v.window)}; it must be 1 (none) or an odd whole number ≥ 5`);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
374
603
|
if ('hold' in q) {
|
|
375
604
|
const h = q.hold;
|
|
376
605
|
const hw = `${cw} "params" "hold"`;
|
|
@@ -384,7 +613,7 @@ function readCleaned(p: Problems, where: string, c: unknown, frames: number | nu
|
|
|
384
613
|
for (const k of ['holds', 'unheld', 'seams']) if (!(k in c)) p.add('POSE_CLEANED', `${cw} has "params" "hold" and no "${k}"; a record whose hold ran lists its holds, the joints it did not hold and its seams (an empty array when none)`);
|
|
385
614
|
}
|
|
386
615
|
} else for (const k of ['holds', 'unheld', 'seams']) if (k in c) p.add('POSE_CLEANED', `${cw} has "${k}" and no "params" "hold"; the holds' figures are recorded in "params" "hold"`);
|
|
387
|
-
if ('hipsInferred' in q) readHipsInferred(p, `${cw} "params" "hipsInferred"`, q.hipsInferred, frames);
|
|
616
|
+
if ('hipsInferred' in q) readHipsInferred(p, `${cw} "params" "hipsInferred"`, q.hipsInferred, frames, note);
|
|
388
617
|
if (typeof q.loop !== 'boolean') p.add('POSE_CLEANED', `${cw} "params" "loop" is ${typeName(q.loop)}; it must be true or false`);
|
|
389
618
|
for (const k of ['legs', 'rest'] as const) if (!window(q[k])) p.add('POSE_CLEANED', `${cw} "params" "${k}" is ${typeName(q[k])}; it must be 1 (none) or an odd whole number ≥ 5`);
|
|
390
619
|
}
|
|
@@ -490,11 +719,11 @@ function readCleaned(p: Problems, where: string, c: unknown, frames: number | nu
|
|
|
490
719
|
});
|
|
491
720
|
}
|
|
492
721
|
if (p.list.length > before) return undefined;
|
|
493
|
-
const q = c.params as { loop: boolean; legs: number; rest: number; hipsInferred?: HipsInferred; hold?: HoldParams | 'off' };
|
|
722
|
+
const q = c.params as { loop: boolean; legs: number; rest: number; hipsInferred?: HipsInferred; hold?: HoldParams | 'off'; pelvis?: PelvisParams; depth?: DepthParams };
|
|
494
723
|
const j = c.jitter as { before: number | null; after: number | null };
|
|
495
724
|
return {
|
|
496
725
|
spec: CLEAN_SPEC,
|
|
497
|
-
params: { loop: q.loop, legs: q.legs, rest: q.rest, ...(q.hipsInferred !== undefined ? { hipsInferred: copyHipsInferred(q.hipsInferred) } : {}), ...(q.hold !== undefined ? { hold: holdParamsOf(q.hold) } : {}) },
|
|
726
|
+
params: { loop: q.loop, legs: q.legs, rest: q.rest, ...(q.hipsInferred !== undefined ? { hipsInferred: copyHipsInferred(q.hipsInferred) } : {}), ...(q.hold !== undefined ? { hold: holdParamsOf(q.hold) } : {}), ...(pelvisKnown && q.pelvis !== undefined ? { pelvis: { window: q.pelvis.window } } : {}), ...(depthKnown && q.depth !== undefined ? { depth: { gait: q.depth.gait } } : {}) },
|
|
498
727
|
spikes: {
|
|
499
728
|
found: ((c.spikes as { found: Array<{ frame: number; joint: Body18 }> }).found).map((s) => ({ frame: s.frame, joint: s.joint })),
|
|
500
729
|
untested: [...(c.spikes as { untested: number[] }).untested],
|
|
@@ -507,7 +736,8 @@ function readCleaned(p: Problems, where: string, c: unknown, frames: number | nu
|
|
|
507
736
|
}
|
|
508
737
|
|
|
509
738
|
/** `hipsInferred`: { by: "declared" } or the measured record (the shape at `HipsInferred`), refused by name otherwise. */
|
|
510
|
-
function readHipsInferred(p: Problems, hw: string, h: unknown, frames: number | null): void {
|
|
739
|
+
function readHipsInferred(p: Problems, hw: string, h: unknown, frames: number | null, note: string): void {
|
|
740
|
+
const refuseUnknownKeys = (q: Problems, code: string, w: string, o: Record<string, unknown>, known: readonly string[]) => refuseKeys(q, code, w, o, known, note);
|
|
511
741
|
const measuredShape = '{ by: "measured", method, read, interpolated, held: [[first, last]], declined: [{ frames: [first, last], why }] }';
|
|
512
742
|
if (!isObject(h)) {
|
|
513
743
|
p.add('POSE_CLEANED', `${hw} is ${typeName(h)}; it must be { by: "declared" } or ${measuredShape}`);
|
|
@@ -652,7 +882,7 @@ const holdParamsOf = (h: HoldParams | 'off'): HoldParams | 'off' => (h === 'off'
|
|
|
652
882
|
const copyHold = (h: CleanHold): CleanHold => ({ joint: h.joint, frames: [h.frames[0], h.frames[1]], ...(h.excursion !== undefined ? { excursion: [h.excursion[0], h.excursion[1]] as [number, number] } : {}), jitter: { before: h.jitter.before, after: h.jitter.after } });
|
|
653
883
|
const copyUnheld = (u: CleanUnheld): CleanUnheld => ({ joint: u.joint, frames: [u.frames[0], u.frames[1]], test: u.test, value: u.value });
|
|
654
884
|
|
|
655
|
-
function readJoints(p: Problems, pw: string, joints: unknown): Partial<Record<Body18, Joint>> {
|
|
885
|
+
function readJoints(p: Problems, pw: string, joints: unknown, note: string): Partial<Record<Body18, Joint>> {
|
|
656
886
|
const out: Partial<Record<Body18, Joint>> = {};
|
|
657
887
|
if (!isObject(joints)) {
|
|
658
888
|
p.add('POSE_JOINTS', `${pw} "joints" is ${typeName(joints)}; it must be an object keyed by body-18 joint names`);
|
|
@@ -668,7 +898,7 @@ function readJoints(p: Problems, pw: string, joints: unknown): Partial<Record<Bo
|
|
|
668
898
|
p.add('POSE_JOINT_SHAPE', `${jw} is ${typeName(j)}; a joint is { state, at?, score? }`);
|
|
669
899
|
continue;
|
|
670
900
|
}
|
|
671
|
-
refuseUnknownKeys(p, 'POSE_KEYS', jw, j, ['state', 'at', 'score']);
|
|
901
|
+
refuseUnknownKeys(p, 'POSE_KEYS', jw, j, ['state', 'at', 'score'], note);
|
|
672
902
|
const state = j.state;
|
|
673
903
|
if (typeof state !== 'string' || !(STATES as readonly string[]).includes(state)) {
|
|
674
904
|
p.add('POSE_JOINT_STATE', `${jw} "state" is ${typeName(state)}; it must be one of ${STATES.join(', ')}`);
|
|
@@ -821,6 +1051,9 @@ export function writePoseSequence(seq: PoseSequence): string {
|
|
|
821
1051
|
const round = (v: number, d: number) => Number(v.toFixed(d));
|
|
822
1052
|
const frames = seq.frames.map((f) => ({
|
|
823
1053
|
t: f.t,
|
|
1054
|
+
...(f.pelvisTilt !== undefined ? { pelvisTilt: f.pelvisTilt === null ? null : round(f.pelvisTilt, 3) } : {}),
|
|
1055
|
+
// rig-c/pose#281: the segments in DEPTH_SEGMENTS' order, each { ratio, sign, source }.
|
|
1056
|
+
...(f.depth !== undefined ? { depth: Object.fromEntries(DEPTH_SEGMENT_NAMES.filter((k) => f.depth![k] !== undefined).map((k) => [k, { ratio: round(f.depth![k]!.ratio, 3), sign: f.depth![k]!.sign, source: f.depth![k]!.source }])) } : {}),
|
|
824
1057
|
people: f.people.map((q) => ({
|
|
825
1058
|
id: q.id,
|
|
826
1059
|
joints: Object.fromEntries(
|
|
@@ -840,12 +1073,14 @@ export function writePoseSequence(seq: PoseSequence): string {
|
|
|
840
1073
|
const jit = (v: number | null) => (v === null ? null : round(v, 4));
|
|
841
1074
|
const cleaned = c === undefined ? undefined : {
|
|
842
1075
|
spec: c.spec,
|
|
843
|
-
params: { loop: c.params.loop, legs: c.params.legs, rest: c.params.rest, ...(c.params.hipsInferred !== undefined ? { hipsInferred: copyHipsInferred(c.params.hipsInferred) } : {}), ...(c.params.hold !== undefined ? { hold: holdParamsOf(c.params.hold) } : {}) },
|
|
1076
|
+
params: { loop: c.params.loop, legs: c.params.legs, rest: c.params.rest, ...(c.params.hipsInferred !== undefined ? { hipsInferred: copyHipsInferred(c.params.hipsInferred) } : {}), ...(c.params.hold !== undefined ? { hold: holdParamsOf(c.params.hold) } : {}), ...(c.params.pelvis !== undefined ? { pelvis: { window: c.params.pelvis.window } } : {}), ...(c.params.depth !== undefined ? { depth: { gait: c.params.depth.gait } } : {}) },
|
|
844
1077
|
spikes: { found: c.spikes.found.map((s) => ({ frame: s.frame, joint: s.joint })), untested: [...c.spikes.untested] },
|
|
845
1078
|
jitter: { before: jit(c.jitter.before), after: jit(c.jitter.after) },
|
|
846
1079
|
...(c.holds !== undefined ? { holds: c.holds.map((h) => ({ joint: h.joint, frames: [h.frames[0], h.frames[1]], ...(h.excursion !== undefined ? { excursion: [h.excursion[0], h.excursion[1]] } : {}), jitter: { before: jit(h.jitter.before), after: jit(h.jitter.after) } })) } : {}),
|
|
847
1080
|
...(c.seams !== undefined ? { seams: c.seams.map((x) => ({ joint: x.joint, frame: x.frame, step: round(x.step, 4), gap: x.gap })) } : {}),
|
|
848
1081
|
...(c.unheld !== undefined ? { unheld: c.unheld.map((u) => ({ joint: u.joint, frames: [u.frames[0], u.frames[1]], test: u.test, value: u.value === null ? null : round(u.value, 4) })) } : {}),
|
|
849
1082
|
};
|
|
850
|
-
|
|
1083
|
+
// rig-c/pose#288, #281: the minor the content needs — /1.2 with depth (it includes 1.1's keys), /1.1 with a pelvis track, /1 otherwise.
|
|
1084
|
+
const spec = seq.frames.some((f) => f.depth !== undefined) ? POSE_SPEC_DEPTH : seq.pelvisSource !== undefined ? POSE_SPEC_PELVIS : seq.spec;
|
|
1085
|
+
return `${JSON.stringify({ spec, space: seq.space, width: seq.width, height: seq.height, source: seq.source, ...(seq.cameraSource !== undefined ? { cameraSource: seq.cameraSource } : {}), ...(camera !== undefined ? { camera } : {}), ...(cleaned !== undefined ? { cleaned } : {}), ...(seq.travel !== undefined ? { travel: seq.travel } : {}), ...(seq.pelvisSource !== undefined ? { pelvisSource: seq.pelvisSource } : {}), frames }, null, 2)}\n`;
|
|
851
1086
|
}
|
package/src/problems.ts
CHANGED
|
@@ -59,10 +59,13 @@ export function isObject(v: unknown): v is Record<string, unknown> {
|
|
|
59
59
|
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
/**
|
|
63
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Refuse the keys of `obj` a reader does not know, by name, listing the ones it does. `note`, when
|
|
64
|
+
* given, ends each line: a sequence written by a newer minor of its format says which (rig-c/pose#288).
|
|
65
|
+
*/
|
|
66
|
+
export function refuseUnknownKeys(problems: Problems, code: string, where: string, obj: Record<string, unknown>, known: readonly string[], note = ''): void {
|
|
64
67
|
for (const k of Object.keys(obj)) {
|
|
65
|
-
if (!known.includes(k)) problems.add(code, `${where} has a key this reader does not know: ${JSON.stringify(k)}; the keys it reads are ${known.join(', ')}`);
|
|
68
|
+
if (!known.includes(k)) problems.add(code, `${where} has a key this reader does not know: ${JSON.stringify(k)}; the keys it reads are ${known.join(', ')}${note}`);
|
|
66
69
|
}
|
|
67
70
|
}
|
|
68
71
|
|