spine-rigc 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 +80 -1
- package/cli.ts +263 -13
- package/docs/AUTHORING.md +554 -75
- package/docs/INGEST.md +238 -44
- package/docs/SPEC_COVERAGE.md +14 -3
- package/package.json +1 -1
- package/skills/ingest/SKILL.md +33 -11
- package/src/atlas.ts +135 -16
- package/src/check.ts +83 -1
- package/src/compile.ts +402 -74
- package/src/deformmeasure.ts +322 -151
- package/src/diff.ts +125 -2
- package/src/ingest.ts +1137 -0
- package/src/render.ts +113 -17
- package/src/rig.ts +92 -8
- package/src/timelines.ts +163 -0
- package/src/types.ts +74 -9
- package/src/validate.ts +765 -92
- package/tools/editor_roundtrip.ts +247 -36
package/src/diff.ts
CHANGED
|
@@ -160,6 +160,25 @@ export interface DiffSection {
|
|
|
160
160
|
|
|
161
161
|
export interface DiffReport {
|
|
162
162
|
sections: DiffSection[];
|
|
163
|
+
/**
|
|
164
|
+
* The `skeleton` block itself — measured, and reported beside the sections
|
|
165
|
+
* rather than inside one (issue #578).
|
|
166
|
+
*
|
|
167
|
+
* ⭐ It is a `DiffReported` and not a seventh `DiffSection`, and the reason is
|
|
168
|
+
* the one thing a section must have: a `ratio`. A section's mean is the figure
|
|
169
|
+
* a stored `bench.json` row quotes, and these measures cannot be in one —
|
|
170
|
+
* `docs/GATE.md`'s *What never gates* covers them twice over, so a `skeleton`
|
|
171
|
+
* section would carry either a vacuous `mean 1.000 over 0 measures` (the false
|
|
172
|
+
* green this file exists to refuse) or a nullable `ratio` every reader of every
|
|
173
|
+
* other section would then have to handle. `DiffReported` already models
|
|
174
|
+
* exactly "measures with no mean over them", which is what the header is.
|
|
175
|
+
*
|
|
176
|
+
* ⚠️ Not optional, deliberately: a block that may be absent is a block that can
|
|
177
|
+
* silently stop being emitted, and `movedReportedMeasures` over an absent one
|
|
178
|
+
* is the empty list — indistinguishable from one that is all 1.000. The
|
|
179
|
+
* selftest asserts its COUNT for the same reason it does for the others.
|
|
180
|
+
*/
|
|
181
|
+
header: DiffReported;
|
|
163
182
|
/** Raw counts either side, for orientation. Never combined into anything. */
|
|
164
183
|
candidate: Record<string, number>;
|
|
165
184
|
reference: Record<string, number>;
|
|
@@ -978,6 +997,94 @@ function diffEvents(c: Json, r: Json): DiffSection {
|
|
|
978
997
|
]);
|
|
979
998
|
}
|
|
980
999
|
|
|
1000
|
+
// ---------------------------------------------------------------------------
|
|
1001
|
+
// the skeleton header
|
|
1002
|
+
// ---------------------------------------------------------------------------
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* The stage: `x`, `y`, `width`, `height` of the setup-pose bounding box, or the
|
|
1006
|
+
* absence of all four.
|
|
1007
|
+
*
|
|
1008
|
+
* 🔍 **Why this measure exists at all** (issue #578). The stage was the one value
|
|
1009
|
+
* `build` required and no instrument in this tree could see: `validate`, `check`
|
|
1010
|
+
* and `render` all ignore it — `render` frames from the posed bounds — and `diff`
|
|
1011
|
+
* had no header measure, so a sweep that handed a deliberately absurd unit stage
|
|
1012
|
+
* `0,0,1,1` to 37 real exports read **1.000 on every measure** for 32 of them. A
|
|
1013
|
+
* required-and-unmeasured field is the worst combination a field can have: the
|
|
1014
|
+
* only way to satisfy it was to invent a number, and nothing would ever say so.
|
|
1015
|
+
*
|
|
1016
|
+
* ⭐ **`stage_present` is 1/1 or 0/1 and never `0/0`.** Both sides always have a
|
|
1017
|
+
* presence to compare, including when both say "none" — that is agreement, not
|
|
1018
|
+
* an absence of data, and `total: 0` here would be the vacuous 1.000 this file
|
|
1019
|
+
* refuses. `stage_box` is the one that goes vacuous, and only when there are not
|
|
1020
|
+
* two boxes to compare.
|
|
1021
|
+
*/
|
|
1022
|
+
interface StageFacts {
|
|
1023
|
+
present: boolean;
|
|
1024
|
+
/** The four fields as stated, `null` where the header omits one. */
|
|
1025
|
+
box: Array<number | null>;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
const STAGE_FIELDS = ['x', 'y', 'width', 'height'] as const;
|
|
1029
|
+
|
|
1030
|
+
/**
|
|
1031
|
+
* A stage is declared by its EXTENT: a numeric `width` and `height`.
|
|
1032
|
+
*
|
|
1033
|
+
* `x`/`y` alone are an origin for a box that is not there — no export carries
|
|
1034
|
+
* that shape, and rigc refuses to emit it — so they do not make a stage on their
|
|
1035
|
+
* own. It is also what the compiler requires and what `A14_NO_FULL_FRAME_MESH`
|
|
1036
|
+
* and `A19_OVERLAY_PNGS_HAVE_ALPHA` measure against, so the three agree on the
|
|
1037
|
+
* word by construction rather than by memory.
|
|
1038
|
+
*/
|
|
1039
|
+
function stageFacts(root: Json): StageFacts {
|
|
1040
|
+
const header = isObj(root.skeleton) ? root.skeleton : {};
|
|
1041
|
+
const box = STAGE_FIELDS.map((k) => num(header[k]));
|
|
1042
|
+
return { present: num(header.width) !== null && num(header.height) !== null, box };
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/**
|
|
1046
|
+
* The two header measures.
|
|
1047
|
+
*
|
|
1048
|
+
* ⚠️ **The box is compared EXACTLY, and that is a measurement rather than a
|
|
1049
|
+
* choice.** The brief this was built from asked for "the tolerance the other
|
|
1050
|
+
* measures use"; `src/diff.ts` has exactly one tolerance in it — `FRAME`, one
|
|
1051
|
+
* sixtieth of a second, used once, for `animations.duration` — and no spatial
|
|
1052
|
+
* one anywhere, because this file compares no position at all (that is
|
|
1053
|
+
* `bonedist.ts`). A stage is a box an exporter *wrote down*, not a pose anybody
|
|
1054
|
+
* measured, so there is nothing for it to be within a tolerance *of*; inventing
|
|
1055
|
+
* a spatial epsilon here would be a number nobody measured, in the file whose
|
|
1056
|
+
* whole job is to report measured ones.
|
|
1057
|
+
*/
|
|
1058
|
+
function diffHeader(c: Json, r: Json): DiffReported {
|
|
1059
|
+
const a = stageFacts(c);
|
|
1060
|
+
const b = stageFacts(r);
|
|
1061
|
+
const both = a.present && b.present;
|
|
1062
|
+
const agreed = both ? a.box.filter((v, i) => v === b.box[i]).length : 0;
|
|
1063
|
+
const side = (f: StageFacts): string => (f.present ? `${f.box[2]}x${f.box[3]} at ${f.box[0]},${f.box[1]}` : 'none');
|
|
1064
|
+
return {
|
|
1065
|
+
measures: [
|
|
1066
|
+
measure(
|
|
1067
|
+
'skeleton.stage_present',
|
|
1068
|
+
'both sides declare a setup-pose stage, or neither does',
|
|
1069
|
+
a.present === b.present ? 1 : 0,
|
|
1070
|
+
1,
|
|
1071
|
+
`candidate ${side(a)}; reference ${side(b)}. A skeleton declares a stage by stating a width and a height`,
|
|
1072
|
+
),
|
|
1073
|
+
measure(
|
|
1074
|
+
'skeleton.stage_box',
|
|
1075
|
+
'the stage is the same box (x, y, width, height, exactly as stated)',
|
|
1076
|
+
agreed,
|
|
1077
|
+
both ? STAGE_FIELDS.length : 0,
|
|
1078
|
+
both
|
|
1079
|
+
? `candidate ${side(a)}; reference ${side(b)}`
|
|
1080
|
+
: a.present === b.present
|
|
1081
|
+
? 'neither side declares a stage, so there is no box to compare — `stage_present` carries that'
|
|
1082
|
+
: 'only one side declares a stage, so there is no second box to compare — `stage_present` carries that',
|
|
1083
|
+
),
|
|
1084
|
+
],
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
|
|
981
1088
|
// ---------------------------------------------------------------------------
|
|
982
1089
|
// the report
|
|
983
1090
|
// ---------------------------------------------------------------------------
|
|
@@ -1000,6 +1107,7 @@ export function diffSkeletons(candidate: unknown, reference: unknown): DiffRepor
|
|
|
1000
1107
|
const r = isObj(reference) ? reference : {};
|
|
1001
1108
|
return {
|
|
1002
1109
|
sections: [diffBones(c, r), diffSlots(c, r), diffAttachments(c, r), diffConstraints(c, r), diffAnimations(c, r), diffEvents(c, r)],
|
|
1110
|
+
header: diffHeader(c, r),
|
|
1003
1111
|
candidate: orientation(c),
|
|
1004
1112
|
reference: orientation(r),
|
|
1005
1113
|
};
|
|
@@ -1035,7 +1143,9 @@ export function movedAgnosticMeasures(report: DiffReport): string[] {
|
|
|
1035
1143
|
* would notice.
|
|
1036
1144
|
*/
|
|
1037
1145
|
export function movedReportedMeasures(report: DiffReport): string[] {
|
|
1038
|
-
return report.sections.flatMap((s) =>
|
|
1146
|
+
return [...report.sections.flatMap((s) => s.reported?.measures ?? []), ...report.header.measures]
|
|
1147
|
+
.filter((m) => m.ratio < 1)
|
|
1148
|
+
.map((m) => m.id);
|
|
1039
1149
|
}
|
|
1040
1150
|
|
|
1041
1151
|
const fmt = (n: number): string => n.toFixed(3);
|
|
@@ -1049,7 +1159,7 @@ const fmt = (n: number): string => n.toFixed(3);
|
|
|
1049
1159
|
* the report and the line is a summary.
|
|
1050
1160
|
*/
|
|
1051
1161
|
export function reportedFigures(report: DiffReport): string | null {
|
|
1052
|
-
const measures = report.sections.flatMap((s) => s.reported?.measures ?? []);
|
|
1162
|
+
const measures = [...report.sections.flatMap((s) => s.reported?.measures ?? []), ...report.header.measures];
|
|
1053
1163
|
if (measures.length === 0) return null;
|
|
1054
1164
|
return measures.map((m) => `${m.id.slice(m.id.indexOf('.') + 1)} ${fmt(m.ratio)}`).join(' · ');
|
|
1055
1165
|
}
|
|
@@ -1076,6 +1186,14 @@ export function diffLines(report: DiffReport, labels: { candidate: string; refer
|
|
|
1076
1186
|
const keys = Object.keys(report.reference);
|
|
1077
1187
|
lines.push(` .. ${keys.map((k) => `${k}=${report.candidate[k]}/${report.reference[k]}`).join(' ')} (candidate/reference)`);
|
|
1078
1188
|
lines.push('');
|
|
1189
|
+
// The `skeleton` block, first because that is where it sits in the file, and
|
|
1190
|
+
// with `(no mean)` for the same reason a section's `(reported)` block has one.
|
|
1191
|
+
lines.push(
|
|
1192
|
+
` ${'skeleton (reported)'.padEnd(21)} (no mean) over ${report.header.measures.length} measures` +
|
|
1193
|
+
' — the stage, which no reading of the frames could decide',
|
|
1194
|
+
);
|
|
1195
|
+
lines.push(...measureLines(report.header.measures, 'skeleton.'.length));
|
|
1196
|
+
lines.push('');
|
|
1079
1197
|
// Wide enough for `<longest section> (name-agnostic)`, so that a section's two
|
|
1080
1198
|
// headings line their figures up under each other and read as a pair.
|
|
1081
1199
|
const head = (label: string, ratio: number, n: number): string =>
|
|
@@ -1116,6 +1234,11 @@ export function diffLines(report: DiffReport, labels: { candidate: string; refer
|
|
|
1116
1234
|
lines.push(' comparisons, not two halves of one: name-agnostic 1.000 beside a low');
|
|
1117
1235
|
lines.push(' name-matched figure means the shape is right and the vocabulary differs.');
|
|
1118
1236
|
lines.push('');
|
|
1237
|
+
lines.push(' `skeleton` is the file\'s own header block and reports two measures for the stage.');
|
|
1238
|
+
lines.push(' It has no mean for the reason a `(reported)` block never does, and it never');
|
|
1239
|
+
lines.push(' gates for two: no reading of the frames recovers a setup-pose bounding box, and');
|
|
1240
|
+
lines.push(' the ladder\'s briefs withhold the stage size outright.');
|
|
1241
|
+
lines.push('');
|
|
1119
1242
|
lines.push(' A `(reported)` block has no mean because its measures have unlike units, and');
|
|
1120
1243
|
lines.push(' it stays out of the section mean above it for the same reason no clause may');
|
|
1121
1244
|
lines.push(' read it: no reading of the reference frames could have decided these. They are');
|