spine-rigc 1.5.2 → 1.6.1

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/check.ts CHANGED
@@ -81,6 +81,13 @@ import {
81
81
  FRAMES_SIDECAR,
82
82
  FRAMES_SPEC,
83
83
  nonFinitePoseOf,
84
+ unframeableSentence,
85
+ UnframeablePoseError,
86
+ skinRosterOf,
87
+ candidatePosers,
88
+ spinePoser,
89
+ throughPoser,
90
+ PoserChoiceError,
84
91
  SHEET_COLUMNS,
85
92
  SHEET_FILE,
86
93
  SHEET_GAP,
@@ -89,6 +96,9 @@ import {
89
96
  type FramesSidecar,
90
97
  type FrameSet,
91
98
  type PoseOptions,
99
+ type Poser,
100
+ type PoserChoice,
101
+ type PoserName,
92
102
  type TextureSubstitution,
93
103
  type Viewport,
94
104
  } from './render.ts';
@@ -883,6 +893,19 @@ export interface TextureFromReport {
883
893
 
884
894
  export interface CheckReport {
885
895
  candidate: { skeleton: string; atlas: string };
896
+ /**
897
+ * Which implementation of the posing seam posed the CANDIDATE, and why
898
+ * (issue #968): `core` — rigc's own, reading the `skeleton.model.json` a
899
+ * build writes beside the pair — or `spine` (spine-core), with the reason the
900
+ * core was not used. `note` is `render`'s `poser` line, word for word.
901
+ *
902
+ * In the report because a figure is only readable beside what produced it:
903
+ * the reference side is pixels either way, and a report that did not say
904
+ * which poser drew the candidate would be a number with two possible
905
+ * subjects. The two are measured to give the same report on every tree row;
906
+ * this field is what lets a reader see which one this is.
907
+ */
908
+ poser: { name: PoserName; note: string };
886
909
  framesDir: string;
887
910
  framesRoot: string;
888
911
  /**
@@ -1024,6 +1047,36 @@ export interface CheckOptions {
1024
1047
  * in beside the comparison, from the plates the comparison was computed on.
1025
1048
  */
1026
1049
  plates?: CheckPlates;
1050
+ /**
1051
+ * The files `skeletonText` and `atlasText` were read from — what chooses the
1052
+ * candidate's poser, exactly as `render` chooses its own (`candidatePosers`,
1053
+ * issue #968): rigc's core when the `skeleton.model.json` beside the skeleton
1054
+ * records these skeleton bytes, spine-core otherwise. They must be the files
1055
+ * the texts came from; `cli.ts` reads both from them. Absent, the candidate
1056
+ * is posed through spine-core and the report says why.
1057
+ */
1058
+ candidatePaths?: { skeleton: string; atlas: string };
1059
+ /** `--poser`: force one implementation — see `candidatePosers`. `core` on an input that cannot carry it throws `PoserChoiceError`. */
1060
+ poser?: PoserName;
1061
+ /**
1062
+ * What builds the core poser — `candidatePosers`' own `make`. The suite's
1063
+ * `CH01` passes a planted copy, and nothing else passes any.
1064
+ */
1065
+ makeCorePoser?: Parameters<typeof candidatePosers>[4];
1066
+ }
1067
+
1068
+ /**
1069
+ * Both posers for the candidate `check` is handed, and which one it asked for:
1070
+ * `candidatePosers` over the candidate's files when the caller named them, and
1071
+ * spine-core alone when it handed texts only — there is then no directory to
1072
+ * find a model document in, and the reason says so.
1073
+ */
1074
+ function checkPoserChoice(data: Parameters<typeof spinePoser>[0], options: CheckOptions): PoserChoice {
1075
+ const paths = options.candidatePaths;
1076
+ if (paths !== undefined) return candidatePosers(data, paths.skeleton, paths.atlas, options.poser, options.makeCorePoser);
1077
+ const why = 'the candidate was handed to check as text, with no path to find a skeleton.model.json beside';
1078
+ if (options.poser === 'core') throw new PoserChoiceError(`--poser core: ${why}`);
1079
+ return { forced: options.poser, core: null, why: options.poser === 'spine' ? '--poser spine' : why, spine: spinePoser(data) };
1027
1080
  }
1028
1081
 
1029
1082
  // ---------------------------------------------------------------------------
@@ -1121,6 +1174,11 @@ export function checkAgainstFrames(options: CheckOptions): CheckReport {
1121
1174
  }]`,
1122
1175
  );
1123
1176
  }
1177
+ // Which implementation poses the candidate (issue #968), chosen once for the
1178
+ // whole run — every set, the setup pose and the non-finite sentence — so no
1179
+ // two figures in one report can come from two posers. `--poser core` on an
1180
+ // input that cannot carry it is refused here, before anything is posed.
1181
+ const choice = checkPoserChoice(posable.data, options);
1124
1182
  const poseOptions: PoseOptions | undefined =
1125
1183
  substitution || options.skin !== undefined
1126
1184
  ? { ...(substitution ? { texture: true } : {}), ...(options.skin === undefined ? {} : { skin: options.skin }) }
@@ -1225,28 +1283,54 @@ export function checkAgainstFrames(options: CheckOptions): CheckReport {
1225
1283
  // Pose every set once. Its frames are wanted twice — to frame the candidate and
1226
1284
  // to compare it — and posing twice is both slower and a chance for the framing
1227
1285
  // and the comparison to disagree about what they measured.
1228
- const prepared = sets.map((set) => prepareSet(located.root, set, posable, options.as, poseOptions));
1229
- // 🔒 A pose that is not finite is refused before anything measures it (issue
1230
- // #873), in the words `render` and the geometry export use for it. Measured
1231
- // on a planted bone at x=1e309 before this: an overflowing root read "posed no
1232
- // drawable attachment", one at -1e309 "drew no pixel", and one at +1e309 or a
1233
- // rotation at 1e309 exited 0 with MAE 1.00 — the part missing from every
1234
- // frame, and nothing said why.
1235
- const posed = prepared.filter((p) => p.missing === null);
1236
- const overflow = posed.some((p) =>
1237
- p.frames.some((frame) => frame.pieces.some((piece) => piece.world.some((value) => !Number.isFinite(value)))),
1238
- );
1239
- if (overflow) {
1240
- const found = nonFinitePoseOf(
1241
- posable.data,
1242
- options.skin,
1243
- posed.map((p) => ({ animation: p.candidateAnimation, fps: p.set.fps })),
1286
+ //
1287
+ // Posed through the chosen poser, and ALL of it inside one `throughPoser`
1288
+ // call: a core refusal partway (`CoreInputError` — a concave clip, a
1289
+ // non-finite vertex) re-poses every set on spine-core rather than leaving a
1290
+ // report whose sets were posed by two implementations. The animation roster
1291
+ // a set is checked against is the skeleton's, in its own order, whichever
1292
+ // poser runs — the model document lists the spec's order, and a refusal that
1293
+ // listed the names in another order would be a byte of the report that
1294
+ // depends on the poser.
1295
+ const have = posable.data.animations.map((a) => a.name);
1296
+ const posing = throughPoser(choice, (poser) => {
1297
+ const sampled = sets.map((set) => prepareSet(located.root, set, poser, have, options.as, poseOptions));
1298
+ // 🔒 A pose that is not finite is refused before anything measures it (issue
1299
+ // #873), in the words `render` and the geometry export use for it. Measured
1300
+ // on a planted bone at x=1e309 before this: an overflowing root read "posed no
1301
+ // drawable attachment", one at -1e309 "drew no pixel", and one at +1e309 or a
1302
+ // rotation at 1e309 exited 0 with MAE 1.00 — the part missing from every
1303
+ // frame, and nothing said why.
1304
+ const posedSets = sampled.filter((p) => p.missing === null);
1305
+ const overflow = posedSets.some((p) =>
1306
+ p.frames.some((frame) => frame.pieces.some((piece) => piece.world.some((value) => !Number.isFinite(value)))),
1244
1307
  );
1245
- if (found === null) {
1246
- throw new Error('a drawn piece of the candidate is not finite and no bone or vertex of its pose is — the two were posed differently');
1308
+ if (overflow) {
1309
+ const found = nonFinitePoseOf(
1310
+ poser,
1311
+ options.skin,
1312
+ posedSets.map((p) => ({ animation: p.candidateAnimation, fps: p.set.fps })),
1313
+ );
1314
+ if (found === null) {
1315
+ throw new Error('a drawn piece of the candidate is not finite and no bone or vertex of its pose is — the two were posed differently');
1316
+ }
1317
+ throw new CheckError(`the candidate is posed to a number that is not finite, so no frame of it can be compared: ${found}`);
1247
1318
  }
1248
- throw new CheckError(`the candidate is posed to a number that is not finite, so no frame of it can be compared: ${found}`);
1249
- }
1319
+ // 🔒 A candidate whose every drawn vertex sits at one point is refused in
1320
+ // `render`'s own sentence (issue #997), not measured. Measured before this on
1321
+ // a rig whose one drawn slot hangs from a bone only a non-default skin poses:
1322
+ // "the candidate drew no pixel in any frame that was compared" — true, and
1323
+ // silent on the reason, which is a skin the run did not pose.
1324
+ const unframeable = unframeableSentence(
1325
+ posedSets.map((p) => p.frames),
1326
+ poser.slots,
1327
+ options.skin,
1328
+ skinRosterOf(posable.data),
1329
+ );
1330
+ if (unframeable !== null) throw new UnframeablePoseError(unframeable);
1331
+ return sampled;
1332
+ });
1333
+ const prepared = posing.value;
1250
1334
  const pairs = prepared.flatMap((p) => p.pairs);
1251
1335
  if (pairs.length === 0) {
1252
1336
  notes.push('no reference frame has a candidate frame at the same index — nothing below was measured');
@@ -1548,6 +1632,7 @@ export function checkAgainstFrames(options: CheckOptions): CheckReport {
1548
1632
  skeleton: options.labels?.skeleton ?? '(in memory)',
1549
1633
  atlas: options.labels?.atlas ?? '(in memory)',
1550
1634
  },
1635
+ poser: { name: posing.poser, note: posing.note },
1551
1636
  framesDir: resolve(options.framesDir),
1552
1637
  framesRoot: located.root,
1553
1638
  skin: options.skin ?? null,
@@ -2430,13 +2515,14 @@ interface PreparedSet {
2430
2515
  function prepareSet(
2431
2516
  root: string,
2432
2517
  set: FrameSet,
2433
- posable: ReturnType<typeof posableFromText>,
2518
+ poser: Poser,
2519
+ /** The skeleton's animation names in its own order — see the call. */
2520
+ have: readonly string[],
2434
2521
  as: string | undefined,
2435
2522
  poseOptions: PoseOptions | undefined,
2436
2523
  ): PreparedSet {
2437
2524
  const notes: string[] = [];
2438
2525
  const wanted = as ?? set.animation;
2439
- const have = posable.data.animations.map((a) => a.name);
2440
2526
  const disk = framesOnDisk(root, set.dir);
2441
2527
 
2442
2528
  if (wanted !== null && !have.includes(wanted)) {
@@ -2464,10 +2550,10 @@ function prepareSet(
2464
2550
  `[${have.join(', ')}] — the setup pose is what was compared`,
2465
2551
  );
2466
2552
  }
2467
- candidateFrames = sampleSetupPose(posable.data, poseOptions);
2553
+ candidateFrames = sampleSetupPose(poser, poseOptions);
2468
2554
  candidateAnimation = null;
2469
2555
  } else {
2470
- candidateFrames = sampleAnimation(posable.data, wanted, set.fps, poseOptions);
2556
+ candidateFrames = sampleAnimation(poser, wanted, set.fps, poseOptions);
2471
2557
  candidateAnimation = wanted;
2472
2558
  }
2473
2559
 
@@ -3416,6 +3502,9 @@ export function checkLines(report: CheckReport, opts?: { allFrames?: boolean }):
3416
3502
  report.referenceSkin === null ? `no skin recorded in ${FRAMES_SIDECAR}` : report.referenceSkin
3417
3503
  }`,
3418
3504
  );
3505
+ // Always printed, like the skin line above it: which implementation posed the
3506
+ // candidate, and why — `render`'s `poser` line, word for word (issue #968).
3507
+ lines.push(` poser ${report.poser.note}`);
3419
3508
  lines.push(
3420
3509
  ` scope ${
3421
3510
  report.framingScope === 'per-shot'
package/src/checkpics.ts CHANGED
@@ -259,6 +259,8 @@ function picturesSidecar(report: CheckReport, written: PicturesWritten[], allFra
259
259
  spec: FRAMES_SPEC,
260
260
  [COMPARISON_FIELD]: {
261
261
  candidate: report.candidate,
262
+ // Which poser drew the candidate pane (issue #968) — the report's own field.
263
+ poser: report.poser,
262
264
  frames: report.framesDir,
263
265
  skin: report.skin,
264
266
  referenceSkin: report.referenceSkin,