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/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) => (s.reported?.measures ?? []).filter((m) => m.ratio < 1).map((m) => m.id));
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');