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/README.md +2 -1
- package/cli.ts +131 -403
- package/docs/AUTHORING.md +72 -23
- package/docs/INGEST.md +1 -0
- package/docs/MOTION.md +26 -17
- package/package.json +1 -1
- package/src/atlas.ts +25 -0
- package/src/check.ts +113 -24
- package/src/checkpics.ts +2 -0
- package/src/compile.ts +650 -509
- package/src/core/animation.ts +982 -0
- package/src/core/clipping.ts +696 -0
- package/src/core/constraints.ts +1721 -0
- package/src/core/constraints_path.ts +942 -0
- package/src/core/constraints_physics.ts +830 -0
- package/src/core/constraints_slider.ts +505 -0
- package/src/core/deform.ts +613 -0
- package/src/core/draw_order.ts +125 -0
- package/src/core/events.ts +135 -0
- package/src/core/hooks.ts +249 -0
- package/src/core/index.ts +1103 -0
- package/src/core/raw.ts +347 -0
- package/src/core/skins.ts +104 -0
- package/src/core/uvs.ts +448 -0
- package/src/core/vertices.ts +490 -0
- package/src/core/world.ts +233 -0
- package/src/deformmeasure.ts +337 -73
- package/src/deformreport.ts +370 -0
- package/src/depth.ts +101 -2
- package/src/emit_spine.ts +860 -0
- package/src/keys.ts +76 -2
- package/src/mesh.ts +24 -15
- package/src/model.ts +958 -0
- package/src/motion.ts +25 -5
- package/src/render.ts +601 -140
- package/src/render_core.ts +582 -0
- package/src/rig.ts +137 -41
- package/src/transform.ts +26 -4
- package/src/types.ts +50 -4
- package/src/validate.ts +142 -58
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
|
-
|
|
1229
|
-
//
|
|
1230
|
-
//
|
|
1231
|
-
//
|
|
1232
|
-
//
|
|
1233
|
-
//
|
|
1234
|
-
//
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
)
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
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 (
|
|
1246
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
2553
|
+
candidateFrames = sampleSetupPose(poser, poseOptions);
|
|
2468
2554
|
candidateAnimation = null;
|
|
2469
2555
|
} else {
|
|
2470
|
-
candidateFrames = sampleAnimation(
|
|
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,
|